Lattice Workshop - Step 5

Add more actions and rules

Next step

There is not much entities can do so far, so let's add more functionality and some more rules.

First we add a utility function to check for entities with a given component at a given coordinate. For this we use the LibQuery library provided by lattice-ecs.

Next extend the action function. We check for mined tiles at the given coordinate. If there is none and the entity has the Miner component, we add a mined tile at the target coord. If there is a mined tile, the target coord is unoccupoed and the entity has the Movable compoent, we move the entity to the target coord. Lastly, if the target tile is mined but occupied by an entity with Life component and the calling entity has the Attack component, we trigger a combat by calling the combat utility.

The combat utility decreases the health value of the attacked entity by the Attack component value of the attacking entity (and removes the attacked entity if its life points reach 0).

After extending the action method like this, the game is finally playable! You can select entities, move them around, mine tiles and attack other entities.

In the following steps we'll add some juice to make the game look and feel nicer.

Files changed (1) hide show
  1. contracts/src/Game.sol +53 -2
contracts/src/Game.sol CHANGED
@@ -4,6 +4,7 @@ pragma solidity >=0.8.13;
4
4
  import { console } from 'forge-std/console.sol';
5
5
  import { World } from 'lattice-ecs/World.sol';
6
6
  import { Component } from 'lattice-ecs/Component.sol';
7
+ import { QueryFragment, QueryType, LibQuery } from 'lattice-ecs/LibQuery.sol';
7
8
  import { CoordComponent, Coord } from './components/CoordComponent.sol';
8
9
  import { UintComponent } from './components/UintComponent.sol';
9
10
  import { StringComponent } from './components/StringComponent.sol';
@@ -85,6 +86,18 @@ contract Game {
85
86
  }
86
87
  }
87
88
 
89
+ /**
90
+ * Utility function to get all entities with a given componnt at the given coord
91
+ */
92
+ function getEntityWithAt(Component component, Coord memory coord) internal view returns (uint256 entity, bool found) {
93
+ QueryFragment[] memory fragments = new QueryFragment[](2);
94
+ fragments[0] = QueryFragment(QueryType.HasValue, c.position, abi.encode(coord));
95
+ fragments[1] = QueryFragment(QueryType.Has, component, new bytes(0));
96
+ uint256[] memory entities = LibQuery.query(fragments);
97
+ if (entities.length == 0) return (0, false);
98
+ return (entities[0], true);
99
+ }
100
+
88
101
  function mine(Coord memory coord) internal {
89
102
  uint256 tile = World(world).getNumEntities();
90
103
  c.position.set(tile, coord);
@@ -92,6 +105,19 @@ contract Game {
92
105
  c.appearance.set(tile, uint256(Texture.Ground));
93
106
  }
94
107
 
108
+ function combat(uint256 attacker, uint256 defender) internal {
109
+ uint256 atkValue = c.attack.getValue(attacker);
110
+ (uint256 life, uint256 max) = c.life.getValue(defender);
111
+ if (atkValue >= life) {
112
+ // Critical hit, set defender's life to 0 and remove the defender
113
+ c.life.set(defender, 0, max);
114
+ remove(defender);
115
+ } else {
116
+ // Non-critical hit, decrease life
117
+ c.life.set(defender, life - atkValue, max);
118
+ }
119
+ }
120
+
95
121
  function spawn(Coord memory center) public {
96
122
  // Check player is not spawned yet
97
123
  require(c.ownedBy.getEntitiesWithValue(msg.sender).length == 0, 'already spawned');
@@ -136,11 +162,36 @@ contract Game {
136
162
  }
137
163
 
138
164
  function action(uint256 entity, Coord memory target) public onlyEntityOwner(entity) onlyAdjacent(entity, target) {
139
- // If the enity can move, move the entity
140
- if (c.movable.has(entity)) {
165
+ // Check for mined tiles at the target coord
166
+ (uint256 targetEntity, bool foundTargetEntity) = getEntityWithAt(c.mined, target);
167
+
168
+ // If the target coord is not mined and the active entity can mine, mine the target tile
169
+ if (!foundTargetEntity && c.miner.has(entity)) {
170
+ return mine(target);
171
+ }
172
+
173
+ // If the target tile is mined and unoccupied and the active enity can move, move there
174
+ // (Unoccupied and mined means only the mined tile entity has this position)
175
+ if (foundTargetEntity && c.position.getEntitiesWithValue(target).length == 1 && c.movable.has(entity)) {
141
176
  return c.position.set(entity, target);
142
177
  }
143
178
 
179
+ // If the target tile is occupied by an entity with life and the active entity can attack, attack the target entity
180
+ (targetEntity, foundTargetEntity) = getEntityWithAt(c.life, target);
181
+ if (foundTargetEntity && c.attack.has(entity)) {
182
+ // Target entity attacks first
183
+ if (c.attack.has(targetEntity) && c.life.has(entity)) {
184
+ combat(targetEntity, entity);
185
+ }
186
+
187
+ // Calling entity attacks second
188
+ if (c.attack.has(entity) && c.life.has(targetEntity)) {
189
+ combat(entity, targetEntity);
190
+ }
191
+
192
+ return;
193
+ }
194
+
144
195
  revert('Invalid action');
145
196
  }
146
197
  }