Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions assets/icons/undo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 14 additions & 1 deletion src/abilities/Gumble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export default (G: Game) => {
return true;
},

/**
* Provides leap movement when upgraded.
* Allows Gumble to leap over units when moving at least 2 hexagons.
* @return {string} movement type, 'leap' when upgraded, undefined otherwise
*/
movementType: function () {
return this.isUpgraded() ? 'leap' : undefined;
},

activate: function (deadCreature: Creature) {
const deathHex = G.grid.hexAt(deadCreature.x, deadCreature.y);

Expand Down Expand Up @@ -84,7 +93,11 @@ export default (G: Game) => {
G.log('%CreatureName' + deadCreature.id + '% melts into a gooey puddle');
};

createGooTrap();
// Only create the trap when NOT upgraded.
// When upgraded, Gumble gains leap movement instead.
if (!this.isUpgraded()) {
createGooTrap();
}
},
},

Expand Down
9 changes: 8 additions & 1 deletion src/creature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export type CreatureMasteries = {
mental: number;
};

export type Movement = 'normal' | 'flying' | 'hover';
export type Movement = 'normal' | 'flying' | 'hover' | 'leap';

type CreatureStats = CreatureVitals &
CreatureMasteries & {
Expand Down Expand Up @@ -665,6 +665,7 @@ export class Creature {
.start();
}

game.saveUndoSnapshot();
game.gamelog.add({
action: 'move',
target: {
Expand Down Expand Up @@ -718,6 +719,8 @@ export class Creature {

if (this.movementType() === 'flying') {
o.range = game.grid.getFlyingRange(this.x, this.y, remainingMove, this.size, this.id);
} else if (this.movementType() === 'leap') {
o.range = game.grid.getLeapRange(this.x, this.y, remainingMove, this.size, this.id);
}

const selectNormal = function (hex, args) {
Expand Down Expand Up @@ -961,6 +964,10 @@ export class Creature {
game.signals.creature.dispatch('movementComplete', { creature: this, hex });
// @ts-expect-error 2554
game.onCreatureMove(this, hex); // Trigger
// Enable Undo Move button after movement completes if undo is available
if (game.undoAvailable && !game.undoUsedThisRound && game.UI?.btnUndo) {
game.UI.btnUndo.changeState('slideIn');
}
}
}, 100);
}
Expand Down
119 changes: 119 additions & 0 deletions src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export default class Game {
freezedInput: boolean;
turnThrottle: boolean;
turn: number;
undoAvailable: boolean;
undoUsedThisRound: boolean;
undoSnapshot: any;
Phaser: Phaser;
msg: any; // type this properly
triggers: Record<string, RegExp>;
Expand Down Expand Up @@ -190,6 +193,10 @@ export default class Game {
this.freezedInput = false;
this.turnThrottle = false;
this.turn = 0;
// Undo Move properties
this.undoAvailable = false;
this.undoUsedThisRound = false;
this.undoSnapshot = null;

// Phaser
this.Phaser = new Phaser.Game(1920, 1080, Phaser.AUTO, 'combatwrapper', {
Expand Down Expand Up @@ -755,6 +762,7 @@ export default class Game {
nextRound() {
this.turn++;
this.log(`Round ${this.turn}`, 'roundmarker', true);
this.undoUsedThisRound = false;
this.onStartOfRound();
this.nextCreature();
}
Expand Down Expand Up @@ -997,6 +1005,117 @@ export default class Game {
this.nextCreature();
}

/**
* Save the current creature state for the Undo Move feature.
* Called before a move or ability action.
*/
saveUndoSnapshot() {
if (!this.activeCreature || this.undoUsedThisRound) {
return;
}

const crea = this.activeCreature;
const game = this;

// Save creature's current state
this.undoSnapshot = {
creatureId: crea.id,
x: crea.x,
y: crea.y,
hexagons: crea.hexagons.map((h) => ({ x: h.x, y: h.y })),
health: crea.health,
energy: crea.energy,
remainingMove: crea.remainingMove,
travelDist: crea.travelDist,
stats: $j.extend(true, {}, crea.stats),
status: $j.extend(true, {}, crea.status),
oldEnergy: crea.oldEnergy,
oldHealth: crea.oldHealth,
// Save grid hex -> creature mapping for the creature's hexes
hexCreatures: crea.hexagons.map((h) => ({
x: h.x,
y: h.y,
creatureId: h.creature ? h.creature.id : null,
})),
};

this.undoAvailable = true;
}

/**
* Restore the creature state from the Undo Move snapshot.
*/
restoreUndoSnapshot() {
if (!this.undoSnapshot || !this.undoAvailable || this.undoUsedThisRound) {
return false;
}

const snap = this.undoSnapshot;
const crea = this.creatures.find((c) => c.id === snap.creatureId);

if (!crea) {
return false;
}

const game = this;

// Clear current hex assignments
crea.hexagons.forEach((h) => {
h.creature = undefined;
});
crea.hexagons = [];

// Restore position
crea.x = snap.x;
crea.y = snap.y;

// Restore hexagons
for (let i = 0; i < snap.hexagons.length; i++) {
const h = game.grid.hexes[snap.hexagons[i].y][snap.hexagons[i].x];
crea.hexagons.push(h);
h.creature = crea;
}

// Restore creature display position
if (crea.creatureSprite) {
crea.creatureSprite.setHex(crea.hexagons[0]);
}

// Restore stats
crea.health = snap.health;
crea.energy = snap.energy;
crea.remainingMove = snap.remainingMove;
crea.travelDist = snap.travelDist;
crea.stats = $j.extend(true, {}, snap.stats);
crea.status = $j.extend(true, {}, snap.status);
crea.oldEnergy = snap.oldEnergy;
crea.oldHealth = snap.oldHealth;

// Update UI
if (game.UI) {
game.UI.healthBar.animSize(crea.health / crea.stats.health);
game.UI.energyBar.animSize(crea.energy / crea.stats.energy);
game.UI.updateActivebox();
}

// Update queue display
game.updateQueueDisplay();

// Reset undo state
this.undoAvailable = false;
this.undoUsedThisRound = true;
this.undoSnapshot = null;

// Re-enable delay button if creature can still delay
if (crea.canWait && !this.queue.isCurrentEmpty()) {
this.UI.btnDelay.changeState('slideIn');
}

this.log('Undo Move: restored creature to previous state');

return true;
}

startTimer() {
clearInterval(this.timeInterval);

Expand Down
9 changes: 9 additions & 0 deletions src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,15 @@
<p>Delayed creatures will act at the end of the round, if alive and still able to.</p>
</div>
</div>
<div style="position:relative">
<div id="undo" class="button"></div>
<div class="desc">
<div class="arrow"></div>
<div class="shortcut">Ctrl+Z</div>
<span>Undo Move</span>
<p>Undo the last action and restore the previous state. Usable once per round.</p>
</div>
</div>
<div style="position:relative" >
<div id="fullscreen" class="button slideIn" >
<div></div>
Expand Down
4 changes: 4 additions & 0 deletions src/style/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@
background-image: url('~assets/icons/delay.svg');
}

.button#undo {
background-image: url('~assets/icons/undo.svg');
}

.button#flee {
background-image: url('~assets/icons/flee.svg');
}
Expand Down
16 changes: 16 additions & 0 deletions src/ui/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ export class Hotkeys {
this.ui.btnExit.triggerClick();
}
}

pressZ(event) {
if (event.ctrlKey) {
// Undo Move
if (this.ui.game.undoAvailable && !this.ui.game.undoUsedThisRound && !this.ui.dashopen) {
this.ui.game.restoreUndoSnapshot();
this.ui.btnUndo.changeState('disabled');
}
}
}

pressTab(event) {
if (this.ui.dashopen) {
if (event.shiftKey) this.ui.gridSelectPrevious();
Expand Down Expand Up @@ -217,6 +228,11 @@ export function getHotKeys(hk) {
hk.pressX(event);
},
},
KeyZ: {
onkeydown(event) {
hk.pressZ(event);
},
},
Tab: {
onkeydown(event) {
hk.pressTab(event);
Expand Down
18 changes: 18 additions & 0 deletions src/ui/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class UI {
animationUpgradeTimeOutID: ReturnType<typeof setTimeout>;
queryUnit: string;
btnDelay: Button;
btnUndo: Button;
btnFlee: Button;
btnExit: Button;
materializeButton: Button;
Expand Down Expand Up @@ -249,6 +250,23 @@ export class UI {
);
this.buttons.push(this.btnDelay);

// Undo Move Button
this.btnUndo = new Button(
{
$button: $j('#undo.button'),
hasShortcut: false,
click: () => {
if (!this.dashopen && game.undoAvailable && !game.undoUsedThisRound) {
game.restoreUndoSnapshot();
this.btnUndo.changeState('disabled');
}
},
state: ButtonStateEnum.disabled,
},
{ isAcceptingInput: this.configuration.isAcceptingInput },
);
this.buttons.push(this.btnUndo);

// Flee Match Button
this.btnFlee = new Button(
{
Expand Down
19 changes: 13 additions & 6 deletions src/utility/hex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,11 @@
* @param {number} size - Size of the creature.
* @param {number} id - ID of the creature.
* @param {boolean} ignoreReachable - Take into account the reachable property.
* @param {boolean} ignoreCreatures - Ignore creatures when checking walkability (for leap movement).
* @param {boolean} debug - If true and const.DEBUG is true, print debug information to the console.
* @returns True if this hex is walkable.
*/
isWalkable(size: number, id: number, ignoreReachable = false, debug = false) {
isWalkable(size: number, id: number, ignoreReachable = false, ignoreCreatures = false, debug = false) {

Check failure on line 406 in src/utility/hex.ts

View workflow job for this annotation

GitHub Actions / test

Replace `size:·number,·id:·number,·ignoreReachable·=·false,·ignoreCreatures·=·false,·debug·=·false` with `⏎↹↹size:·number,⏎↹↹id:·number,⏎↹↹ignoreReachable·=·false,⏎↹↹ignoreCreatures·=·false,⏎↹↹debug·=·false,⏎↹`
// NOTE: If not in DEBUG mode, don't debug.
debug = DEBUG && debug;

Expand All @@ -421,7 +422,7 @@
}

let isNotMovingCreature;
if (hex.creature instanceof Creature) {
if (hex.creature instanceof Creature && !ignoreCreatures) {
isNotMovingCreature = hex.creature.id !== id;
blocked = blocked || isNotMovingCreature; // Not blocked if this block contains the moving creature
}
Expand Down Expand Up @@ -568,6 +569,11 @@
const player = this.displayClasses.match(/0|1|2|3/);
this.display.loadTexture(`hex_p${player}`);
this.grid.displayHexesGroup.bringToTop(this.display);
} else if (this.displayClasses.match(/dashedGrid/g)) {
// Dashed hex grid overlay at 25% opacity for empty (non-unit occupied) hexes
this.display.loadTexture('hex_dashed');
this.display.alpha = 0.25;
this.grid.displayHexesGroup.bringToTop(this.display);
} else if (this.displayClasses.match(/adj/)) {
this.display.loadTexture('hex_path');
} else if (this.displayClasses.match(/dashed/)) {
Expand Down Expand Up @@ -615,13 +621,14 @@
align: 'center',
},
);
if (this.creature || this.trap || this.drop) {
this.coordText.stroke = '#ffffff';
this.coordText.strokeThickness = 5;
}
// Always add white stroke for visibility on any background (units, traps, drops, etc.)
this.coordText.stroke = '#ffffff';
this.coordText.strokeThickness = 5;
this.coordText.anchor.setTo(0.5);
this.grid.overlayHexesGroup.add(this.coordText);
}
// Ensure coord text is always on top of everything in the overlay group
this.grid.overlayHexesGroup.bringToTop(this.coordText);
} else if (this.coordText && this.coordText.exists) {
this.coordText.destroy();
}
Expand Down
Loading
Loading