diff --git a/routes/+page.svelte b/routes/+page.svelte index d9dc78f..65b2c76 100644 --- a/routes/+page.svelte +++ b/routes/+page.svelte @@ -15,4 +15,6 @@

Stage 3-1 Stage 3-2 + Stage 3-3 + Stage 3-4

diff --git a/src/ability.ts b/src/ability.ts index 4d2cace..18e6b11 100644 --- a/src/ability.ts +++ b/src/ability.ts @@ -5,17 +5,7 @@ import { createSnapshot } from "./history.ts"; import * as History from "./history.ts"; import type { AbilityInit, Context, Coords, MovableObject } from "./public-types.ts"; -export function init(cx: Context, options?: AbilityInit) { - cx.state.update((prev) => ({ - ...prev, - usage: options?.enabled ?? { - copy: Number.POSITIVE_INFINITY, - paste: Number.POSITIVE_INFINITY, - cut: Number.POSITIVE_INFINITY, - }, - inventoryIsInfinite: options?.inventoryIsInfinite ?? false, - })); - +export function init(cx: Context) { console.log("ability init"); document.addEventListener("copy", (e) => { const { onGround } = cx.dynamic.player; @@ -160,12 +150,13 @@ export function placeMovableObject(cx: Context, x: number, y: number, object: Mo console.error("[placeMovableObject] cannot place object"); return; } + const newObjectId = Math.random().toString(); for (const rel of object.relativePositions) { const positionX = x + rel.x; const positionY = y + rel.y; grid.setBlock(cx, positionX, positionY, { block: object.block, - objectId: object.objectId, + objectId: newObjectId, switchId: undefined, }); } diff --git a/src/constants.ts b/src/constants.ts index b919bad..9bf8d03 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,8 +21,10 @@ export enum Block { fallable = "fallable", switch = "switch", switchBase = "switch-base", - switchingBlockOFF = "switching-block-off", - switchingBlockON = "switching-block-on", + switchingBlockOFF = "switching-block-off", // 初期状態で出現している + switchingBlockON = "switching-block-on", // スイッチを押すと隠れる + inverseSwitchingBlockOFF = "inverse-switching-block-off", // 初期状態で隠れている + inverseSwitchingBlockON = "inverse-switching-block-on", // スイッチを押すと出現する switchPressed = "switch-pressed", spike = "spike", goal = "goal", @@ -46,6 +48,7 @@ export const BlockDefinitionMap = new Map([ ["s", Block.switch], ["S", Block.switchBase], ["w", Block.switchingBlockOFF], + ["W", Block.inverseSwitchingBlockOFF], ["^", Block.spike], ["g", Block.goal], ]); diff --git a/src/grid.ts b/src/grid.ts index 60fcf2c..72ea568 100644 --- a/src/grid.ts +++ b/src/grid.ts @@ -40,7 +40,13 @@ export type GridCell = } | { // switches / triggerable blocks - block: Block.switch | Block.switchingBlockOFF | Block.switchingBlockON | Block.switchPressed; + block: + | Block.switch + | Block.switchingBlockOFF + | Block.switchingBlockON + | Block.inverseSwitchingBlockOFF + | Block.inverseSwitchingBlockON + | Block.switchPressed; switchId?: string; // optional でいいの? objectId?: unknown; }; @@ -49,6 +55,7 @@ export class Grid { __vsom: VirtualSOM; marginY: number; // windowの上端とy=0上端の距離(px) oobSprites: Sprite[]; // グリッド定義の外を埋めるやつ + oobFallableSprites: { sprite: Sprite | null; vy: number }[]; // グリッド定義外に落ちたfallable constructor( cx: { _stage_container: Container; @@ -61,6 +68,7 @@ export class Grid { ) { const stage = cx._stage_container; this.oobSprites = []; + this.oobFallableSprites = []; this.marginY = (height - cellSize * stageDefinition.stage.length) / 2; const vsprites: VirtualSOM = []; @@ -104,6 +112,7 @@ export class Grid { }); break; } + case Block.inverseSwitchingBlockOFF: case Block.switchingBlockOFF: { const switchId = ( get(cx.state).cells[y][x] as { @@ -126,6 +135,7 @@ export class Grid { }); break; } + case Block.inverseSwitchingBlockON: case Block.switchingBlockON: case Block.switchPressed: throw new Error(`[Grid.constructor]: block is not supported: ${dblock}`); @@ -226,6 +236,15 @@ export class Grid { } } this.initOOBSprites(cx, cellSize); + this.clearFallableSprites(cx); + } + // 画面外のfallableはステージ内のfallableのコピーでありhistoryの対象ではないので、 + // undoなどでステージ全体に変更を加えるときに使う + clearFallableSprites(cx: { _stage_container: Container }) { + for (const sprite of this.oobFallableSprites) { + if (sprite.sprite) cx._stage_container.removeChild(sprite.sprite); + } + this.oobFallableSprites = []; } getBlock(cx: Context, x: number, y: number): Block | null { return get(cx.state).cells[y]?.[x]?.block ?? null; @@ -307,7 +326,7 @@ export class Grid { vprev.block = Block.switchPressed; vprev.dy = 0; vprev.vy = 0; - } else if (vprev?.block === Block.switchPressed) { + } else if (vprev?.block === Block.switchPressed && cNewCell.block === Block.switch) { // switchがプレイヤーに押されているのが戻るとき assert( cprev.block === Block.switchPressed || cprev.block === Block.switch, @@ -328,7 +347,7 @@ export class Grid { vprev.vy = 0; } // switch上にオブジェクトを置くとき - else if (vprev?.block === Block.switch) { + else if (vprev?.block === Block.switch || vprev?.block === Block.switchPressed) { if (cNewCell.block !== Block.movable && cNewCell.block !== Block.fallable) { console.warn("No block other than movable cannot be placed on the switch"); console.log("cell.block", cNewCell.block); @@ -338,7 +357,10 @@ export class Grid { stage.addChild(movableSprite); assert(cNewCell.objectId !== undefined, "movable block must have objectId"); assert( - (cprev.block === Block.switch || cprev.block === Block.movable || cprev.block === Block.fallable) && + (cprev.block === Block.switch || + cprev.block === Block.switchPressed || + cprev.block === Block.movable || + cprev.block === Block.fallable) && cprev.switchId !== undefined, "block is not switch", ); @@ -354,6 +376,7 @@ export class Grid { get(cx.state).switches.filter((s) => { if (s.x === x && s.y === y) { s.pressedByBlock = true; + s.pressedByPlayer = false; } return s; }); @@ -391,50 +414,60 @@ export class Grid { }); } // switchingBlockOFFがONに切り替わるとき - else if (vprev?.block === Block.switchingBlockOFF) { - if (cNewCell.block !== Block.switchingBlockON) { + else if (vprev?.block === Block.switchingBlockOFF || vprev?.block === Block.inverseSwitchingBlockOFF) { + if (cNewCell.block !== Block.switchingBlockON && cNewCell.block !== Block.inverseSwitchingBlockON) { console.warn("No block other than switchingBlockON cannot replace the switchingBlockOFF"); return; } assert( - cprev.block === Block.switchingBlockOFF || cprev.block === Block.switchingBlockON, + cprev.block === Block.switchingBlockOFF || + cprev.block === Block.switchingBlockON || + cprev.block === Block.inverseSwitchingBlockOFF || + cprev.block === Block.inverseSwitchingBlockON, "block is not switchingBlock", ); + const inversed = vprev.block === Block.inverseSwitchingBlockOFF; + const switchONBlock = inversed ? Block.inverseSwitchingBlockON : Block.switchingBlockON; const switchId = cprev.switchId; if (!switchId) throw new Error("switchId is undefined"); - const blockSprite = createSprite(blockSize, Block.switchingBlockON, x, y, marginY, switchColor(switchId)); + const blockSprite = createSprite(blockSize, switchONBlock, x, y, marginY, switchColor(switchId)); stage.addChild(blockSprite); cells[y][x] = { - block: Block.switchingBlockON, + block: switchONBlock, switchId, objectId: undefined, }; vprev.sprite = blockSprite; - vprev.block = Block.switchingBlockON; + vprev.block = switchONBlock; vprev.dy = 0; vprev.vy = 0; } // switchingBlockONがOFFに切り替わるとき - else if (vprev?.block === Block.switchingBlockON) { - if (cNewCell.block !== Block.switchingBlockOFF) { + else if (vprev?.block === Block.switchingBlockON || vprev?.block === Block.inverseSwitchingBlockON) { + if (cNewCell.block !== Block.switchingBlockOFF && cNewCell.block !== Block.inverseSwitchingBlockOFF) { console.warn("No block other than switchingBlockOFF cannot replace the switchingBlockON"); return; } assert( - cprev.block === Block.switchingBlockOFF || cprev.block === Block.switchingBlockON, + cprev.block === Block.switchingBlockOFF || + cprev.block === Block.switchingBlockON || + cprev.block === Block.inverseSwitchingBlockOFF || + cprev.block === Block.inverseSwitchingBlockON, "block is not switchingBlock", ); + const inversed = vprev.block === Block.inverseSwitchingBlockON; + const switchOFFBlock = inversed ? Block.inverseSwitchingBlockOFF : Block.switchingBlockOFF; const switchId = cprev.switchId; if (!switchId) throw new Error("switchId is undefined"); - const blockSprite = createSprite(blockSize, Block.switchingBlockOFF, x, y, marginY, switchColor(switchId)); + const blockSprite = createSprite(blockSize, switchOFFBlock, x, y, marginY, switchColor(switchId)); stage.addChild(blockSprite); cells[y][x] = { - block: Block.switchingBlockOFF, + block: switchOFFBlock, switchId, objectId: undefined, }; vprev.sprite = blockSprite; - vprev.block = Block.switchingBlockOFF; + vprev.block = switchOFFBlock; vprev.dy = 0; vprev.vy = 0; } else { @@ -507,6 +540,19 @@ export class Grid { tick(cx: Context, ticker: Ticker) { const { blockSize, gridX, gridY, marginY } = get(cx.config); const cells = get(cx.state).cells; + + for (const s of this.oobFallableSprites) { + if (s.sprite) { + s.sprite.y += s.vy * ticker.deltaTime; + s.vy += consts.gravity * blockSize * ticker.deltaTime; + if (s.sprite.y > gridY * blockSize + marginY * 2) { + // 画面外に出たら消す + s.sprite.parent?.removeChild(s.sprite); + s.sprite = null; + } + } + } + for (let y = gridY - 1; y >= 0; y--) { for (let x = 0; x < gridX; x++) { const vcell = this.__vsom[y][x]; @@ -522,14 +568,20 @@ export class Grid { } let swapDiff = 0; + let goOOB = false; while (swapDiff * blockSize <= vcell.dy) { // 下にブロックがあるなどの要因で止まる if (!isAvail(cells, x, y + swapDiff + 1)) break; vcell.dy -= blockSize; swapDiff++; + if (y + swapDiff >= cells.length) { + goOOB = true; + break; + } } + // これ以上下に行けない - if (!isAvail(cells, x, y + swapDiff)) { + if (!goOOB && !isAvail(cells, x, y + swapDiff)) { // 着地 (dy はたいてい 0 未満なので、別で判定が必要) if (vcell.dy >= 0) { vcell.dy = 0; @@ -538,7 +590,18 @@ export class Grid { } vcell.sprite.y = y * blockSize + marginY + vcell.dy; - if (swapDiff > 0) { + + if (goOOB) { + // vcell.sprite はsetBlockで消えてしまうので、あたらしく作る + const oobSprite = createSprite(blockSize, Block.fallable, x, y, marginY); + oobSprite.y = (y + swapDiff) * blockSize + marginY + vcell.dy; + cx._stage_container.addChild(oobSprite); + this.oobFallableSprites.push({ + sprite: oobSprite, + vy: vcell.vy, + }); + this.setBlock(cx, x, y, { block: null }); + } else if (swapDiff > 0) { this.setBlock(cx, x, y, { block: null }); this.setBlock(cx, x, y + swapDiff, ccell); const vSwapCell = this.__vsom[y + swapDiff][x]; @@ -561,8 +624,7 @@ function isAvail(cells: GridCell[][], x: number, y: number) { switch (true) { case y >= cells.length: // 床が抜けている - // どうする? - return false; + return true; case cell.block === null || cell.block === Block.switch: // 下にブロックがない return true; @@ -609,6 +671,8 @@ export function createCellsFromStageDefinition(stageDefinition: StageDefinition) } // switches case Block.switch: + case Block.inverseSwitchingBlockON: + case Block.inverseSwitchingBlockOFF: case Block.switchingBlockON: case Block.switchingBlockOFF: { const group = stageDefinition.switchGroups.find((b) => b.x === x && b.y === y); @@ -681,6 +745,7 @@ function createSprite( updateSprite(switchBaseSprite, blockSize, x, y, marginY, 0); return switchBaseSprite; } + case Block.inverseSwitchingBlockON: case Block.switchingBlockOFF: { const sprite = new Sprite(rockTexture); if (switchColor) sprite.tint = switchColor; @@ -688,6 +753,7 @@ function createSprite( updateSprite(sprite, blockSize, x, y, marginY, 0); return sprite; } + case Block.inverseSwitchingBlockOFF: case Block.switchingBlockON: { const sprite = new Sprite(rockTexture); if (switchColor) sprite.tint = switchColor; diff --git a/src/history.ts b/src/history.ts index 1d4cfa9..698b673 100644 --- a/src/history.ts +++ b/src/history.ts @@ -105,6 +105,7 @@ function restore(cx: Context, ss: StateSnapshot) { cx.dynamic.player.y = ss.playerY; cx.dynamic.player.facing = ss.playerFacing; cx.grid.diffAndUpdateTo(cx, ss.game.cells); + cx.grid.clearFallableSprites(cx); printCells(ss.game.cells, "restore"); } function stash(cx: Context) { diff --git a/src/main.ts b/src/main.ts index 1910142..5b3561b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -73,11 +73,11 @@ export async function setup( }; const initialGameState = { inventory: null, - inventoryIsInfinite: false, + inventoryIsInfinite: !!stageDefinition.inventoryIsInfinite, usage: stageDefinition.usage ?? { copy: 0, - cut: Infinity, - paste: Infinity, + cut: Number.POSITIVE_INFINITY, + paste: Number.POSITIVE_INFINITY, }, cells: createCellsFromStageDefinition(stageDefinition), paused: false, diff --git a/src/player.ts b/src/player.ts index f7da457..3dfc261 100644 --- a/src/player.ts +++ b/src/player.ts @@ -7,13 +7,7 @@ import { Block } from "./constants.ts"; import type { AbilityInit, Context } from "./public-types.ts"; import { highlightHoldTexture, highlightTexture } from "./resources.ts"; -export function init( - cx: Context, - spriteOptions?: SpriteOptions | Texture, - options?: { - ability?: AbilityInit; - }, -) { +export function init(cx: Context, spriteOptions?: SpriteOptions | Texture) { const sprite = new Sprite(spriteOptions); // Center the sprite's anchor point sprite.anchor.set(0.5, 1); @@ -36,7 +30,7 @@ export function init( document.addEventListener("keydown", (event) => handleInput(cx, event, true)); document.addEventListener("keyup", (event) => handleInput(cx, event, false)); console.log("player init"); - Ability.init(cx, options?.ability); + Ability.init(cx); return { sprite, get coords() { @@ -185,6 +179,7 @@ export function tick(cx: Context, ticker: Ticker) { cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== Block.switch && cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== Block.switchPressed && cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== Block.switchingBlockON && + cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== Block.inverseSwitchingBlockOFF && cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== Block.goal && cx.grid.getBlock(cx, Math.floor(x), Math.floor(y)) !== undefined; const isSwitchBase = (x: number, y: number) => @@ -254,7 +249,10 @@ export function tick(cx: Context, ticker: Ticker) { pressedByPlayer: true, }; } - return s; + return { + ...s, + pressedByPlayer: false, + }; }); return prev; }); @@ -282,14 +280,18 @@ export function tick(cx: Context, ticker: Ticker) { // スイッチの状態を反映 const switches = get(cx.state).switches; - for (const s of switches) { - const switchingBlock = get(cx.state).switchingBlocks.filter((sb) => sb.id === s.id); + const switchIds = [...new Set(switches.map((s) => s.id))]; // 重複削除 + for (const sId of switchIds) { + const switchingBlock = get(cx.state).switchingBlocks.filter((sb) => sb.id === sId); // スイッチが押されているとき - if (s.pressedByPlayer || s.pressedByBlock) { + if (switches.filter((s) => s.id === sId).some((s) => s.pressedByPlayer || s.pressedByBlock)) { for (const sb of switchingBlock) { if (cx.grid.getBlock(cx, sb.x, sb.y) === Block.switchingBlockOFF) { cx.grid.setBlock(cx, sb.x, sb.y, { block: Block.switchingBlockON }); } + if (cx.grid.getBlock(cx, sb.x, sb.y) === Block.inverseSwitchingBlockOFF) { + cx.grid.setBlock(cx, sb.x, sb.y, { block: Block.inverseSwitchingBlockON }); + } } } else { // スイッチが押されていないとき @@ -297,6 +299,9 @@ export function tick(cx: Context, ticker: Ticker) { if (cx.grid.getBlock(cx, sb.x, sb.y) === Block.switchingBlockON) { cx.grid.setBlock(cx, sb.x, sb.y, { block: Block.switchingBlockOFF }); } + if (cx.grid.getBlock(cx, sb.x, sb.y) === Block.inverseSwitchingBlockON) { + cx.grid.setBlock(cx, sb.x, sb.y, { block: Block.inverseSwitchingBlockOFF }); + } } } } diff --git a/src/public-types.ts b/src/public-types.ts index 3702d12..2e803b9 100644 --- a/src/public-types.ts +++ b/src/public-types.ts @@ -105,7 +105,6 @@ export type Coords = { // Ability export type AbilityInit = { enabled?: AbilityUsage; - inventoryIsInfinite?: boolean; }; export type AbilityUsage = { // 回数 or Number.POSITIVE_INFINITY diff --git a/src/stage-preprocessor.ts b/src/stage-preprocessor.ts index 95d5b46..4769e81 100644 --- a/src/stage-preprocessor.ts +++ b/src/stage-preprocessor.ts @@ -128,6 +128,6 @@ function validate(input: PreprocessInput) { function validateLen(stage: Stage, expectY: number, expectX: number) { assert(stage.length === expectY, `stage length do not equal: expected ${expectY}, got ${stage.length}`); for (const row of stage) { - assert(row.length === expectX, `stage rows' lengths are not equal: expected ${length}, got ${row.length}`); + assert(row.length === expectX, `stage rows' lengths are not equal: expected ${expectX}, got ${row.length}`); } } diff --git a/src/stages.ts b/src/stages.ts index 84c57d6..5dc6b7b 100644 --- a/src/stages.ts +++ b/src/stages.ts @@ -22,6 +22,8 @@ export const stages = new Map([ ["8", stage8], ["3-1", world3.stage1], ["3-2", world3.stage2], + ["3-3", world3.stage3], + ["3-4", world3.stage4], ["4-1", world4.stage1], ["4-2", world4.stage2], ["4-3", world4.stage3], diff --git a/src/stages/type.ts b/src/stages/type.ts index a9183a5..e4d7380 100644 --- a/src/stages/type.ts +++ b/src/stages/type.ts @@ -26,6 +26,7 @@ export type StageDefinition = { isTutorial?: boolean; initialPlayerX: number; // 左端から0-indexed initialPlayerY: number; // 上端から0-indexed +1すると浮かずに地面に立つ + inventoryIsInfinite?: boolean; // ブロックと fallable のグループ // 複数ブロックからなるオブジェクトについては明示的に指定 blockGroups: BlockGroup[]; diff --git a/src/stages/world3.ts b/src/stages/world3.ts index c74d1b0..71acb76 100644 --- a/src/stages/world3.ts +++ b/src/stages/world3.ts @@ -5,47 +5,116 @@ export namespace world3 { stage: [ "bbbbbbbbbbbbbbbbbb", ".........b........", - ".........b........", - ".........w........", - ".s....f..w.....g..", - "bSbbbbbbbbbbbbbbbb", + ".........b......g.", + ".........b...bbbbb", + ".........b...bbbbb", + ".........m...bbbbb", + "bbbbbbbbbbbbbbbbbb", ], - initialPlayerX: 3, - initialPlayerY: 5, + isTutorial: false, + initialPlayerX: 1, + initialPlayerY: 6, + inventoryIsInfinite: true, blockGroups: [], - switchGroups: [ - { - x: 1, - y: 4, - switchId: "1", - }, - { - x: 9, - y: 3, - switchId: "1", - }, - { - x: 9, - y: 4, - switchId: "1", - }, - ], + switchGroups: [], + usage: { + copy: Number.POSITIVE_INFINITY, + cut: Number.POSITIVE_INFINITY, + paste: Number.POSITIVE_INFINITY, + }, }; - // fallable+スイッチのテスト用 あとでけす export const stage2: StageDefinition = { stage: [ "bbbbbbbbbbbbbbbbbb", "..................", - "........bb.....g..", - ".......b.....bbbbb", - "......b......bbbbb", - ".....b.......bbbbb", - "....b........bbbbb", - "bbbbbbbbbb.bbbbbbb", + "................g.", + ".............bbbbb", + "......f......bbbbb", + "......f......bbbbb", + "......f......bbbbb", + "bbbbbbbbb.bb.bbbbb", ], + isTutorial: false, initialPlayerX: 1, - initialPlayerY: 2, + initialPlayerY: 6, + inventoryIsInfinite: true, blockGroups: [], switchGroups: [], + usage: { + copy: Number.POSITIVE_INFINITY, + cut: Number.POSITIVE_INFINITY, + paste: Number.POSITIVE_INFINITY, + }, + }; + export const stage3: StageDefinition = { + stage: [ + "bbbbbbbbbbbbbbbbb", + "g...W........w..b", + "b...bb..bb..bb..b", + "b...bb..bb..bb..b", + "b....bf..bs..bm.b", + "b..bbbbbbbSbbbbbb", + ], + isTutorial: false, + initialPlayerX: 5, + initialPlayerY: 1, + inventoryIsInfinite: true, + blockGroups: [], + switchGroups: [ + { x: 4, y: 1, switchId: "1" }, + { x: 13, y: 1, switchId: "1" }, + { x: 10, y: 4, switchId: "1" }, + ], + usage: { + copy: Number.POSITIVE_INFINITY, + cut: Number.POSITIVE_INFINITY, + paste: Number.POSITIVE_INFINITY, + }, + }; + export const stage4: StageDefinition = { + stage: [ + // 2345678901234567890123 + "bbbbbbbbbbbbbbbbbbbbbbbb", + "bbbb..W..w.....bmmmmW...", // 1 + "bbbb..bb.w.........W....", // 2 + ".......b.bbbbbbbbbbb....", // 3 + ".s............w....w....", // 4 + "bSbbb.........w..m.w...b", // 5 + "bbbbb.........bbbbbb..bb", // 6 + "bbbbb.........bg.....bbb", // 7 + "bbbbbssss...f.b.....bbbb", // 8 + "bbbbbSSSSbbbbbb....bbbbb", + ], + isTutorial: false, + initialPlayerX: 10, + initialPlayerY: 8, + inventoryIsInfinite: true, + blockGroups: [ + { x: 16, y: 1, objectId: "1" }, + { x: 17, y: 1, objectId: "1" }, + { x: 18, y: 1, objectId: "1" }, + { x: 19, y: 1, objectId: "1" }, + ], + switchGroups: [ + { x: 5, y: 8, switchId: "1" }, + { x: 6, y: 8, switchId: "1" }, + { x: 7, y: 8, switchId: "1" }, + { x: 8, y: 8, switchId: "1" }, + { x: 1, y: 4, switchId: "2" }, + { x: 6, y: 1, switchId: "1" }, + { x: 9, y: 1, switchId: "1" }, + { x: 9, y: 2, switchId: "1" }, + { x: 14, y: 4, switchId: "2" }, + { x: 14, y: 5, switchId: "2" }, + { x: 19, y: 4, switchId: "2" }, + { x: 19, y: 5, switchId: "2" }, + { x: 19, y: 2, switchId: "2" }, + { x: 20, y: 1, switchId: "2" }, + ], + usage: { + copy: Number.POSITIVE_INFINITY, + cut: Number.POSITIVE_INFINITY, + paste: Number.POSITIVE_INFINITY, + }, }; }