- commit
- 2cc00719f9e49d43da6acb1e09b56b1c3bc2f719
- parent
- 8fe3dd69b67734cb2b465ea248c1cf01e3c28f55
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 25 | +++++++++++++++++++++++++ |
| M | tests/ | 29 | +++++++++++++++++++++++++++++ |
@@ -289,6 +289,9 @@ export function createJuice({ reducedMotion = false, now = () => performance.now
289289 for (const entity of entities.values()) {290290 entity.facing = null;291291 entity.faceFlipStart = null;292+ entity.effect = null;293+ entity.popDuration = 0;294+ entity.bounceDuration = 0;292295 }293296 rules.clear();294297 wipe = { start: at, duration: duration(WIPE_MS) };@@ -372,6 +375,28 @@ export function createJuice({ reducedMotion = false, now = () => performance.now
372375 entity.popDuration = duration(POP_MS);373376 entity.effect = { type: 'transform', text: transform.from || '', cell: { x: entity.x, y: entity.y }, color: 'fg', role: 'fg', start: at, t0: at, duration: duration(EFFECT_MS) };374377 }378+ // Undo can restore an object that had been removed by SINK or DEFEAT.379+ // Such an object has no movement entry because it did not move in the380+ // undo diff, so reconcile the settled state to seed its visual position.381+ if (result.kind === 'undo' && state && Array.isArray(state.objects)) {382+ const restoredIds = new Set(state.objects.map(object => object.id));383+ let restoredDeathEffect = false;384+ for (const [id, entity] of entities) {385+ if (!restoredIds.has(id) || !['sink', 'defeat'].includes(entity.effect?.type)) continue;386+ // A destroyed actor can still be tweening toward its hazard when the387+ // player presses Undo. Remove that stale death presentation before388+ // reconciling the restored settled cell.389+ entity.effect = null;390+ entity.fromX = entity.x;391+ entity.fromY = entity.y;392+ entity.start = at;393+ entity.duration = 0;394+ entity.pushed = false;395+ restoredDeathEffect = true;396+ }397+ if (restoredDeathEffect) globalShake = null;398+ syncState(state, at);399+ }375400 if (result.won && result.kind === 'step') win = { beatStart: at + moveDuration, cardAt: at + moveDuration + WIN_BEAT_MS };376401 }377402 @@ -117,6 +117,35 @@ test('reset clears facing immediately and undo restores nonmoving entities', ()
117117 assert.equal(juice.frame(300).positions.get('rock').facing, null);118118 });119119 120+test('undo reseeds an actor removed by death so its visual returns', () => {121+ const juice = createJuice({ now: () => 0 });122+ const alive = state([object('player', 1, 1)]);123+ const dead = state([]);124+ juice.syncState(alive, 0);125+ juice.consume({ kind: 'step', accepted: true, moved: [{ id: 'player', from: { x: 1, y: 1 }, to: { x: 2, y: 1 }, pushedBy: null }], rulesAdded: [], rulesRemoved: [], destroyed: [{ id: 'player', noun: 'MIAU', at: { x: 2, y: 1 }, cause: 'defeat' }] }, 0, dead);126+ juice.syncState(dead, 10);127+ assert.equal(juice.frame(10).positions.has('player'), true);128+ juice.consume({ kind: 'undo', accepted: true, moved: [], rulesAdded: [], rulesRemoved: [], destroyed: [], restored: true }, 20, alive);129+ assert.deepEqual(130+ { x: juice.frame(20).positions.get('player').x, y: juice.frame(20).positions.get('player').y },131+ { x: 1, y: 1 },132+ );133+ assert.equal(juice.frame(20).effects.some(effect => effect.type === 'defeat' || effect.type === 'sink'), false);134+ assert.equal(juice.frame(20).shake.amplitude, 0);135+});136+137+test('reset clears death effects immediately after a fatal turn', () => {138+ const juice = createJuice({ now: () => 0 });139+ const alive = state([object('player', 1, 1)]);140+ juice.syncState(alive, 0);141+ juice.consume({ kind: 'step', accepted: true, moved: [{ id: 'player', from: { x: 1, y: 1 }, to: { x: 2, y: 1 }, pushedBy: null }], rulesAdded: [], rulesRemoved: [], destroyed: [{ id: 'player', noun: 'MIAU', at: { x: 2, y: 1 }, cause: 'sink' }] }, 0, state([]));142+ juice.consume({ kind: 'reset', accepted: true, moved: [], rulesAdded: [], rulesRemoved: [], destroyed: [] }, 20, alive);143+ const frame = juice.frame(20);144+ assert.deepEqual(frame.effects, []);145+ assert.equal(frame.shake.amplitude, 0);146+ assert.equal(frame.positions.get('player').facing, null);147+});148+120149 test('rule formation waits 45ms after movement lands', () => {121150 const juice = createJuice({ now: () => 0 });122151 const rule = { text: 'MIAU IS YOU', subject: 'MIAU', predicate: 'YOU', sourceIds: ['a', 'b', 'c'], axis: 'h' };