From 72179360e68bbfd9f278fde786217948b07cf1e7 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Wed, 1 Jan 2020 11:59:15 -0800 Subject: [PATCH 01/10] Memoize canvas check --- components/Artboard/Artboard.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/components/Artboard/Artboard.tsx b/components/Artboard/Artboard.tsx index bc69a0e..412fa7b 100644 --- a/components/Artboard/Artboard.tsx +++ b/components/Artboard/Artboard.tsx @@ -1,6 +1,6 @@ import { Container, Sprite, Stage } from "@inlet/react-pixi"; import { settings, utils, SCALE_MODES, Texture } from "pixi.js"; -import React, { memo, useState } from "react"; +import React, { memo, useState, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import * as coordinates from "../../lib/coordinates"; import { useHotKey } from "../../lib/useHotKey"; @@ -19,18 +19,6 @@ utils.skipHello(); const LEFT_MOUSE_BUTTON = 1; -const webGlSupported = () => { - try { - var canvas = document.createElement("canvas"); - return !!( - window.WebGLRenderingContext && - (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")) - ); - } catch (e) { - return false; - } -}; - interface ArtboardProps { chunks: Chunk[]; } @@ -45,13 +33,24 @@ const Artboard: React.FC = ({ chunks }) => { endY: 0, }); const keysPressed = useHotKey(); + const webGlSupported = useMemo(() => { + try { + var canvas = document.createElement("canvas"); + return !!( + window.WebGLRenderingContext && + (canvas.getContext("webgl") || canvas.getContext("experimental-webgl")) + ); + } catch (e) { + return false; + } + }, []); return ( Date: Wed, 1 Jan 2020 16:23:34 -0800 Subject: [PATCH 02/10] Allow placement of multitile buildings. --- components/Artboard/Artboard.tsx | 20 ++++++-- components/Cursor.tsx | 11 ++--- cypress/integration/multitile.spec.ts | 28 +++++++++++ lib/coordinates.test.ts | 69 +++++++++++++++++++++++++++ lib/coordinates.ts | 18 +++++++ store/actions/tilesActions.ts | 17 +++---- store/reducers/tilesReducer.ts | 55 +++++++++++++++++++-- store/selectors/tilesSelectors.ts | 17 +------ store/types.ts | 1 + 9 files changed, 198 insertions(+), 38 deletions(-) create mode 100644 cypress/integration/multitile.spec.ts create mode 100644 lib/coordinates.test.ts diff --git a/components/Artboard/Artboard.tsx b/components/Artboard/Artboard.tsx index 412fa7b..08815ee 100644 --- a/components/Artboard/Artboard.tsx +++ b/components/Artboard/Artboard.tsx @@ -8,6 +8,7 @@ import { tilesActions } from "../../store/actions"; import { selectSelection, selectSelectionOffset, + selectCurrentCommand, } from "../../store/reducers/toolReducer"; import { selectChunks } from "../../store/selectors"; import { Chunk, Coords, SelectedCoords, TileSprite } from "../../store/types"; @@ -22,7 +23,7 @@ const LEFT_MOUSE_BUTTON = 1; interface ArtboardProps { chunks: Chunk[]; } -const Artboard: React.FC = ({ chunks }) => { +const Artboard = ({ chunks }: ArtboardProps) => { const selection = useSelector(selectSelection); const selectionOffset = useSelector(selectSelectionOffset); const dispatch = useDispatch(); @@ -32,6 +33,13 @@ const Artboard: React.FC = ({ chunks }) => { endX: 0, endY: 0, }); + const cursorSize = useSelector(state => { + const command = selectCurrentCommand(state); + if ("width" in command && "height" in command) { + return { width: command.width, height: command.height }; + } + return { width: 1, height: 1 }; + }); const keysPressed = useHotKey(); const webGlSupported = useMemo(() => { try { @@ -81,7 +89,13 @@ const Artboard: React.FC = ({ chunks }) => { const key = `${chunk.startX},${chunk.endY}`; return ; })} - + {selection && ( )} @@ -93,7 +107,7 @@ const Artboard: React.FC = ({ chunks }) => { interface ChunkProps { tiles: TileSprite[]; } -const ChunkTiles: React.FunctionComponent = memo(({ tiles }) => { +const ChunkTiles = memo(({ tiles }: ChunkProps) => { return ( <> {tiles.map(tile => { diff --git a/components/Cursor.tsx b/components/Cursor.tsx index 92c663d..ed6975e 100644 --- a/components/Cursor.tsx +++ b/components/Cursor.tsx @@ -5,17 +5,12 @@ import React from "react"; const TILE_SIZE = 16; interface CursorProps { - endX?: number; - endY?: number; + endX: number | undefined; + endY: number | undefined; startX: number; startY: number; } -export const Cursor: React.FunctionComponent = ({ - startX, - startY, - endX, - endY, -}) => { +export const Cursor = ({ startX, startY, endX, endY }: CursorProps) => { return ( { + beforeEach(() => { + cy.visit("/"); + }); + + it("writes multitile buildings in one click", () => { + cy.visit("/"); + cy.getId({ name: "tool", item: "paint-rectangle" }).click(); + cy.getId("stage").then( + dragTiles({ startX: 1, startY: 1, endX: 3, endY: 3 }), + ); + cy.getId({ name: "tool", item: "paint" }).click(); + cy.getId({ name: "phase", item: "build" }).click(); + cy.getId({ name: "command", item: "farmersWorkshop" }).click(); + + cy.getId("stage").then(clickTile({ x: 1, y: 1 })); + cy.getId("export").click(); + cy.getId({ name: "export-text", item: "build" }).should( + "have.value", + `#build +ww,ww,ww +ww,ww,ww +ww,ww,ww`, + ); + }); +}); diff --git a/lib/coordinates.test.ts b/lib/coordinates.test.ts new file mode 100644 index 0000000..a216881 --- /dev/null +++ b/lib/coordinates.test.ts @@ -0,0 +1,69 @@ +import * as coordinates from "./coordinates"; + +describe("coordinates", () => { + describe("neighborIds", () => { + it("returns an array of neighboring ids for the given id with defaults", () => { + const neighborIds = coordinates.neighborIds({ x: 2, y: 2 }); + expect(neighborIds).toEqual([ + "1,1", + "2,1", + "3,1", + "1,2", + "2,2", + "3,2", + "1,3", + "2,3", + "3,3", + ]); + }); + + it("returns an array of neighboring ids for the given id with custom distance", () => { + const neighborIds = coordinates.neighborIds({ x: 3, y: 3 }, 2); + expect(neighborIds).toEqual([ + "1,1", + "2,1", + "3,1", + "4,1", + "5,1", + "1,2", + "2,2", + "3,2", + "4,2", + "5,2", + "1,3", + "2,3", + "3,3", + "4,3", + "5,3", + "1,4", + "2,4", + "3,4", + "4,4", + "5,4", + "1,5", + "2,5", + "3,5", + "4,5", + "5,5", + ]); + }); + + it("returns an array of neighboring ids for the given id with custom offset", () => { + const neighborIds = coordinates.neighborIds({ x: 1, y: 1 }, 1, { + x: 1, + y: 1, + }); + expect(neighborIds).toEqual([ + "1,1", + "2,1", + "3,1", + "1,2", + "2,2", + "3,2", + "1,3", + "2,3", + "3,3", + ]); + }); + }); +}); diff --git a/lib/coordinates.ts b/lib/coordinates.ts index 2a3b59d..0a95970 100644 --- a/lib/coordinates.ts +++ b/lib/coordinates.ts @@ -62,3 +62,21 @@ export const each = (selection: SelectedCoords, func: Mapper) => { } } }; + +export const neighborIds = ( + coords: Coords, + distance = 1, + originOffset: Partial = {}, +) => { + return range( + -distance + (originOffset.y ?? 0), + distance + (originOffset.y ?? 0) + 1, + ).flatMap(y => { + return range( + -distance + (originOffset.x ?? 0), + distance + (originOffset.x ?? 0) + 1, + ).flatMap(x => { + return toId(coords.x + x, coords.y + y); + }); + }); +}; diff --git a/store/actions/tilesActions.ts b/store/actions/tilesActions.ts index a1dea77..04dd664 100644 --- a/store/actions/tilesActions.ts +++ b/store/actions/tilesActions.ts @@ -103,6 +103,7 @@ export const flipSelection = (direction: "horizontal" | "vertical") => { }; export const zLevelUp = createAction("app/tool/Z_LEVEL_UP")(); export const zLevelDown = createAction("app/tool/Z_LEVEL_DOWN")(); + export const clickTile = (coords: Coords) => { return (dispatch: Dispatch, getState: () => State) => { if (coords.x < 1 || coords.y < 1) { @@ -224,7 +225,7 @@ export const importAll = createAction( let newTile; if (phase === "query") { if (!tileMap[id]) { - // tslint:disable-next-line no-console + // eslint-disable-next-line no-console console.warn( `cannot add an adjustment to a tile with no item: ${shortcut}, phase: ${phase} at ${id}`, ); @@ -232,7 +233,7 @@ export const importAll = createAction( } const commandSlug = tileMap[id].item; if (!commandSlug) { - // tslint:disable-next-line no-console + // eslint-disable-next-line no-console console.warn( `received an adjustment with no matching command: ${shortcut}, phase: ${phase} at ${id}`, ); @@ -242,7 +243,7 @@ export const importAll = createAction( adj => adj.shortcut === shortcut[0] && adj.phase === phase, ); if (!adjustment || adjustment.requires !== commandSlug) { - // tslint:disable-next-line no-console + // eslint-disable-next-line no-console console.warn( `unknown adjustment for shortcut: ${shortcut} for command: ${commandSlug}, phase: ${phase} at ${id}`, ); @@ -259,7 +260,7 @@ export const importAll = createAction( comm => comm.shortcut === shortcut && comm.phase === phase, ); if (!command) { - // tslint:disable-next-line no-console + // eslint-disable-next-line no-console console.warn( `unknown command for shortcut: ${shortcut}, phase: ${phase} at ${id}`, ); @@ -269,7 +270,7 @@ export const importAll = createAction( command.type === "item" && (!tileMap[id] || tileMap[id].designation !== "mine") ) { - // tslint:disable-next-line no-console + // eslint-disable-next-line no-console console.warn( `cannot add an item to a space which is not mined: ${shortcut}, phase: ${phase} at ${id}`, ); @@ -281,11 +282,12 @@ export const importAll = createAction( } if (!tileMap[id]) { tileMap[id] = { - id, + adjustments: {}, coordinates: coordinates.fromId(id), designation: undefined, + id, item: undefined, - adjustments: {}, + multitileOrigin: undefined, }; } tileMap[id] = { @@ -321,7 +323,6 @@ const shouldUpdate = (tile: Tile | undefined, command: Command) => { return false; } // need to dig before placing - // TODO only "dig" is likely valid here, but check if (command.type === "item" && tile.designation !== "mine") { return false; } diff --git a/store/reducers/tilesReducer.ts b/store/reducers/tilesReducer.ts index 3227d43..495a8f8 100644 --- a/store/reducers/tilesReducer.ts +++ b/store/reducers/tilesReducer.ts @@ -94,9 +94,13 @@ export const tilesReducer = ( case getType(tilesActions.updateTile): { const { x, y, command } = action.payload; const id = coordinates.toId(x, y); - const newTile = addCommand(command, draft[id], id); - if (newTile) { - draft[id] = newTile; + if ("width" in command && "height" in command) { + addMultiCommand(command, id, draft); + } else { + const newTile = addCommand(command, draft[id], id); + if (newTile) { + draft[id] = newTile; + } } break; } @@ -277,7 +281,7 @@ const addCommand = ( current: Draft | undefined, id: string, ): Tile | undefined => { - if (command.type !== "designation" && (!current || !current.designation)) { + if (!canPlace(command, current)) { return undefined; } if (!current) { @@ -286,6 +290,7 @@ const addCommand = ( coordinates: coordinates.fromId(id), designation: undefined, item: undefined, + multitileOrigin: undefined, adjustments: {}, [command.type]: command.slug, }; @@ -294,6 +299,48 @@ const addCommand = ( return current; }; +// Add +const addMultiCommand = ( + command: Command, + id: string, + draft: Draft, +): void => { + const neighborIds = coordinates.neighborIds(coordinates.fromId(id), 1, { + x: 1, + y: 1, + }); + const available = neighborIds.every(neighborId => { + return canPlace(command, draft[neighborId]); + }); + if (!available) { + return; + } + + neighborIds.forEach(neighborId => { + const current = draft[neighborId]; + if (!current) { + draft[neighborId] = { + id, + coordinates: coordinates.fromId(id), + designation: undefined, + item: undefined, + multitileOrigin: undefined, + adjustments: {}, + [command.type]: command.slug, + }; + } else { + current[command.type] = command.slug; + } + }); +}; + +const canPlace = (command: Command, current: Draft | undefined) => { + return !( + command.type !== "designation" && + (!current || !current.designation) + ); +}; + export const selectTile = ( state: State, { x, y }: { x: number; y: number }, diff --git a/store/selectors/tilesSelectors.ts b/store/selectors/tilesSelectors.ts index 7f8fc8a..705d468 100644 --- a/store/selectors/tilesSelectors.ts +++ b/store/selectors/tilesSelectors.ts @@ -173,7 +173,8 @@ const createWalls = ( // TODO performance issues with within .filter(wallId => within(coordinates.fromId(wallId), selection)) .map(wallId => { - const wallNumber = neighborIds(coordinates.fromId(wallId)) + const wallNumber = coordinates + .neighborIds(coordinates.fromId(wallId)) .filter(id => id !== wallId) .reduce((bits, id, index) => { if (exposed(tiles[id])) { @@ -190,20 +191,6 @@ const createWalls = ( ); }; -const neighborIds = ({ x, y }: Coords) => { - return [ - coordinates.toId(x - 1, y - 1), - coordinates.toId(x, y - 1), - coordinates.toId(x + 1, y - 1), - coordinates.toId(x - 1, y), - coordinates.toId(x, y), - coordinates.toId(x + 1, y), - coordinates.toId(x - 1, y + 1), - coordinates.toId(x, y + 1), - coordinates.toId(x + 1, y + 1), - ]; -}; - const exposed = (tile: Tile | undefined) => { if (!tile) { return false; diff --git a/store/types.ts b/store/types.ts index 6b75fc5..96e6e4a 100644 --- a/store/types.ts +++ b/store/types.ts @@ -71,6 +71,7 @@ export type ImportMap = { [Key in PhaseSlug]?: string }; export interface Tile { readonly designation: CommandSlug | undefined; readonly item: CommandSlug | undefined; + readonly multitileOrigin: string | undefined; readonly adjustments: AdjustmentData; readonly id: string; // performance only, avoid creating in selectors From ec4764f2259c3a2430f364124018ae17241fa125 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Wed, 1 Jan 2020 16:23:43 -0800 Subject: [PATCH 03/10] Fix lint on CI --- .eslintrc.js | 3 +-- package.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 8351f73..a40871d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { "@typescript-eslint/no-inferrable-types": "error", "@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": warnOnLocal, "@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/prefer-function-type": "error", "@typescript-eslint/restrict-plus-operands": "error", @@ -72,7 +72,6 @@ module.exports = { "no-sequences": "error", "no-shadow": "error", "no-unused-expressions": warnOnLocal, - "no-unused-vars": warnOnLocal, "padding-line-between-statements": [ warnOnLocal, { diff --git a/package.json b/package.json index 934cc08..0477b29 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "cy:dev": "start-server-and-test dev http://localhost:3000 cy:open", "cy:prod": "start-server-and-test build:start http://localhost:3000 cy:open", "export": "next export", - "lint": "eslint ./ --ext .ts,.tsx", - "lint:fix": "eslint ./ --fix --ext .ts,.tsx", + "lint": "cross-env LINT_ERRORS=true eslint ./ --ext .ts,.tsx", + "lint:fix": "cross-env LINT_ERRORS=true eslint ./ --fix --ext .ts,.tsx", "tsc": "tsc --project ./tsconfig.json && tsc --project ./cypress/tsconfig.json" }, "keywords": [], From 0833a1e1e5a3d6416c9c054b166d0c05a39aa518 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Thu, 2 Jan 2020 09:21:05 -0800 Subject: [PATCH 04/10] Set up erase for multitile builds --- cypress/integration/multitile.spec.ts | 48 ++++++++++++++++++++++ lib/coordinates.test.ts | 46 +++++---------------- lib/coordinates.ts | 13 ++---- store/reducers/tilesReducer.ts | 57 +++++++++++++++++++++----- store/selectors/tilesSelectors.test.ts | 5 +++ 5 files changed, 113 insertions(+), 56 deletions(-) diff --git a/cypress/integration/multitile.spec.ts b/cypress/integration/multitile.spec.ts index b09e847..d96bc34 100644 --- a/cypress/integration/multitile.spec.ts +++ b/cypress/integration/multitile.spec.ts @@ -25,4 +25,52 @@ ww,ww,ww ww,ww,ww`, ); }); + + it("erases multitile buildings when clicking on the origin", () => { + cy.visit("/"); + cy.getId({ name: "tool", item: "paint-rectangle" }).click(); + cy.getId("stage").then( + dragTiles({ startX: 1, startY: 1, endX: 3, endY: 3 }), + ); + cy.getId({ name: "tool", item: "paint" }).click(); + cy.getId({ name: "phase", item: "build" }).click(); + cy.getId({ name: "command", item: "farmersWorkshop" }).click(); + + cy.getId("stage").then(clickTile({ x: 1, y: 1 })); + cy.getId("export").click(); + cy.getId({ name: "export-text", item: "build" }).should( + "have.value", + `#build +ww,ww,ww +ww,ww,ww +ww,ww,ww`, + ); + cy.getId({ name: "tool", item: "erase" }).click(); + cy.getId("stage").then(clickTile({ x: 1, y: 1 })); + cy.getId({ name: "export-text", item: "build" }).should("not.exist"); + }); + + it("erases multitile buildings when clicking other tiles", () => { + cy.visit("/"); + cy.getId({ name: "tool", item: "paint-rectangle" }).click(); + cy.getId("stage").then( + dragTiles({ startX: 1, startY: 1, endX: 3, endY: 3 }), + ); + cy.getId({ name: "tool", item: "paint" }).click(); + cy.getId({ name: "phase", item: "build" }).click(); + cy.getId({ name: "command", item: "farmersWorkshop" }).click(); + + cy.getId("stage").then(clickTile({ x: 1, y: 1 })); + cy.getId("export").click(); + cy.getId({ name: "export-text", item: "build" }).should( + "have.value", + `#build +ww,ww,ww +ww,ww,ww +ww,ww,ww`, + ); + cy.getId({ name: "tool", item: "erase" }).click(); + cy.getId("stage").then(clickTile({ x: 3, y: 3 })); + cy.getId({ name: "export-text", item: "build" }).should("not.exist"); + }); }); diff --git a/lib/coordinates.test.ts b/lib/coordinates.test.ts index a216881..755d0eb 100644 --- a/lib/coordinates.test.ts +++ b/lib/coordinates.test.ts @@ -17,52 +17,26 @@ describe("coordinates", () => { ]); }); - it("returns an array of neighboring ids for the given id with custom distance", () => { - const neighborIds = coordinates.neighborIds({ x: 3, y: 3 }, 2); + it("returns an array of neighboring ids for the given id with custom offset", () => { + const neighborIds = coordinates.neighborIds( + { x: 2, y: 2 }, + { + startX: 0, + startY: -1, + endX: 2, + endY: 1, + }, + ); expect(neighborIds).toEqual([ - "1,1", "2,1", "3,1", "4,1", - "5,1", - "1,2", "2,2", "3,2", "4,2", - "5,2", - "1,3", "2,3", "3,3", "4,3", - "5,3", - "1,4", - "2,4", - "3,4", - "4,4", - "5,4", - "1,5", - "2,5", - "3,5", - "4,5", - "5,5", - ]); - }); - - it("returns an array of neighboring ids for the given id with custom offset", () => { - const neighborIds = coordinates.neighborIds({ x: 1, y: 1 }, 1, { - x: 1, - y: 1, - }); - expect(neighborIds).toEqual([ - "1,1", - "2,1", - "3,1", - "1,2", - "2,2", - "3,2", - "1,3", - "2,3", - "3,3", ]); }); }); diff --git a/lib/coordinates.ts b/lib/coordinates.ts index 0a95970..6483503 100644 --- a/lib/coordinates.ts +++ b/lib/coordinates.ts @@ -65,17 +65,10 @@ export const each = (selection: SelectedCoords, func: Mapper) => { export const neighborIds = ( coords: Coords, - distance = 1, - originOffset: Partial = {}, + offsets: Partial = {}, ) => { - return range( - -distance + (originOffset.y ?? 0), - distance + (originOffset.y ?? 0) + 1, - ).flatMap(y => { - return range( - -distance + (originOffset.x ?? 0), - distance + (originOffset.x ?? 0) + 1, - ).flatMap(x => { + return range(offsets.startY ?? -1, (offsets.endY ?? 1) + 1).flatMap(y => { + return range(offsets.startX ?? -1, (offsets.endX ?? 1) + 1).flatMap(x => { return toId(coords.x + x, coords.y + y); }); }); diff --git a/store/reducers/tilesReducer.ts b/store/reducers/tilesReducer.ts index 495a8f8..fbbcdad 100644 --- a/store/reducers/tilesReducer.ts +++ b/store/reducers/tilesReducer.ts @@ -89,6 +89,8 @@ export const tilesReducer = ( outerDraft.data[state.zLevel] = produce( state.data[state.zLevel], draft => { + // Save a reference to the current state of things as we + // mutate the draft const currentTiles = state.data[state.zLevel]; switch (action.type) { case getType(tilesActions.updateTile): { @@ -199,12 +201,7 @@ export const tilesReducer = ( case getType(tilesActions.removeTile): { const { x, y, command } = action.payload; const id = coordinates.toId(x, y); - const newTile = removeCommand(command, draft[id]); - if (newTile) { - draft[id] = newTile; - } else { - delete draft[id]; - } + removeTile(id, draft, currentTiles, command); break; } case getType(tilesActions.setAdjustment): { @@ -258,6 +255,42 @@ export const tilesReducer = ( }); }; +const removeTile = ( + id: string, + draft: Draft, + currentTiles: TilesMap, + command: Command, +): void => { + if (currentTiles[id]) { + if (currentTiles[id]?.multitileOrigin) { + removeTile( + currentTiles[id]?.multitileOrigin, + draft, + currentTiles, + command, + ); + } else if ("width" in command && "height" in command) { + coordinates + .neighborIds(coordinates.fromId(id), { + startX: 0, + startY: 0, + endX: command.width - 1, + endY: command.height - 1, + }) + .forEach(neighborId => { + draft[neighborId][command.type] = undefined; + }); + } + } + + const newTile = removeCommand(command, draft[id]); + if (newTile) { + draft[id] = newTile; + } else { + delete draft[id]; + } +}; + const deleteAll = (draft: Draft) => { for (const id of Object.keys(draft)) { delete draft[id]; @@ -305,9 +338,11 @@ const addMultiCommand = ( id: string, draft: Draft, ): void => { - const neighborIds = coordinates.neighborIds(coordinates.fromId(id), 1, { - x: 1, - y: 1, + const neighborIds = coordinates.neighborIds(coordinates.fromId(id), { + startX: 0, + startY: 0, + endX: command.width - 1, + endY: command.height - 1, }); const available = neighborIds.every(neighborId => { return canPlace(command, draft[neighborId]); @@ -318,18 +353,20 @@ const addMultiCommand = ( neighborIds.forEach(neighborId => { const current = draft[neighborId]; + const multitileOrigin = neighborId !== id ? id : undefined; if (!current) { draft[neighborId] = { id, coordinates: coordinates.fromId(id), designation: undefined, item: undefined, - multitileOrigin: undefined, + multitileOrigin, adjustments: {}, [command.type]: command.slug, }; } else { current[command.type] = command.slug; + current.multitileOrigin = multitileOrigin; } }); }; diff --git a/store/selectors/tilesSelectors.test.ts b/store/selectors/tilesSelectors.test.ts index 12346cf..4c3fdcc 100644 --- a/store/selectors/tilesSelectors.test.ts +++ b/store/selectors/tilesSelectors.test.ts @@ -26,6 +26,7 @@ describe("selectChunks", () => { "1,1": { designation: "mine", item: undefined, + multitileOrigin: undefined, adjustments: {}, id: "1,1", coordinates: { @@ -49,6 +50,7 @@ describe("selectChunks", () => { designation: "mine", item: undefined, adjustments: {}, + multitileOrigin: undefined, id: "1,1", coordinates: { x: 1, @@ -58,6 +60,7 @@ describe("selectChunks", () => { "1,2": { designation: "mine", item: undefined, + multitileOrigin: undefined, adjustments: {}, id: "1,2", coordinates: { @@ -88,6 +91,7 @@ describe("selectChunks", () => { "0,0": { designation: "mine", item: undefined, + multitileOrigin: undefined, adjustments: {}, id: "0,0", coordinates: { @@ -98,6 +102,7 @@ describe("selectChunks", () => { "9,9": { designation: "mine", item: undefined, + multitileOrigin: undefined, adjustments: {}, id: "9,9", coordinates: { From 7733640f059b9aa28ea10f534f028994a9bf7dd0 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Thu, 2 Jan 2020 10:29:46 -0800 Subject: [PATCH 05/10] Fix type errors --- store/reducers/tilesReducer.ts | 13 ++++++++++--- store/types.ts | 9 +++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/store/reducers/tilesReducer.ts b/store/reducers/tilesReducer.ts index fbbcdad..44f3488 100644 --- a/store/reducers/tilesReducer.ts +++ b/store/reducers/tilesReducer.ts @@ -3,7 +3,15 @@ import { range } from "lodash"; import { ActionType, getType } from "typesafe-actions"; import * as coordinates from "../../lib/coordinates"; import { tilesActions } from "../actions"; -import { Command, State, Tile, TilesMap, TilesState, ZPatch } from "../types"; +import { + Command, + State, + Tile, + TilesMap, + TilesState, + ZPatch, + MultitileCommand, +} from "../types"; export const INITIAL_STATE: TilesState = { data: range(0, 128).reduce((result, zIndex) => { @@ -334,7 +342,7 @@ const addCommand = ( // Add const addMultiCommand = ( - command: Command, + command: MultitileCommand, id: string, draft: Draft, ): void => { @@ -359,7 +367,6 @@ const addMultiCommand = ( id, coordinates: coordinates.fromId(id), designation: undefined, - item: undefined, multitileOrigin, adjustments: {}, [command.type]: command.slug, diff --git a/store/types.ts b/store/types.ts index 96e6e4a..f212f26 100644 --- a/store/types.ts +++ b/store/types.ts @@ -61,6 +61,15 @@ export interface SelectAdjustment { readonly selectName: string; } +type MultitileCommandMap = { + [Key in keyof CommandMap]: CommandMap[Key] extends { + width: number; + height: number; + } + ? CommandMap[Key] + : never; +}; +export type MultitileCommand = ValueOf; export type Adjustment = SelectAdjustment | ResizeAdjustment; export type Phase = typeof phases; From 936a4334ebc66407bb01932fd0174447ca59eb79 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Thu, 2 Jan 2020 19:13:10 -0800 Subject: [PATCH 06/10] Test readability --- cypress/integration/multitile.spec.ts | 28 +++--- cypress/integration/persistence.spec.ts | 3 +- cypress/integration/tools.spec.ts | 120 +++++++++++++----------- cypress/integration/zLevel.spec.ts | 10 +- cypress/lib/template.ts | 7 ++ 5 files changed, 93 insertions(+), 75 deletions(-) create mode 100644 cypress/lib/template.ts diff --git a/cypress/integration/multitile.spec.ts b/cypress/integration/multitile.spec.ts index d96bc34..d271f06 100644 --- a/cypress/integration/multitile.spec.ts +++ b/cypress/integration/multitile.spec.ts @@ -1,4 +1,5 @@ import { dragTiles, clickTile } from "../lib/tiles"; +import { template } from "../lib/template"; describe("z-levels", () => { beforeEach(() => { @@ -19,10 +20,11 @@ describe("z-levels", () => { cy.getId("export").click(); cy.getId({ name: "export-text", item: "build" }).should( "have.value", - `#build -ww,ww,ww -ww,ww,ww -ww,ww,ww`, + template(` + #build + ww,ww,ww + ww,ww,ww + ww,ww,ww`), ); }); @@ -40,10 +42,11 @@ ww,ww,ww`, cy.getId("export").click(); cy.getId({ name: "export-text", item: "build" }).should( "have.value", - `#build -ww,ww,ww -ww,ww,ww -ww,ww,ww`, + template(` + #build + ww,ww,ww + ww,ww,ww + ww,ww,ww`), ); cy.getId({ name: "tool", item: "erase" }).click(); cy.getId("stage").then(clickTile({ x: 1, y: 1 })); @@ -64,10 +67,11 @@ ww,ww,ww`, cy.getId("export").click(); cy.getId({ name: "export-text", item: "build" }).should( "have.value", - `#build -ww,ww,ww -ww,ww,ww -ww,ww,ww`, + template(` + #build + ww,ww,ww + ww,ww,ww + ww,ww,ww`), ); cy.getId({ name: "tool", item: "erase" }).click(); cy.getId("stage").then(clickTile({ x: 3, y: 3 })); diff --git a/cypress/integration/persistence.spec.ts b/cypress/integration/persistence.spec.ts index a8f4ec1..f7915df 100644 --- a/cypress/integration/persistence.spec.ts +++ b/cypress/integration/persistence.spec.ts @@ -13,8 +13,7 @@ describe("z-levels", () => { cy.getId("export").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d`, + "#dig\nd,d", ); }); }); diff --git a/cypress/integration/tools.spec.ts b/cypress/integration/tools.spec.ts index 0a7abd6..4966f9b 100644 --- a/cypress/integration/tools.spec.ts +++ b/cypress/integration/tools.spec.ts @@ -1,13 +1,6 @@ import { triggerHotkeys } from "../lib/hotkeys"; import { clickTile, dragTiles, setTiles } from "../lib/tiles"; - -const template = (string: string) => { - return string - .trim() - .split("\n") - .map(part => part.trim()) - .join("\n"); -}; +import { template } from "../lib/template"; describe("tools", () => { describe("paint tools", () => { @@ -142,10 +135,11 @@ describe("tools", () => { ); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - template(`#dig - d,d,d - d,d,d - d,d,d`), + template(` + #dig + d,d,d + d,d,d + d,d,d`), ); cy.getId("stage").then( dragTiles({ @@ -167,19 +161,21 @@ describe("tools", () => { cy.getId("undo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -d,d,d -d,d,d`, + template(` + #dig + d,d,d + d,d,d + d,d,d`), ); cy.getId("redo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -d,d,d -d,d,d -~,d,d`, + template(` + #dig + d,d,d + d,d,d + d,d,d + ~,d,d`), ); }); @@ -265,9 +261,10 @@ d,d,d ); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,~,~ -~,d,d`, + template(` + #dig + d,~,~ + ~,d,d`), ); cy.getId("undo").click(); cy.getId({ name: "export-text", item: "dig" }).should( @@ -277,9 +274,10 @@ d,~,~ cy.getId("redo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,~,~ -~,d,d`, + template(` + #dig + d,~,~ + ~,d,d`), ); }); @@ -306,9 +304,10 @@ d,~,~ cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -~,d,d`, + template(` + #dig + d,d,d + ~,d,d`), ); cy.getId("undo").click(); cy.getId({ name: "export-text", item: "dig" }).should( @@ -318,9 +317,10 @@ d,d,d cy.getId("redo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -~,d,d`, + template(` + #dig + d,d,d + ~,d,d`), ); }); @@ -374,50 +374,56 @@ d,d,d cy.getId("selection-flip-horizontal").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -d,~,~ -d,~,~`, + template(` + #dig + d,d,d + d,~,~ + d,~,~`), ); cy.getId("undo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -~,~,d -~,~,d`, + template(` + #dig + d,d,d + ~,~,d + ~,~,d`), ); cy.getId("redo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -d,~,~ -d,~,~`, + template(` + #dig + d,d,d + d,~,~ + d,~,~`), ); cy.getId("selection-flip-vertical").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,~,~ -d,~,~ -d,d,d`, + template(` + #dig + d,~,~ + d,~,~ + d,d,d`), ); cy.getId("undo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,d,d -d,~,~ -d,~,~`, + template(` + #dig + d,d,d + d,~,~ + d,~,~`), ); cy.getId("redo").click(); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -d,~,~ -d,~,~ -d,d,d`, + template(` + #dig + d,~,~ + d,~,~ + d,d,d`), ); }); }); diff --git a/cypress/integration/zLevel.spec.ts b/cypress/integration/zLevel.spec.ts index 9299187..a1a0746 100644 --- a/cypress/integration/zLevel.spec.ts +++ b/cypress/integration/zLevel.spec.ts @@ -1,5 +1,6 @@ import { triggerHotkeys } from "../lib/hotkeys"; import { clickTile } from "../lib/tiles"; +import { template } from "../lib/template"; describe("z-levels", () => { beforeEach(() => { @@ -58,10 +59,11 @@ describe("z-levels", () => { cy.getId("stage").then(clickTile({ x: 2, y: 1 })); cy.getId({ name: "export-text", item: "dig" }).should( "have.value", - `#dig -~,d -#> -d,~`, + template(` + #dig + ~,d + #> + d,~`), ); }); }); diff --git a/cypress/lib/template.ts b/cypress/lib/template.ts new file mode 100644 index 0000000..894c1c4 --- /dev/null +++ b/cypress/lib/template.ts @@ -0,0 +1,7 @@ +export const template = (string: string) => { + return string + .trim() + .split("\n") + .map(part => part.trim()) + .join("\n"); +}; From 442f00b2e3c3aacb8b8644e2b7ddbbc18f0197e5 Mon Sep 17 00:00:00 2001 From: David Edmondson Date: Sat, 4 Jan 2020 21:49:38 -0800 Subject: [PATCH 07/10] Switch to cypress-testing-library Remove React.FC --- components/AdjustmentBar.tsx | 26 +-- components/Artboard/Artboard.tsx | 2 +- components/Button.tsx | 3 - components/CommandBar.tsx | 2 +- components/ExportBar.tsx | 51 +++-- components/MultiSelectBar.tsx | 2 +- components/SelectBar.tsx | 2 +- components/Toolbar.tsx | 15 +- cypress/integration/adjustments.spec.ts | 49 ++--- cypress/integration/importExport.spec.ts | 78 +++---- cypress/integration/multitile.spec.ts | 60 ++--- cypress/integration/persistence.spec.ts | 13 +- cypress/integration/tools.spec.ts | 266 +++++++++-------------- cypress/integration/undoRedo.spec.ts | 35 +++ cypress/integration/undoRedo.ts | 35 --- cypress/integration/zLevel.spec.ts | 77 +++---- cypress/locators/index.ts | 27 --- cypress/support/commands.ts | 84 +------ cypress/tsconfig.json | 2 +- package.json | 2 + pages/index.tsx | 2 +- yarn.lock | 57 ++++- 22 files changed, 358 insertions(+), 532 deletions(-) create mode 100644 cypress/integration/undoRedo.spec.ts delete mode 100644 cypress/integration/undoRedo.ts delete mode 100644 cypress/locators/index.ts diff --git a/components/AdjustmentBar.tsx b/components/AdjustmentBar.tsx index 964d95f..e809136 100644 --- a/components/AdjustmentBar.tsx +++ b/components/AdjustmentBar.tsx @@ -20,7 +20,7 @@ const tileValue = (tile: Tile | undefined, adjustment: Adjustment) => { return tile.adjustments[adjustment.slug]; }; -export const AdjustmentBar: React.FunctionComponent = ({ tile }) => { +export const AdjustmentBar = ({ tile }: Props) => { const commandMap = selectCommandMap(); const adjustmentMap = selectAdjustmentMap(); // we implicitly know this is non-null based on it being rendered @@ -34,8 +34,8 @@ export const AdjustmentBar: React.FunctionComponent = ({ tile }) => { }); }, shallowEqual); return ( - - {name} + + {name} {adjustments && adjustments.map(adjustment => { const value = tileValue(tile, adjustment); @@ -69,11 +69,7 @@ interface ResizeInputProps { value: number | undefined; tile: Tile; } -const ResizeInput: React.FunctionComponent = ({ - adjustment, - value, - tile, -}) => { +const ResizeInput = ({ adjustment, value, tile }: ResizeInputProps) => { const dispatch = useDispatch(); return ( @@ -99,8 +95,7 @@ const ResizeInput: React.FunctionComponent = ({ {value && ( {" "} - {value}{" "} + {value}