diff --git a/src/creature.ts b/src/creature.ts index f1666550e..d56861e82 100644 --- a/src/creature.ts +++ b/src/creature.ts @@ -681,8 +681,30 @@ export class Creature { }); } game.UI.btnDelay.changeState('disabled'); + + // Determine animation: use 'fly' for Gumble's leap (2+ hexes when ability upgraded) + let animation: string = args.creature.movementType() === 'flying' ? 'fly' : 'walk'; + if (animation === 'walk') { + // Check if Gumble can leap over units for this movement + const isGumbleLeaping = + args.creature.type === 14 && + args.creature.abilities[0] && + args.creature.abilities[0].isUpgraded(); + if (isGumbleLeaping) { + // Use hex distance to determine if this is a 2+ hex leap + // Simple offset coord distance (conservative estimate) + const d = Math.max( + Math.abs(hex.x - args.creature.x), + Math.abs(hex.y - args.creature.y), + ); + if (d >= 2) { + animation = 'fly'; + } + } + } + args.creature.moveTo(hex, { - animation: args.creature.movementType() === 'flying' ? 'fly' : 'walk', + animation: animation, callback: function () { game.activeCreature.queryMove(); }, diff --git a/src/data/units.ts b/src/data/units.ts index f8cdf6475..6155616c1 100644 --- a/src/data/units.ts +++ b/src/data/units.ts @@ -1367,7 +1367,7 @@ export const unitData: UnitDataStructure = [ title: 'Gooey Body', desc: 'When killed, it melts into a puddle that traps any unit that walks on top of it.', info: "Can't move current round while on goo.", - upgrade: 'Does not affect allied units.', + upgrade: 'Can leap over units when moving 2+ hexes.', }, { title: 'Gummy Mallet', diff --git a/src/script.ts b/src/script.ts index aae50221b..a56bd1463 100644 --- a/src/script.ts +++ b/src/script.ts @@ -96,6 +96,9 @@ $j(() => { const fullscreen = new Fullscreen(document.getElementById('fullscreen')); $j('#fullscreen').on('click', () => fullscreen.toggle()); + // Attempt to lock orientation to landscape on page load (works without fullscreen on some browsers) + Fullscreen.lockLandscapeOrientation(); + const startScreenHotkeys = { Space: { keyDownTest() { diff --git a/src/ui/fullscreen.ts b/src/ui/fullscreen.ts index aad819ce1..7c4c7d6b2 100644 --- a/src/ui/fullscreen.ts +++ b/src/ui/fullscreen.ts @@ -20,6 +20,8 @@ export class Fullscreen { const gameElement = document.getElementById('AncientBeast'); if (gameElement) { await gameElement.requestFullscreen(); + // Lock to landscape after entering fullscreen + Fullscreen.lockLandscapeOrientation(); } } @@ -29,6 +31,19 @@ export class Fullscreen { } } + /** + * Attempts to lock the screen orientation to landscape. + * Most browsers require fullscreen to be active before locking orientation. + */ + static lockLandscapeOrientation() { + const screenOrientation = screen as Screen & { orientation?: ScreenOrientation }; + if (screenOrientation.orientation && 'lock' in screenOrientation.orientation) { + (screenOrientation.orientation as ScreenOrientation).lock('landscape').catch((err) => { + console.warn('Could not lock screen orientation:', err); + }); + } + } + updateButtonState() { if (document.fullscreenElement) { this.button.classList.add('fullscreenMode'); diff --git a/src/ui/interface.ts b/src/ui/interface.ts index 14453859e..7783947bf 100644 --- a/src/ui/interface.ts +++ b/src/ui/interface.ts @@ -1071,12 +1071,12 @@ export class UI { .children('#upgrade') .text('Upgrade: ' + stats.ability_info[key].upgrade); - if (stats.ability_info[key].costs !== undefined && key !== 0) { + if (typeof stats.ability_info[key].costs?.energy === 'number' && key !== 0) { $ability .children('.wrapper') .children('.info') .children('#cost') - .text(' - costs ' + stats.ability_info[key].costs.energy + ' energy pts.'); + .text(' - costs ' + stats.ability_info[key].costs!.energy + ' energy pts.'); } else { $ability .children('.wrapper') @@ -1260,12 +1260,12 @@ export class UI { $ability.children('.wrapper').children('.info').children('#upgrade').text(' '); } - if (stats.ability_info[key].costs !== undefined && key !== 0) { + if (typeof stats.ability_info[key].costs?.energy === 'number' && key !== 0) { $ability .children('.wrapper') .children('.info') .children('#cost') - .text(' - costs ' + stats.ability_info[key].costs.energy + ' energy pts.'); + .text(' - costs ' + stats.ability_info[key].costs!.energy + ' energy pts.'); } else { $ability .children('.wrapper') diff --git a/src/utility/hex.ts b/src/utility/hex.ts index 1e3d3bd29..2cbabe838 100644 --- a/src/utility/hex.ts +++ b/src/utility/hex.ts @@ -180,10 +180,23 @@ export class Hex { this.overlay = grid.overlayHexesGroup.create(x, y, 'hex'); this.overlay.alpha = 0; + // Track if last interaction was from touch to avoid double-firing + let lastInteractionTouch = false; + // Binding Events - this.hitBox.events.onInputOver.add(() => { + this.hitBox.events.onInputOver.add((_, pointer) => { if (game.freezedInput || game.UI.dashopen) return; + // Skip hover effect for touch input - on Android, touch triggers + // onInputOver before onInputUp, making users tap twice to act. + // Touch input is handled directly in onInputDown instead. + if (pointer && pointer.pointerType === 'touch') { + lastInteractionTouch = true; + return; + } + + lastInteractionTouch = false; + // Show dashed overlay on current hexes of active creature if (this.reachable && game.activeCreature) { game.activeCreature.highlightCurrentHexesAsDashed(); @@ -194,9 +207,27 @@ export class Hex { this.onSelectFn(this); }, this); + // onInputDown: handle touch directly to avoid double-tap issue on Android + this.hitBox.events.onInputDown.add((_, pointer) => { + if (game.freezedInput || game.UI.dashopen) return; + + // For touch input, immediately trigger the confirm action. + // This bypasses the hover-first behavior that requires double-tapping on Android. + if (pointer && pointer.pointerType === 'touch') { + lastInteractionTouch = true; + this.onConfirmFn(this); + } + }, this); + this.hitBox.events.onInputOut.add((_, pointer) => { if (game.freezedInput || game.UI.dashopen || !pointer.withinGame) return; + // Skip hover-off for touch since we skip hover-on for touch + if (lastInteractionTouch) { + lastInteractionTouch = false; + return; + } + // Clear dashed overlay when leaving a reachable hex if (this.reachable && game.activeCreature) { game.activeCreature.clearDashedOverlayOnHexes(); @@ -212,6 +243,11 @@ export class Hex { return; } + // Skip onInputUp for touch since we already handled it in onInputDown + if (Pointer.pointerType === 'touch') { + return; + } + switch (Pointer.button) { case 0: // Left mouse button pressed diff --git a/src/utility/hexgrid.ts b/src/utility/hexgrid.ts index 6dde46c9d..edfb3a9e8 100644 --- a/src/utility/hexgrid.ts +++ b/src/utility/hexgrid.ts @@ -1495,7 +1495,28 @@ export class HexGrid { }); } + /** + * Compute hex distance between two hexes in the offset coordinate system. + * Converts to cube coordinates for the calculation. + */ + private hexDistance(hex1: { x: number; y: number }, hex2: { x: number; y: number }): number { + // Convert offset coordinates (odd-q) to cube coordinates + const offsetToCube = (x: number, y: number) => { + const cubeX = x - (y - (y & 1)) / 2; + const cubeZ = y; + const cubeY = -cubeX - cubeZ; + return { x: cubeX, y: cubeY, z: cubeZ }; + }; + const c1 = offsetToCube(hex1.x, hex1.y); + const c2 = offsetToCube(hex2.x, hex2.y); + return Math.max(Math.abs(c1.x - c2.x), Math.abs(c1.y - c2.y), Math.abs(c1.z - c2.z)); + } + findCreatureMovementHexes(creature) { + // Gumble (type 14) with upgraded ability 0 can leap over units when moving 2+ hexes + const isGumbleWithLeapUpgrade = + creature.type === 14 && creature.abilities[0] && creature.abilities[0].isUpgraded(); + if (creature.movementType() === 'flying') { return this.getFlyingRange( creature.x, @@ -1505,13 +1526,34 @@ export class HexGrid { creature.id, ); } else { - return this.getMovementRange( + const hexes = this.getMovementRange( creature.x, creature.y, creature.stats.movement, creature.size, creature.id, ); + + // Add flying hexes (2+ hexes away) for Gumble's leap upgrade + if (isGumbleWithLeapUpgrade) { + const flyingHexes = this.getFlyingRange( + creature.x, + creature.y, + creature.stats.movement, + creature.size, + creature.id, + ); + // Only include flying hexes that are at least 2 hexes away + const leapHexes = flyingHexes.filter((hex) => this.hexDistance(creature, hex) >= 2); + // Merge with normal hexes, avoiding duplicates + for (const hex of leapHexes) { + if (!hexes.some((h) => h.x === hex.x && h.y === hex.y)) { + hexes.push(hex); + } + } + } + + return hexes; } }