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,