From 056498e570f5da524acb0c0692771b47433cee46 Mon Sep 17 00:00:00 2001 From: Steedie Date: Mon, 13 May 2024 02:38:23 +0100 Subject: [PATCH 1/3] tutorial-room-7 --- .../src/maps/tutorial-room-7/Locations.yaml | 253 +++++++++++++++++ contracts/src/maps/tutorial-room-7/README.md | 266 ++++++++++++++++++ .../src/maps/tutorial-room-7/Unattackable.js | 65 +++++ .../maps/tutorial-room-7/Unattackable.yaml | 17 ++ contracts/src/maps/tutorial-room-7/Zone.js | 197 +++++++++++++ contracts/src/maps/tutorial-room-7/Zone.sol | 92 ++++++ contracts/src/maps/tutorial-room-7/Zone.yaml | 10 + .../readme-images/screenshot.png | 3 + .../readme-images/toggle-tiles.png | 3 + .../readme-images/unattackable.png | 3 + .../readme-images/zone-select.png | 3 + 11 files changed, 912 insertions(+) create mode 100644 contracts/src/maps/tutorial-room-7/Locations.yaml create mode 100644 contracts/src/maps/tutorial-room-7/README.md create mode 100644 contracts/src/maps/tutorial-room-7/Unattackable.js create mode 100644 contracts/src/maps/tutorial-room-7/Unattackable.yaml create mode 100644 contracts/src/maps/tutorial-room-7/Zone.js create mode 100644 contracts/src/maps/tutorial-room-7/Zone.sol create mode 100644 contracts/src/maps/tutorial-room-7/Zone.yaml create mode 100644 contracts/src/maps/tutorial-room-7/readme-images/screenshot.png create mode 100644 contracts/src/maps/tutorial-room-7/readme-images/toggle-tiles.png create mode 100644 contracts/src/maps/tutorial-room-7/readme-images/unattackable.png create mode 100644 contracts/src/maps/tutorial-room-7/readme-images/zone-select.png diff --git a/contracts/src/maps/tutorial-room-7/Locations.yaml b/contracts/src/maps/tutorial-room-7/Locations.yaml new file mode 100644 index 000000000..50ca5d238 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Locations.yaml @@ -0,0 +1,253 @@ + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 0, 0 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 1, -1 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 2, -2 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 3, -3 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 4, -4 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 5, -5 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 6, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 7, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -2, 8, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -3, 9, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 6, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 2, 6, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 3, 6, -9 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -3, 10, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -2, 9, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 8, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 7, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 7, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 2, 7, -9 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 3, 7, -10 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -2, 10, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 9, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 8, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 8, -9 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 2, 8, -10 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 10, -10 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 10, -9 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 0, 9, -9 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 9, -10 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 5, -4 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -2, 6, -4 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -3, 7, -4 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 4, -5 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 2, 4, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 3, 4, -7 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 3, 5, -8 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -3, 8, -5 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -2, 7, -5 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ -1, 6, -5 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 1, 5, -6 ] + +--- +kind: Tile +spec: + biome: DISCOVERED + location: [ 2, 5, -7 ] + +--- +kind: Building +spec: + name: Unattackable + location: [ 0, 7, -7 ] + facingDirection: LEFT \ No newline at end of file diff --git a/contracts/src/maps/tutorial-room-7/README.md b/contracts/src/maps/tutorial-room-7/README.md new file mode 100644 index 000000000..3f8911815 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/README.md @@ -0,0 +1,266 @@ +# Downstream Game Creation Tutorial 7 + +## Aim +We will follow the steps below to learn about the ZoneKind + + + +## 1. Setup +If you've followed the previous tutorials, you will already be familiar with setting up your own map. + +If not, then I recommend that you first complete these tutorials: +- [tutorial-room-1](https://github.com/playmint/ds/blob/main/contracts/src/maps/tutorial-room-1/README.md) + - to learn how to set up your map +- [tutorial-room-4](https://github.com/playmint/ds/blob/main/contracts/src/maps/tutorial-room-4/README.md) + - to learn how to colour tiles + +## 2. Create a map +Enter the [tile-fabricator](http://localhost:3000/tile-fabricator) and draw the map like this: + + + +Now that we're on `tutorial-room-7`, we are going to draw this over the `tutorial-room-1` area. + +## 3. Zone files +To implement the ZoneKind, you need to create these files: +- Zone.yaml +- Zone.sol +- Zone.js + +## Zone.yaml +Here is how I've setup the `Zone.yaml` file for this tutorial: +```yaml +--- +kind: ZoneKind +spec: + name: tutorial-room-7 + description: This zone shows off what you can do with a ZoneKind + url: images/building-totems/Block_Van.png + contract: + file: ./Zone.sol + plugin: + file: ./Zone.js +``` + +We can use this to predefine the zone's name, description, and image URL. + +When the zone is deployed, this shows up on the zone select page: + + + +We're also using the yaml file to implement logic to our zone. + +## Zone.js +In this exmaple, `Zone.js` is just being used to colour the tiles: +```js +const z = hexToSignedDecimal(state.world.key); + const middleCoords = [z, 0, 7, -7]; + const allRoomTiles = getTilesInRange(middleCoords, 3); + const walkableTiles = getWalkableTilesInRange(middleCoords, 3); + allRoomTiles.forEach((tileId) => { + if (walkableTiles.includes(tileId)) { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#32B25A", + }); + } else { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#EC5C61", + }); + } + }); +``` + +I'm using similar helper function as we used to create the dico room ([tutorial-room-4](https://github.com/playmint/ds/blob/main/contracts/src/maps/tutorial-room-4/README.md)), but with some slight modifications to create a walkable path, represented by green tiles, and an unwalkable path, represented by red tiles. + +As you can see in this modified function: +```js +function getWalkableTilesInRange(middleCoords, range) { + const [z, q, r, s] = middleCoords; + let tilesInRange = []; + for (let dx = -range; dx <= range; dx++) { + for ( + let dy = Math.max(-range, -dx - range); + dy <= Math.min(range, -dx + range); + dy++ + ) { + const dz = -dx - dy; + const theseCoords = [z, q + dx, r + dy, s + dz]; + if (theseCoords[1] == 0 || theseCoords[2] == 7 || theseCoords[3] == -7) { + const tileId = getTileIdFromCoords([z, q + dx, r + dy, s + dz]); + tilesInRange.push(tileId); + } + } + } + return tilesInRange; +} +``` + +It only pushes the tile to the list if it matches a certain coord. + +## Zone.sol +Take a look at the example `Zone.sol` file and you will notice that it may look similar to a BuildingKind solidity file. + +There are however some new concepts being used here that we haven't yet convered. + +I've predefined a list of tile IDs that we're going to use to stop units from walking on them: + +```solidity + bytes24[18] public unwalkableTiles; + + constructor() { + unwalkableTiles = [ + bytes24(0xe5a62ffc0000000000000000000000000001fffd0008fffb), + bytes24(0xe5a62ffc0000000000000000000000000001fffd0009fffa), + bytes24(0xe5a62ffc0000000000000000000000000001fffe0006fffc), + bytes24(0xe5a62ffc0000000000000000000000000001fffe0008fffa), + bytes24(0xe5a62ffc0000000000000000000000000001fffe000afff8), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0005fffc), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0006fffb), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0009fff8), + bytes24(0xe5a62ffc0000000000000000000000000001ffff000afff7), + bytes24(0xe5a62ffc000000000000000000000000000100010004fffb), + bytes24(0xe5a62ffc000000000000000000000000000100010005fffa), + bytes24(0xe5a62ffc000000000000000000000000000100010008fff7), + bytes24(0xe5a62ffc000000000000000000000000000100010009fff6), + bytes24(0xe5a62ffc000000000000000000000000000100020004fffa), + bytes24(0xe5a62ffc000000000000000000000000000100020006fff8), + bytes24(0xe5a62ffc000000000000000000000000000100020008fff6), + bytes24(0xe5a62ffc000000000000000000000000000100030005fff8), + bytes24(0xe5a62ffc000000000000000000000000000100030006fff7) + ]; + } +``` + +We're using a `constructor` to set the `unwalableTiles` array values. + +The way I created this list in the first place, was by making code in `Zone.js` that compiled the tile IDs of the unwalkable tiles that could be copy & pasted straight into the solidity code: +```js +// // code to compile unwalkable tile IDs + // let string = "unwalkableTiles = [\n"; + // printThese.forEach((tileId) => { + // string += "bytes24(" + tileId + "),\n"; + // }); + // string += "];"; + // console.log(string); +``` +In the example `Zone.js` file, uncomment this bit of the code as well as: +```js +//printThese.push(tileId); +``` +to have the tile IDs printed to the browser's developer console. + +`ZoneKind` allows us to hook in to particular events and execute logic when something happens. + +See all the hooks we can use here: https://github.com/playmint/ds/blob/main/contracts/src/ext/ZoneKind.sol + +In our example, we're using: +- use +- onCombatStart +- onUnitArrive + +### onUnitArive +```solidity + function onUnitArrive(Game ds, bytes24 /*zoneID*/, bytes24 mobileUnitID) external override { + State state = ds.getState(); + + bytes24 tile = state.getNextLocation(mobileUnitID); + (, int16 q, int16 r, int16 s) = getTileCoords(tile); + + bool canWalkHere = true; + for (uint256 i = 0; i < unwalkableTiles.length; i++) { + (, int16 q2, int16 r2, int16 s2) = getTileCoords(unwalkableTiles[i]); + if (q == q2 && r == r2 && s == s2) { + canWalkHere = false; + break; + } + } + require(canWalkHere, "Zone logic is stopping you from walking here"); + } +``` +This code checks where the unit is trying to move to. If they're trying to move to a tile that exists in the `unwalkableTiles` array, the movement will not be allowed. + +### onCombatStart +```solidity + function onCombatStart(Game /*ds*/, bytes24 /*zoneID*/, bytes24 /*mobileUnitID*/, bytes24 /*sessionID*/) external pure override { + revert("Combat is disabled in this zone"); + } +``` +Because we have access to this hook, we can do things like removing combat in a zone, or perhaps limitting combat based on game logic. In this example, we're simply disabling combat as a whole. + +With this implemented, when a unit tries to enter combat in this zone, nothing will happen: + + + +### use +The ZoneKind has the unique ability to be able to access "dev" actions. Which are some of the actions that are used during the `ds apply`/`ds destroy` commands like spawning and removing buildings and tiles. + +For the zone, instead of dispatching `BUILDING_USE`, we dispatch `ZONE_USE`, and it interact with the zone's solidity logic. + +We've added this function: +```solidity + function toggleUnwalkableTiles(bytes24 b) external {} +``` +Which we're going to use to call `DEV_SPAWN_TILE`/`DEV_DESTROY_TILE` on all the `unwalkableTiles`. + +```solidity +function use(Game ds, bytes24, /*zoneID*/ bytes24, /*mobileUnitID*/ bytes calldata payload) public override { + State state = ds.getState(); + if ((bytes4)(payload) == this.toggleUnwalkableTiles.selector) { + // Getting zone using buildingInstance + (bytes24 buildingInstance) = abi.decode(payload[4:], (bytes24)); + bytes24 buildingTile = state.getFixedLocation(buildingInstance); + (int16 z,,,) = getTileCoords(buildingTile); + + for (uint256 i = 0; i < unwalkableTiles.length; i++) { + bytes24 tile = unwalkableTiles[i]; + (, int16 q, int16 r, int16 s) = getTileCoords(tile); + if (showingWalkableTiles) { + ds.getDispatcher().dispatch(abi.encodeCall(Actions.DEV_DESTROY_TILE, (z, q, r, s))); + } else { + ds.getDispatcher().dispatch(abi.encodeCall(Actions.DEV_SPAWN_TILE, (z, q, r, s))); + } + } + + showingWalkableTiles = !showingWalkableTiles; + } + } +``` + +Note see what function way passed in via the payload: +```solidity +if ((bytes4)(payload) == this.toggleUnwalkableTiles.selector) +``` + +This is being dispatched from `Unattackable.js`: +```js +const toggleUnwalkableTiles = () => { + const mobileUnit = getMobileUnit(state); + if (!mobileUnit) { + console.log("no selected unit"); + return; + } + + const payload = ds.encodeCall("function toggleUnwalkableTiles(bytes24)", [ + selectedBuilding.id, + ]); + + ds.dispatch({ + name: "ZONE_USE", + args: [mobileUnit.id, payload], + }); + }; +``` + +Here you can see the use of `ZONE_USE`. + +(View the example code to create a simple building with a button if you haven't already) + +Now, when the button on this building is clicked, the tiles either get spawned, or destroyed: + + \ No newline at end of file diff --git a/contracts/src/maps/tutorial-room-7/Unattackable.js b/contracts/src/maps/tutorial-room-7/Unattackable.js new file mode 100644 index 000000000..c034b84c2 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Unattackable.js @@ -0,0 +1,65 @@ +import ds from "downstream"; + +export default async function update(state) { + + const selectedTile = getSelectedTile(state); + const selectedBuilding = + selectedTile && getBuildingOnTile(state, selectedTile); + + const toggleUnwalkableTiles = () => { + const mobileUnit = getMobileUnit(state); + if (!mobileUnit) { + console.log("no selected unit"); + return; + } + + const payload = ds.encodeCall("function toggleUnwalkableTiles(bytes24)", [ + selectedBuilding.id, + ]); + + ds.dispatch({ + name: "ZONE_USE", + args: [mobileUnit.id, payload], + }); + }; + + return { + version: 1, + components: [ + { + id: "unattackable", + type: "building", + content: [ + { + id: "default", + type: "inline", + html: "

This button calls dispatches the ZONE_USE action

", + buttons: [ + { + text: "Toggle Tiles", + type: "action", + action: toggleUnwalkableTiles, + disabled: false, + }, + ], + }, + ], + }, + ], + }; +} + +function getMobileUnit(state) { + return state?.selected?.mobileUnit; +} + +function getSelectedTile(state) { + const tiles = state?.selected?.tiles || {}; + return tiles && tiles.length === 1 ? tiles[0] : undefined; +} + +function getBuildingOnTile(state, tile) { + return (state?.world?.buildings || []).find( + (b) => tile && b.location?.tile?.id === tile.id, + ); +} \ No newline at end of file diff --git a/contracts/src/maps/tutorial-room-7/Unattackable.yaml b/contracts/src/maps/tutorial-room-7/Unattackable.yaml new file mode 100644 index 000000000..370511d25 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Unattackable.yaml @@ -0,0 +1,17 @@ +--- +kind: BuildingKind +spec: + name: Unattackable + description: Ha ha you can't touch me... + category: custom + model: 05-05 + color: 0 + plugin: + file: ./Unattackable.js + materials: + - name: Red Goo + quantity: 10 + - name: Blue Goo + quantity: 10 + - name: Green Goo + quantity: 10 \ No newline at end of file diff --git a/contracts/src/maps/tutorial-room-7/Zone.js b/contracts/src/maps/tutorial-room-7/Zone.js new file mode 100644 index 000000000..410239801 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Zone.js @@ -0,0 +1,197 @@ +import ds from "downstream"; + +const TILE_ID_PREFIX = "0xe5a62ffc"; + +export default async function update(state) { + const map = []; + //console.log(state); + + const mobileUnit = getMobileUnit(state); + + const printThese = []; + + const z = hexToSignedDecimal(state.world.key); + const middleCoords = [z, 0, 7, -7]; + const allRoomTiles = getTilesInRange(middleCoords, 3); + const walkableTiles = getWalkableTilesInRange(middleCoords, 3); + allRoomTiles.forEach((tileId) => { + if (walkableTiles.includes(tileId)) { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#32B25A", + }); + } else { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#EC5C61", + }); + //printThese.push(tileId); + } + }); + + // // code to compile unwalkable tile IDs + // let string = "unwalkableTiles = [\n"; + // printThese.forEach((tileId) => { + // string += "bytes24(" + tileId + "),\n"; + // }); + // string += "];"; + // console.log(string); + + return { + version: 1, + map: map, + components: [], + }; +} + +// --- Helper functions --- + +// Get the mobile unit from the state +function getMobileUnit(state) { + return state?.selected?.mobileUnit; +} + +// Convert hexadecimal to signed decimal +function hexToSignedDecimal(hex) { + if (hex.startsWith("0x")) { + hex = hex.substr(2); + } + + let num = parseInt(hex, 16); + let bits = hex.length * 4; + let maxVal = Math.pow(2, bits); + + // Check if the highest bit is set (negative number) + if (num >= maxVal / 2) { + num -= maxVal; + } + + return num; +} + +// Get tile coordinates from hexadecimal coordinates +function getTileCoords(coords) { + return [ + hexToSignedDecimal(coords[0]), + hexToSignedDecimal(coords[1]), + hexToSignedDecimal(coords[2]), + hexToSignedDecimal(coords[3]), + ]; +} + +// Calculate distance between two tiles +function distance(tileCoords, nextTile) { + return Math.max( + Math.abs(tileCoords[0] - nextTile[0]), + Math.abs(tileCoords[1] - nextTile[1]), + Math.abs(tileCoords[2] - nextTile[2]), + ); +} + +// Convert an integer to a 16-bit hexadecimal string +function toInt16Hex(value) { + return ("0000" + toTwos(value, 16).toString(16)).slice(-4); +} + +const BN_0 = BigInt(0); +const BN_1 = BigInt(1); + +// Convert a two's complement binary representation to a BigInt +function fromTwos(n, w) { + let value = BigInt(n); + let width = BigInt(w); + if (value >> (width - BN_1)) { + const mask = (BN_1 << width) - BN_1; + return -((~value & mask) + BN_1); + } + return value; +} + +// Convert a BigInt to a two's complement binary representation +function toTwos(_value, _width) { + let value = BigInt(_value); + let width = BigInt(_width); + const limit = BN_1 << (width - BN_1); + if (value < BN_0) { + value = -value; + const mask = (BN_1 << width) - BN_1; + return (~value & mask) + BN_1; + } + return value; +} + +// Get tile ID from coordinates +function getTileIdFromCoords(coords) { + const z = toInt16Hex(coords[0]); + const q = toInt16Hex(coords[1]); + const r = toInt16Hex(coords[2]); + const s = toInt16Hex(coords[3]); + return `${TILE_ID_PREFIX}000000000000000000000000${z}${q}${r}${s}`; +} + +// Decode a tile ID into its q, r, s hexagonal coordinates +function getTileCoordsFromId(tileId) { + const coords = [...tileId] + .slice(2) + .reduce((bs, b, idx) => { + if (idx % 4 === 0) { + bs.push("0x"); + } + bs[bs.length - 1] += b; + return bs; + }, []) + .map((n) => Number(fromTwos(n, 16))) + .slice(-4); + if (coords.length !== 4) { + throw new Error(`failed to get z,q,r,s from tile id ${tileId}`); + } + return coords; +} + +function getTilesInRange(middleCoords, range) { + const [z, q, r, s] = middleCoords; + let tilesInRange = []; + for (let dx = -range; dx <= range; dx++) { + for ( + let dy = Math.max(-range, -dx - range); + dy <= Math.min(range, -dx + range); + dy++ + ) { + const dz = -dx - dy; + const tileId = getTileIdFromCoords([z, q + dx, r + dy, s + dz]); + tilesInRange.push(tileId); + } + } + return tilesInRange; +} + +function getWalkableTilesInRange(middleCoords, range) { + const [z, q, r, s] = middleCoords; + let tilesInRange = []; + for (let dx = -range; dx <= range; dx++) { + for ( + let dy = Math.max(-range, -dx - range); + dy <= Math.min(range, -dx + range); + dy++ + ) { + const dz = -dx - dy; + const theseCoords = [z, q + dx, r + dy, s + dz]; + if (theseCoords[1] == 0 || theseCoords[2] == 7 || theseCoords[3] == -7) { + const tileId = getTileIdFromCoords([z, q + dx, r + dy, s + dz]); + tilesInRange.push(tileId); + } + } + } + return tilesInRange; +} + +function logState(state) { + console.log("State sent to pluging:", state); +} + +// the source for this code is on github where you can find other example buildings: +// https://github.com/playmint/ds/tree/main/contracts/src/example-plugins diff --git a/contracts/src/maps/tutorial-room-7/Zone.sol b/contracts/src/maps/tutorial-room-7/Zone.sol new file mode 100644 index 000000000..bf347c009 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Zone.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Game} from "cog/IGame.sol"; +import {State, CompoundKeyDecoder} from "cog/IState.sol"; +import {Schema, CombatWinState} from "@ds/schema/Schema.sol"; +import {ZoneKind} from "@ds/ext/ZoneKind.sol"; +import {Actions} from "@ds/actions/Actions.sol"; + +using Schema for State; + +contract Zone is ZoneKind { + function toggleUnwalkableTiles(bytes24 b) external {} + + bool public showingWalkableTiles = true; + bytes24[18] public unwalkableTiles; + + constructor() { + unwalkableTiles = [ + bytes24(0xe5a62ffc0000000000000000000000000001fffd0008fffb), + bytes24(0xe5a62ffc0000000000000000000000000001fffd0009fffa), + bytes24(0xe5a62ffc0000000000000000000000000001fffe0006fffc), + bytes24(0xe5a62ffc0000000000000000000000000001fffe0008fffa), + bytes24(0xe5a62ffc0000000000000000000000000001fffe000afff8), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0005fffc), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0006fffb), + bytes24(0xe5a62ffc0000000000000000000000000001ffff0009fff8), + bytes24(0xe5a62ffc0000000000000000000000000001ffff000afff7), + bytes24(0xe5a62ffc000000000000000000000000000100010004fffb), + bytes24(0xe5a62ffc000000000000000000000000000100010005fffa), + bytes24(0xe5a62ffc000000000000000000000000000100010008fff7), + bytes24(0xe5a62ffc000000000000000000000000000100010009fff6), + bytes24(0xe5a62ffc000000000000000000000000000100020004fffa), + bytes24(0xe5a62ffc000000000000000000000000000100020006fff8), + bytes24(0xe5a62ffc000000000000000000000000000100020008fff6), + bytes24(0xe5a62ffc000000000000000000000000000100030005fff8), + bytes24(0xe5a62ffc000000000000000000000000000100030006fff7) + ]; + } + + function use(Game ds, bytes24, /*zoneID*/ bytes24, /*mobileUnitID*/ bytes calldata payload) public override { + State state = ds.getState(); + if ((bytes4)(payload) == this.toggleUnwalkableTiles.selector) { + // Getting zone using buildingInstance + (bytes24 buildingInstance) = abi.decode(payload[4:], (bytes24)); + bytes24 buildingTile = state.getFixedLocation(buildingInstance); + (int16 z,,,) = getTileCoords(buildingTile); + + for (uint256 i = 0; i < unwalkableTiles.length; i++) { + bytes24 tile = unwalkableTiles[i]; + (, int16 q, int16 r, int16 s) = getTileCoords(tile); + if (showingWalkableTiles) { + ds.getDispatcher().dispatch(abi.encodeCall(Actions.DEV_DESTROY_TILE, (z, q, r, s))); + } else { + ds.getDispatcher().dispatch(abi.encodeCall(Actions.DEV_SPAWN_TILE, (z, q, r, s))); + } + } + + showingWalkableTiles = !showingWalkableTiles; + } + } + + function getTileCoords(bytes24 tile) internal pure returns (int16, int16, int16, int16) { + int16[4] memory keys = CompoundKeyDecoder.INT16_ARRAY(tile); + return (keys[0], keys[1], keys[2], keys[3]); + } + + function onUnitArrive(Game ds, bytes24, /*zoneID*/ bytes24 mobileUnitID) external override { + State state = ds.getState(); + + bytes24 tile = state.getNextLocation(mobileUnitID); + (, int16 q, int16 r, int16 s) = getTileCoords(tile); + + bool canWalkHere = true; + for (uint256 i = 0; i < unwalkableTiles.length; i++) { + (, int16 q2, int16 r2, int16 s2) = getTileCoords(unwalkableTiles[i]); + if (q == q2 && r == r2 && s == s2) { + canWalkHere = false; + break; + } + } + require(canWalkHere, "Zone logic is stopping you from walking here"); + } + + function onCombatStart(Game, /*ds*/ bytes24, /*zoneID*/ bytes24, /*mobileUnitID*/ bytes24 /*sessionID*/ ) + external + pure + override + { + revert("Combat is disabled in this zone"); + } +} diff --git a/contracts/src/maps/tutorial-room-7/Zone.yaml b/contracts/src/maps/tutorial-room-7/Zone.yaml new file mode 100644 index 000000000..a35e5d2b7 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/Zone.yaml @@ -0,0 +1,10 @@ +--- +kind: ZoneKind +spec: + name: tutorial-room-7 + description: This zone shows off what you can do with a ZoneKind + url: images/building-totems/Block_Van.png + contract: + file: ./Zone.sol + plugin: + file: ./Zone.js diff --git a/contracts/src/maps/tutorial-room-7/readme-images/screenshot.png b/contracts/src/maps/tutorial-room-7/readme-images/screenshot.png new file mode 100644 index 000000000..a89d30e77 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/readme-images/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5061fee4278588b6538b94274aa9d4218d01963008054cd9a4515647793cea88 +size 403746 diff --git a/contracts/src/maps/tutorial-room-7/readme-images/toggle-tiles.png b/contracts/src/maps/tutorial-room-7/readme-images/toggle-tiles.png new file mode 100644 index 000000000..b34414a8d --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/readme-images/toggle-tiles.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e7660f0b78346512091345a5ad4394fc6785a6a23fa8d990a9b76195425e0890 +size 388789 diff --git a/contracts/src/maps/tutorial-room-7/readme-images/unattackable.png b/contracts/src/maps/tutorial-room-7/readme-images/unattackable.png new file mode 100644 index 000000000..aa2587a05 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/readme-images/unattackable.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db9015b92139b8844197a0888e01136f9045c63f1829f1d46e3dd26ef8837fac +size 360965 diff --git a/contracts/src/maps/tutorial-room-7/readme-images/zone-select.png b/contracts/src/maps/tutorial-room-7/readme-images/zone-select.png new file mode 100644 index 000000000..4a1c41f90 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/readme-images/zone-select.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:05b0f6fa1e862a281feb393257622affb0239ef951ca7878c3e659aad764e901 +size 88886 From 5c7c2fd5063aad0a921b06f06b21c256a58b0689 Mon Sep 17 00:00:00 2001 From: Steedie Date: Mon, 13 May 2024 09:22:27 +0100 Subject: [PATCH 2/3] clarity, spelling & grammar --- contracts/src/maps/tutorial-room-7/README.md | 34 ++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/contracts/src/maps/tutorial-room-7/README.md b/contracts/src/maps/tutorial-room-7/README.md index 3f8911815..93b73351b 100644 --- a/contracts/src/maps/tutorial-room-7/README.md +++ b/contracts/src/maps/tutorial-room-7/README.md @@ -51,7 +51,7 @@ When the zone is deployed, this shows up on the zone select page: We're also using the yaml file to implement logic to our zone. ## Zone.js -In this exmaple, `Zone.js` is just being used to colour the tiles: +In this example, `Zone.js` is just being used to colour the tiles: ```js const z = hexToSignedDecimal(state.world.key); const middleCoords = [z, 0, 7, -7]; @@ -106,7 +106,27 @@ It only pushes the tile to the list if it matches a certain coord. ## Zone.sol Take a look at the example `Zone.sol` file and you will notice that it may look similar to a BuildingKind solidity file. -There are however some new concepts being used here that we haven't yet convered. +There are however some new concepts being used here that we haven't yet covered. + +In our example, we'll setup the file like this: +```solidity +pragma solidity ^0.8.13; + +import {Game} from "cog/IGame.sol"; +import {State, CompoundKeyDecoder} from "cog/IState.sol"; +import {Schema, CombatWinState} from "@ds/schema/Schema.sol"; +import {ZoneKind} from "@ds/ext/ZoneKind.sol"; +import {Actions} from "@ds/actions/Actions.sol"; + +using Schema for State; + +contract Zone is ZoneKind { + ... +``` + +We're importing `ZoneKind` so we can make use of the built-in hooks. + + I've predefined a list of tile IDs that we're going to use to stop units from walking on them: @@ -137,7 +157,7 @@ I've predefined a list of tile IDs that we're going to use to stop units from wa } ``` -We're using a `constructor` to set the `unwalableTiles` array values. +We're using a `constructor` to set the `unwalkableTiles` array values. The way I created this list in the first place, was by making code in `Zone.js` that compiled the tile IDs of the unwalkable tiles that could be copy & pasted straight into the solidity code: ```js @@ -185,13 +205,15 @@ In our example, we're using: ``` This code checks where the unit is trying to move to. If they're trying to move to a tile that exists in the `unwalkableTiles` array, the movement will not be allowed. +In our case, this is all the red tiles. Once you've implemented this, you can apply the map, and notice the unit will skip over the red tiles. + ### onCombatStart ```solidity function onCombatStart(Game /*ds*/, bytes24 /*zoneID*/, bytes24 /*mobileUnitID*/, bytes24 /*sessionID*/) external pure override { revert("Combat is disabled in this zone"); } ``` -Because we have access to this hook, we can do things like removing combat in a zone, or perhaps limitting combat based on game logic. In this example, we're simply disabling combat as a whole. +Because we have access to this hook, we can do things like removing combat in a zone, or perhaps limiting combat based on game logic. In this example, we're simply disabling combat as a whole. With this implemented, when a unit tries to enter combat in this zone, nothing will happen: @@ -200,7 +222,7 @@ With this implemented, when a unit tries to enter combat in this zone, nothing w ### use The ZoneKind has the unique ability to be able to access "dev" actions. Which are some of the actions that are used during the `ds apply`/`ds destroy` commands like spawning and removing buildings and tiles. -For the zone, instead of dispatching `BUILDING_USE`, we dispatch `ZONE_USE`, and it interact with the zone's solidity logic. +For the zone, instead of dispatching `BUILDING_USE`, we dispatch `ZONE_USE`, and it interacts with the zone's solidity logic. We've added this function: ```solidity @@ -232,7 +254,7 @@ function use(Game ds, bytes24, /*zoneID*/ bytes24, /*mobileUnitID*/ bytes callda } ``` -Note see what function way passed in via the payload: +Note see what function was passed in via the payload: ```solidity if ((bytes4)(payload) == this.toggleUnwalkableTiles.selector) ``` From 2fde8f60bfe1af5adb4093c2718f917d2d08c7a7 Mon Sep 17 00:00:00 2001 From: Steedie Date: Mon, 13 May 2024 17:02:51 +0100 Subject: [PATCH 3/3] blocker tile & readme improvements --- contracts/src/maps/tutorial-room-7/README.md | 48 ++++++++++++++++++- contracts/src/maps/tutorial-room-7/Zone.js | 9 +++- .../readme-images/path-finding.png | 3 ++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 contracts/src/maps/tutorial-room-7/readme-images/path-finding.png diff --git a/contracts/src/maps/tutorial-room-7/README.md b/contracts/src/maps/tutorial-room-7/README.md index 93b73351b..0c5e766fc 100644 --- a/contracts/src/maps/tutorial-room-7/README.md +++ b/contracts/src/maps/tutorial-room-7/README.md @@ -21,6 +21,19 @@ Enter the [tile-fabricator](http://localhost:3000/tile-fabricator) and draw the Now that we're on `tutorial-room-7`, we are going to draw this over the `tutorial-room-1` area. +### ds destroy + +If you already have `tutorial-room-1` applied to your zone, then you may want to use the `ds destroy` command to remove it before applying the new map over the top. + +`ds destroy` works similarly to `ds apply`, but it essentially does the oposite. + +Navigate to the `tutorial-room-1` map directory and run this command: +```ds +ds destroy -n -z -R -f . +``` + +You will then see the tiles and buildings being destroyed in the game, and in the console output. + ## 3. Zone files To implement the ZoneKind, you need to create these files: - Zone.yaml @@ -76,7 +89,7 @@ const z = hexToSignedDecimal(state.world.key); }); ``` -I'm using similar helper function as we used to create the dico room ([tutorial-room-4](https://github.com/playmint/ds/blob/main/contracts/src/maps/tutorial-room-4/README.md)), but with some slight modifications to create a walkable path, represented by green tiles, and an unwalkable path, represented by red tiles. +I'm using similar helper function as we used to create the disco room ([tutorial-room-4](https://github.com/playmint/ds/blob/main/contracts/src/maps/tutorial-room-4/README.md)), but with some slight modifications to create a walkable path, represented by green tiles, and an unwalkable path, represented by red tiles. As you can see in this modified function: ```js @@ -207,6 +220,39 @@ This code checks where the unit is trying to move to. If they're trying to move In our case, this is all the red tiles. Once you've implemented this, you can apply the map, and notice the unit will skip over the red tiles. +Once you've confirmed that the unit is unable to walk on the red tiles, we can modify our plugin code so that the pathfinding on the frontend completely avoids walking on the red tiles: +```js +allRoomTiles.forEach((tileId) => { + if (walkableTiles.includes(tileId)) { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#32B25A", + }); + } else { + map.push({ + type: "tile", + key: "color", + id: tileId, + value: "#EC5C61", + }, + // ADD THIS TO TURN THE RED TILES INTO "blocker" TILES + { + type: "tile", + key: "blocker", + id: tileId, + value: 'true', + }, + ); + } + }); +``` + +With this change, you should see that the pathfinding is going around the red tiles: + + + ### onCombatStart ```solidity function onCombatStart(Game /*ds*/, bytes24 /*zoneID*/, bytes24 /*mobileUnitID*/, bytes24 /*sessionID*/) external pure override { diff --git a/contracts/src/maps/tutorial-room-7/Zone.js b/contracts/src/maps/tutorial-room-7/Zone.js index 410239801..be42d55df 100644 --- a/contracts/src/maps/tutorial-room-7/Zone.js +++ b/contracts/src/maps/tutorial-room-7/Zone.js @@ -28,7 +28,14 @@ export default async function update(state) { key: "color", id: tileId, value: "#EC5C61", - }); + }, + { + type: "tile", + key: "blocker", + id: tileId, + value: 'true', + }, + ); //printThese.push(tileId); } }); diff --git a/contracts/src/maps/tutorial-room-7/readme-images/path-finding.png b/contracts/src/maps/tutorial-room-7/readme-images/path-finding.png new file mode 100644 index 000000000..ba11d8b06 --- /dev/null +++ b/contracts/src/maps/tutorial-room-7/readme-images/path-finding.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:45b0e82865be3a0ef1c8bf4e186635ce4efb234dbcb8a09dc254332963e726bc +size 402639