diff --git a/assets/icons/rematch.svg b/assets/icons/rematch.svg
new file mode 100644
index 000000000..a91eba786
--- /dev/null
+++ b/assets/icons/rematch.svg
@@ -0,0 +1 @@
+
diff --git a/src/abilities/Gumble.ts b/src/abilities/Gumble.ts
index 0d364cd12..dfe6289cf 100644
--- a/src/abilities/Gumble.ts
+++ b/src/abilities/Gumble.ts
@@ -20,11 +20,26 @@ export default (G: Game) => {
// Trigger when Gumble dies
trigger: 'onCreatureDeath',
+ /**
+ * Provides flying movement when upgraded, allowing Gumble to leap over units.
+ * Only applies when the ability is upgraded.
+ * @returns {string|undefined} 'flying' if upgraded, undefined otherwise
+ */
+ movementType: function () {
+ return this.isUpgraded() ? 'flying' : undefined;
+ },
+
require: function () {
return true;
},
activate: function (deadCreature: Creature) {
+ // If upgraded, no trap is created - instead Gumble gains flying movement
+ // which allows it to leap over units during the moving phase
+ if (this.isUpgraded()) {
+ return;
+ }
+
const deathHex = G.grid.hexAt(deadCreature.x, deadCreature.y);
const ability = this;
@@ -46,12 +61,7 @@ export default (G: Game) => {
return false;
}
- // If upgraded, don't affect allied units
- if (ability.isUpgraded()) {
- return creatureOnGoo.player !== ability.creature.player;
- }
-
- // Otherwise affect all units
+ // Trap affects all units when not upgraded
return true;
},
effectFn: function (_, creature: Creature) {
@@ -206,6 +216,26 @@ export default (G: Game) => {
// eslint-disable-next-line
ability.animation(...arguments);
},
+ fnOnSelect: function (hex: Hex) {
+ // Show creature cardboard at 10% opacity at the target hex
+ // for direct movement ability feedback (issue #2534)
+ if (hex.x !== creature.x || hex.y !== creature.y) {
+ const creaData = G.retrieveCreatureStats(creature.type);
+ G.grid.previewCreature(
+ { x: hex.x, y: hex.y },
+ creaData,
+ creature.player,
+ false,
+ 0.1,
+ );
+ }
+ },
+ fnOnCancel: function () {
+ if (G.grid.materialize_overlay) {
+ G.grid.materialize_overlay.alpha = 0;
+ }
+ creature.queryMove();
+ },
size: creature.size,
flipped: creature.player.flipped,
id: creature.id,
diff --git a/src/game.ts b/src/game.ts
index d1ca5f98c..b326b6ffb 100644
--- a/src/game.ts
+++ b/src/game.ts
@@ -1606,6 +1606,17 @@ export default class Game {
this.gamelog.reset();
}
+ rematch() {
+ const savedConfig = this.configData;
+ this.resetGame();
+ this.configData = savedConfig;
+ if (this.configData && Object.keys(this.configData).length > 0) {
+ this.loadGame(this.configData, undefined, undefined, () => {});
+ } else {
+ this.UI.showGameSetup();
+ }
+ }
+
/**
* Setup signal channels based on a list of channel names.
*
diff --git a/src/index.ejs b/src/index.ejs
index 1c4380416..21b5cabe6 100644
--- a/src/index.ejs
+++ b/src/index.ejs
@@ -330,6 +330,16 @@
Give up but only after first 12 rounds.
+
+
+
+
+
+
hotkey R
+
Rematch
+
Start a new match with the same settings.
+
+
diff --git a/src/style/styles.less b/src/style/styles.less
index 03e4a8738..5db38afc4 100644
--- a/src/style/styles.less
+++ b/src/style/styles.less
@@ -431,6 +431,10 @@
background-image: url('~assets/icons/exit.svg');
}
+.button#rematch {
+ background-image: url('~assets/icons/rematch.svg');
+}
+
.p0.player-text.bright {
color: #f55;
}
@@ -1046,6 +1050,22 @@ div.section.info {
background-color: rgba(0, 0, 0, 0.85);
}
+// Delay preview - semi-transparent avatar shown in queue on hover
+.vignette.delay-preview {
+ pointer-events: none;
+ z-index: 2;
+ animation: delayPreviewPulse 1s ease-in-out infinite alternate;
+}
+
+@keyframes delayPreviewPulse {
+ from {
+ opacity: 0.35;
+ }
+ to {
+ opacity: 0.6;
+ }
+}
+
.vignette.hex .frame {
background-image: url('~assets/interface/frame.png');
background-color: rgba(0, 0, 0, 0.85);
@@ -1707,6 +1727,7 @@ span.pure {
margin-top: 20px;
margin-bottom: 50px;
#flee,
+ #rematch,
#exit {
margin-left: auto;
margin-right: auto;
diff --git a/src/ui/hotkeys.ts b/src/ui/hotkeys.ts
index 7f36f2e17..a2a5c07c2 100644
--- a/src/ui/hotkeys.ts
+++ b/src/ui/hotkeys.ts
@@ -48,7 +48,11 @@ export class Hotkeys {
}
pressR() {
- this.ui.dashopen ? this.ui.closeDash() : this.ui.abilitiesButtons[3].triggerClick();
+ if (!this.ui.$scoreboard.hasClass('hide')) {
+ this.ui.btnRematch.triggerClick();
+ } else {
+ this.ui.dashopen ? this.ui.closeDash() : this.ui.abilitiesButtons[3].triggerClick();
+ }
}
pressA(event) {
diff --git a/src/ui/interface.ts b/src/ui/interface.ts
index 14453859e..25afe0539 100644
--- a/src/ui/interface.ts
+++ b/src/ui/interface.ts
@@ -75,10 +75,13 @@ export class UI {
btnSkipTurn: Button;
dashopen: boolean;
animationUpgradeTimeOutID: ReturnType
;
+ animationUpgradeSoundTimeOutID: ReturnType;
+ animationUpgradeIconTimeOutID: ReturnType;
queryUnit: string;
btnDelay: Button;
btnFlee: Button;
btnExit: Button;
+ btnRematch: Button;
materializeButton: Button;
clickedAbility: number;
selectedAbility: number;
@@ -213,6 +216,13 @@ export class UI {
// Prevents upgrade animation from carrying on into opponent's turn and disabling their button
clearTimeout(this.animationUpgradeTimeOutID);
+ clearTimeout(this.animationUpgradeSoundTimeOutID);
+ clearTimeout(this.animationUpgradeIconTimeOutID);
+ // Also remove any upgrade animation classes that might be lingering
+ this.abilitiesButtons.forEach((btn) => {
+ btn.$button.removeClass('upgradeTransition');
+ btn.$button.removeClass('upgradeIcon');
+ });
game.skipTurn();
this.lastViewedCreature = '';
@@ -301,6 +311,24 @@ export class UI {
);
this.buttons.push(this.btnExit);
+ this.btnRematch = new Button(
+ {
+ $button: $j('#rematch.button'),
+ hasShortcut: true,
+ click: () => {
+ if (this.dashopen) {
+ return;
+ }
+ if (window.confirm('Are you sure you want to rematch with the same settings?')) {
+ game.rematch();
+ }
+ },
+ state: ButtonStateEnum.normal,
+ },
+ { isAcceptingInput: this.configuration.isAcceptingInput },
+ );
+ this.buttons.push(this.btnRematch);
+
this.materializeButton = new Button(
{
$button: $j('#materialize_button'),
@@ -2029,12 +2057,12 @@ export class UI {
btn.changeState(ButtonStateEnum.slideIn); // Keep the button in view
// After .3s play the upgrade sound
- setTimeout(() => {
+ this.animationUpgradeSoundTimeOutID = setTimeout(() => {
game.soundsys.playSFX('sounds/upgrade');
}, 300);
// After 2s remove the background and update the button if it's not a passive
- setTimeout(() => {
+ this.animationUpgradeIconTimeOutID = setTimeout(() => {
btn.$button.removeClass('upgradeIcon');
}, 1200);
diff --git a/src/utility/hexgrid.ts b/src/utility/hexgrid.ts
index 6dde46c9d..2622c756b 100644
--- a/src/utility/hexgrid.ts
+++ b/src/utility/hexgrid.ts
@@ -1671,7 +1671,7 @@ export class HexGrid {
* @param {{x:number, y:number}} pos - Coordinates {x,y}
* @param {object} creatureData - Object containing info from the database (game.retrieveCreatureStats)
*/
- previewCreature(pos, creatureData, player, secondary = false) {
+ previewCreature(pos, creatureData, player, secondary = false, opacity = 0.5) {
const game = this.game;
const hex = this.hexes[pos.y][pos.x - (creatureData.size - 1)];
const cardboard =
@@ -1721,7 +1721,7 @@ export class HexGrid {
creatureData.display['offset-x']) +
preview.texture.width / 2;
preview.y = hex.displayPos.y + creatureData.display['offset-y'] + preview.texture.height;
- preview.alpha = 0.5;
+ preview.alpha = opacity;
if (player.flipped) {
preview.scale.setTo(-1, 1);
@@ -1729,11 +1729,13 @@ export class HexGrid {
preview.scale.setTo(1, 1);
}
+ // Flicker between opacity and 30% of opacity (minimum 0.05)
+ const flickerTarget = Math.max(opacity * 0.3, 0.05);
const flickering = game.Phaser.add
.tween(preview)
.to(
{
- alpha: 0.15,
+ alpha: flickerTarget,
},
777,
Phaser.Easing.Linear.None,