diff --git a/public/assets/copy-cursor.png b/public/assets/copy-cursor.png new file mode 100644 index 0000000..12a5f49 Binary files /dev/null and b/public/assets/copy-cursor.png differ diff --git a/public/assets/toolbar.png b/public/assets/toolbar.png index 32c5fc8..58b6a84 100644 Binary files a/public/assets/toolbar.png and b/public/assets/toolbar.png differ diff --git a/src/app.tsx b/src/app.tsx index a65bc73..1155560 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -152,6 +152,9 @@ export function Game(props: GameProps): JSX.Element { if (tool == 'bomb') { return 'url(assets/bomb-cursor.png) 16 16, pointer'; } + if (tool == 'copy') { + return 'url(assets/copy-cursor.png) 16 16, pointer'; + } if (tool == 'hand') { return 'grab'; } diff --git a/src/core/bonus.ts b/src/core/bonus.ts index ed52a14..9241710 100644 --- a/src/core/bonus.ts +++ b/src/core/bonus.ts @@ -7,14 +7,19 @@ import { Layer, mkLayer } from './layer'; import { CoreState, Tile, TileEntity } from './state'; import { MoveTile } from './state-helpers'; -export type Bonus = +export type ScoringBonus = | { t: 'bonus' } | { t: 'bomb' } - | { t: 'empty' } - | { t: 'block' } | { t: 'required', letter: string } | { t: 'consonant' } | { t: 'vowel' } + | { t: 'copy' } + ; + +export type Bonus = + | ScoringBonus + | { t: 'empty' } + | { t: 'block' } ; export function bonusGenerator(p: Point, seed: number): Bonus { @@ -44,6 +49,9 @@ export function bonusGenerator(p: Point, seed: number): Bonus { if (ph < 0.5) { return { t: 'required', letter: deterministicLetterSample(ph * 1e9) }; } + else if (ph < 0.53) { + return { t: 'copy' }; + } else { return { t: 'block' }; } @@ -64,40 +72,40 @@ export function isBlocking(tile: MoveTile, bonus: Bonus): boolean { return true; } -type Scoring = - | { t: 'bonus', p: Point } - | { t: 'bomb', p: Point } - | { t: 'required', p: Point } - | { t: 'vowel', p: Point } - | { t: 'consonant', p: Point } - ; +type Scoring = { + bonus: ScoringBonus, + p: Point +}; export function adjacentScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { switch (bonus.t) { - case 'bonus': return [{ t: 'bonus', p }]; - case 'bomb': return [{ t: 'bomb', p }]; - case 'vowel': return [{ t: 'vowel', p }]; - case 'consonant': return [{ t: 'consonant', p }]; + case 'bonus': return [{ bonus, p }]; + case 'bomb': return [{ bonus, p }]; + case 'vowel': return [{ bonus, p }]; + case 'consonant': return [{ bonus, p }]; + case 'copy': return [{ bonus, p }]; default: return []; } } export function overlapScoringOfBonus(bonus: Bonus, p: Point): Scoring[] { switch (bonus.t) { - case 'required': return [{ t: 'required', p }]; + case 'required': return [{ bonus, p }]; default: return []; } } export function resolveScoring(state: Draft, scoring: Scoring): void { - switch (scoring.t) { + const bonus = scoring.bonus; + switch (bonus.t) { case 'bonus': state.score++; return; case 'bomb': state.inventory.bombs++; return; case 'required': state.score += 10; return; case 'vowel': state.inventory.vowels += 5; return case 'consonant': state.inventory.consonants += 5; return + case 'copy': state.inventory.copies += 3; return } - unreachable(scoring); + unreachable(bonus); } // Bonus Layer Generation diff --git a/src/core/intent.ts b/src/core/intent.ts index e802ca6..1f3c3b3 100644 --- a/src/core/intent.ts +++ b/src/core/intent.ts @@ -3,10 +3,11 @@ import { produce } from '../util/produce'; import { vm } from '../util/vutil'; import { GameState, TileEntity } from './state'; import { tryKillTileOfState } from './kill-helpers'; -import { Tool, bombIntent, dynamiteIntent } from './tools'; +import { Tool, bombIntent, copyIntent, dynamiteIntent } from './tools'; import { SelectionOperation, selectionOperationOfMods } from './selection'; import { vacuous_down, deselect } from './reduce'; -import { withCoreState } from './state-helpers'; +import { drawSpecificOfState, withCoreState } from './state-helpers'; +import { tileAtPoint } from './tile-helpers'; export type KillIntent = | { t: 'kill', radius: number, cost: number } @@ -18,6 +19,7 @@ export type Intent = | { t: 'panWorld' } | { t: 'exchangeTiles', id: string } | { t: 'startSelection', opn: SelectionOperation } + | { t: 'copy' } | KillIntent ; @@ -50,6 +52,7 @@ export function getIntentOfMouseDown(tool: Tool, wp: WidgetPoint, button: number return bombIntent; case 'vowel': throw new Error(`shoudn't be able have vowel tool active`); case 'consonant': throw new Error(`shoudn't be able have consonant tool active`); + case 'copy': return copyIntent; } } @@ -110,5 +113,21 @@ export function reduceIntent(state: GameState, intent: Intent, wp: WidgetPoint): opn: intent.opn, }; }); + case 'copy': { + if (wp.t == 'world') { + const hoverTile = tileAtPoint(state.coreState, wp.p_in_local); + if (hoverTile == undefined) + return state; + const newCs = drawSpecificOfState(state.coreState, hoverTile.letter); + if (newCs == state.coreState) return state; + return withCoreState(state, cs => produce(newCs, s => { + s.inventory.copies--; + })); + } + else { + return state; + } + + } } } diff --git a/src/core/reduce.ts b/src/core/reduce.ts index 68399ee..48b4a2f 100644 --- a/src/core/reduce.ts +++ b/src/core/reduce.ts @@ -275,6 +275,7 @@ function reduceGameAction(state: GameState, action: GameAction): effectful.Resul s.inventory.bombs = 15; s.inventory.vowels = 15; s.inventory.consonants = 15; + s.inventory.copies = 15; })))); } return gs(state); diff --git a/src/core/shortcuts.ts b/src/core/shortcuts.ts index 55b255c..5eb4d6b 100644 --- a/src/core/shortcuts.ts +++ b/src/core/shortcuts.ts @@ -34,5 +34,10 @@ export function tryReduceShortcut(state: GameState, code: string): GameState | u return withCoreState(state, cs => reduceToolSelect(cs, 'consonant')); } } + if (code == 'x') { + if (state.coreState.inventory.copies >= 1) { + return withCoreState(state, cs => reduceToolSelect(cs, 'copy')); + } + } return undefined; } diff --git a/src/core/state-helpers.ts b/src/core/state-helpers.ts index 9e9d5c6..eb2a424 100644 --- a/src/core/state-helpers.ts +++ b/src/core/state-helpers.ts @@ -65,6 +65,15 @@ export function drawOfState(state: CoreState, drawForce?: DrawForce): CoreState })); } +export function drawSpecificOfState(state: CoreState, letter: string): CoreState { + const handLength = get_hand_tiles(state).length; + if (handLength >= HAND_TILE_LIMIT) + return state; + return checkValid(produce(state, s => { + addHandTile(s, ensureTileId({ letter, p_in_world_int: { x: 0, y: handLength } })); + })); +} + const directions: Point[] = [[1, 0], [-1, 0], [0, 1], [0, -1]].map(([x, y]) => ({ x, y })); export function resolveValid(state: CoreState): CoreState { diff --git a/src/core/state.ts b/src/core/state.ts index df42d5b..0dc3f1d 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -94,6 +94,7 @@ export type CoreState = { bombs: number, vowels: number, consonants: number, + copies: number, } bonusLayerName: string, }; @@ -139,6 +140,7 @@ export function mkGameState(seed?: number): GameState { bombs: 0, vowels: 0, consonants: 0, + copies: 0, }, bonusLayerName: 'game', }, diff --git a/src/core/tools.ts b/src/core/tools.ts index 2274560..78abf89 100644 --- a/src/core/tools.ts +++ b/src/core/tools.ts @@ -13,6 +13,7 @@ const tools = [ 'bomb', 'vowel', 'consonant', + 'copy', ] as const; export type Tool = (typeof tools)[number]; @@ -35,6 +36,7 @@ export function getCurrentTool(state: CoreState): Tool { export const dynamiteIntent: Intent & { t: 'kill' } = { t: 'kill', radius: 0, cost: 1 }; export const BOMB_RADIUS = 2; export const bombIntent: Intent & { t: 'bomb' } = { t: 'bomb' }; +export const copyIntent: Intent & { t: 'copy' } = { t: 'copy' }; export function getCurrentTools(state: CoreState): Tool[] { if (state.lost) { @@ -53,6 +55,9 @@ export function getCurrentTools(state: CoreState): Tool[] { if (state.inventory.consonants > 0) { tools.push('consonant'); } + if (state.inventory.copies > 0) { + tools.push('copy'); + } return tools; } @@ -66,14 +71,14 @@ export function reduceToolSelect(state: CoreState, tool: Tool): CoreState { switch (tool) { case 'consonant': { const newState = drawOfState(state, 'consonant'); - if (newState == state) return newState; + if (newState == state) return state; return produce(newState, s => { s.inventory.consonants--; }); } case 'vowel': { const newState = drawOfState(state, 'vowel'); - if (newState == state) return newState; + if (newState == state) return state; return produce(newState, s => { s.inventory.vowels--; }); diff --git a/src/ui/drawBonus.ts b/src/ui/drawBonus.ts index 0e970ff..a405f3a 100644 --- a/src/ui/drawBonus.ts +++ b/src/ui/drawBonus.ts @@ -58,7 +58,10 @@ export function drawBonus(d: CanvasRenderingContext2D, bonus: Bonus, pan_canvas_ drawImage(d, toolbarImg, rectOfTool('vowel'), rect_in_canvas); return; } - + case 'copy': { + drawImage(d, toolbarImg, rectOfTool('copy'), rect_in_canvas); + return; + } } unreachable(bonus); } diff --git a/src/ui/instructions.tsx b/src/ui/instructions.tsx index 1564e3d..4b51444 100644 --- a/src/ui/instructions.tsx +++ b/src/ui/instructions.tsx @@ -139,6 +139,7 @@ function exampleState(): GameState { bombs: 3, vowels: 0, consonants: 0, + copies: 0, }, }, mouseState: { diff --git a/src/ui/render.ts b/src/ui/render.ts index a01980c..3fa6c1b 100644 --- a/src/ui/render.ts +++ b/src/ui/render.ts @@ -83,6 +83,9 @@ function drawToolbar(d: CanvasRenderingContext2D, state: CoreState): void { else if (tool == 'consonant') { drawToolbarCount(d, rect_in_canvas, state.inventory.consonants); } + else if (tool == 'copy') { + drawToolbarCount(d, rect_in_canvas, state.inventory.copies); + } // indicate current tool if (tool == currentTool) { @@ -193,13 +196,15 @@ export function rawPaint(ci: CanvasInfo, state: GameState) { } function drawPauseButton() { - d.textAlign = 'center'; - d.textBaseline = 'middle'; - if (!cs.lost) { - fillText(d, '⏸', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); - } - else { - fillText(d, '⟳', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + if (cs.panic) { + d.textAlign = 'center'; + d.textBaseline = 'middle'; + if (!cs.lost) { + fillText(d, '⏸', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + } + else { + fillText(d, '⟳', midpointOfRect(pause_button_bds_in_canvas), 'black', '48px sans-serif'); + } } }