Skip to content

Battle Tanks: exclude online-only power-ups locally, continue pickups, improve fullscreen controls and result UI - #75

Merged
bashmohandes merged 2 commits into
masterfrom
codex/fix-power-ups-display-in-local-mode
Aug 16, 2026
Merged

Battle Tanks: exclude online-only power-ups locally, continue pickups, improve fullscreen controls and result UI#75
bashmohandes merged 2 commits into
masterfrom
codex/fix-power-ups-display-in-local-mode

Conversation

@bashmohandes

Copy link
Copy Markdown
Owner

Motivation

  • Players reported online-only power-ups appearing in local mode and local matches stopping power-up spawns after a while.
  • On iPhone full screen in landscape the on-screen controls were covering the arena.
  • The end-of-game winner UI was small and hard to notice and should be a prominent card with Rematch and Share actions.

Description

  • Add a local pickup whitelist and prevent onlineOnly items from spawning when state.onlineMode is false by introducing LOCAL_PICKUP_IDS and guarding spawnPickup for requested/auto picks. (battle-tanks/scripts/game.js).
  • Remove the artificial 18-turn cutoff so pickups continue to spawn every SPAWN_EVERY_TURNS for long matches by changing advancePickupSchedule. (battle-tanks/scripts/game.js).
  • Mark authoritative online rooms with room.game.onlineMode = true and preserve onlineMode across rematches by carrying it through resetMatch. (server/battle-tanks-rooms.js, battle-tanks/scripts/game.js).
  • Replace the tiny corner winner controls with a centered result dialog built in DOM and wired to the app, and update app.js to populate and toggle the result card content and visibility. (battle-tanks/index.html, battle-tanks/scripts/app.js).
  • Add styles and responsive rules for the animated result card and reserve a control strip for very short landscape viewports so full-screen controls do not cover tanks on iPhones. (battle-tanks/styles.css).
  • Add regression tests for local pickup filtering, continued pickup scheduling in long matches, and preservation of onlineMode on rematch. (tests/battle-tanks.test.js, tests/battle-tanks-rooms.test.js).

Testing

  • Ran npm run check and it completed successfully with no syntax errors reported.
  • Ran the full test suite with npm test, all automated tests passed (166 tests reported passing).
  • Ran git diff --check to ensure there are no whitespace or diff-check problems and it succeeded.

Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 145bd0aba1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread battle-tanks/styles.css
:root{--power-card-bg:#fffdf4;--power-card-ink:#172b24;--power-card-accent:#d76b45}.power-card-layer{position:absolute;z-index:8;inset:0;display:grid;place-items:center;padding:clamp(10px,4vw,36px);background:rgba(255,245,190,.18);pointer-events:auto;animation:power-backdrop .45s ease-out}.power-card-layer[hidden]{display:none}.power-card{width:min(88%,340px);max-height:88%;overflow:auto;padding:clamp(16px,3vw,26px);border:3px solid var(--power-card-accent);border-radius:18px;color:var(--power-card-ink);background:var(--power-card-bg);box-shadow:0 18px 55px rgba(0,0,0,.5);text-align:center;animation:power-card-play 3.6s both}.power-card[data-theme="stealth"]{--power-card-accent:#6653a5}.power-card[data-theme="defence"]{--power-card-accent:#247c91}.power-card-icon{display:block;font-size:clamp(32px,7vw,58px)}.power-card-player,.power-card-meta{font-weight:800;text-transform:uppercase;letter-spacing:.07em}.power-card h2{margin:.2em 0}.power-card-values{display:flex;justify-content:center;gap:18px}.power-card-values div{display:grid}.power-card-values dt{font-size:.72rem;text-transform:uppercase}.power-card-values dd{margin:0;font-size:1.2rem;font-weight:900}.power-card button{min-height:44px;padding:8px 18px;border:0;border-radius:8px;color:#fff;background:var(--power-card-ink);font-weight:800}@keyframes power-backdrop{0%,100%{background:rgba(255,245,190,.08)}45%{background:rgba(255,245,190,.3)}}@keyframes power-card-play{0%{opacity:0;transform:translateY(-35%) rotate(-7deg) scale(.72)}12%,82%{opacity:1;transform:translateY(0) rotate(0) scale(1)}100%{opacity:0;transform:translateY(12%) rotate(2deg) scale(.94)}}.arena-wrap:fullscreen .power-card-layer,.arena-wrap.is-fullscreen .power-card-layer{position:fixed;inset:0}.arena-wrap:fullscreen .power-card,.arena-wrap.is-fullscreen .power-card{width:min(80vw,390px)}@media(prefers-reduced-motion:reduce){.power-card-layer{animation:none}.power-card{animation:power-card-fade .12s linear both}@keyframes power-card-fade{from{opacity:0}to{opacity:1}}}

/* The result owns the visual focus without obscuring gameplay while a match is active. */
.result-layer{position:absolute;z-index:9;inset:0;display:grid;place-items:center;padding:clamp(12px,4vw,38px);background:rgba(10,24,20,.62);backdrop-filter:blur(5px);animation:result-backdrop .45s ease-out}.result-layer[hidden]{display:none}.result-card{position:relative;width:min(86%,460px);overflow:hidden;padding:clamp(22px,5vw,42px);border:2px solid rgba(255,255,255,.8);border-radius:24px;color:#172b24;background:linear-gradient(145deg,#fffdf4,#ffe49a);box-shadow:0 24px 80px rgba(0,0,0,.55);text-align:center;animation:result-arrive .7s cubic-bezier(.2,1.35,.35,1) both}.result-card::before,.result-card::after{position:absolute;content:'✦';color:#d76b45;font-size:34px;animation:result-spark 1.6s ease-in-out infinite}.result-card::before{top:18px;left:22px}.result-card::after{right:24px;bottom:20px;animation-delay:.4s}.result-burst{display:block;color:#d76b45;font-size:clamp(42px,9vw,72px);line-height:1;text-shadow:0 5px 0 rgba(128,61,35,.15)}.result-kicker{margin:10px 0 3px;font-size:.75rem;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.result-card h2{margin:.1em 0;font-size:clamp(2rem,7vw,4rem);line-height:1}.result-card p:not(.result-kicker){margin:12px auto 22px;font-weight:700}.result-actions{display:flex;justify-content:center;gap:10px}.result-actions button{min-width:130px;min-height:48px}.arena-wrap:fullscreen .result-layer,.arena-wrap.is-fullscreen .result-layer{position:fixed;inset:0}@keyframes result-backdrop{from{opacity:0}to{opacity:1}}@keyframes result-arrive{from{opacity:0;transform:translateY(45px) rotate(-3deg) scale(.72)}to{opacity:1;transform:none}}@keyframes result-spark{50%{transform:rotate(40deg) scale(1.35);opacity:.55}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow the result card to scroll in short arenas

On narrow non-fullscreen phones, the 16:9 arena can be only about 200px tall, while this card's icon, text, padding, and 48px actions require substantially more height. Because the parent arena has overflow:hidden, this layer does not scroll, and the card itself also uses overflow:hidden, the Rematch and Share buttons are clipped and inaccessible; the previous rematch button outside the arena was removed in this change. Add a short-height layout or vertical scrolling so the sole rematch control remains reachable.

Useful? React with 👍 / 👎.

function syncEffectsHud(){const shields=[0,1].map(index=>{const shield=(state.activeEffects?.[index]||[]).find(effect=>effect.effect==='absorb');return shield?`Player ${index+1} shield: ${shield.remainingTurns??0} turns, ${shield.remainingCapacity??0} capacity`:'';}).filter(Boolean);status.textContent=[state.opponentConcealed?'Opponent concealed.':state.announcement,...shields].filter(Boolean).join(' ');}
function announceStateChanges(){const messages=[];if(state.lastImpact?.serial>announcedImpactSerial){announcedImpactSerial=state.lastImpact.serial;const absorbed=(state.lastImpact.affected||[]).reduce((sum,item)=>sum+(Number(item.absorbedDamage)||0),0);if(absorbed)messages.push(`Shields absorbed ${absorbed} damage.`);}if(state.announcement&&state.announcement!==previousAnnouncement)messages.push(state.announcement);[0,1].forEach(index=>{const health=tankHealth(index);if(health>previousHealth[index])messages.push(`Player ${index+1} healed ${health-previousHealth[index]} health.`);const oldShield=previousEffects[index].find(effect=>effect.effect==='absorb'),shield=(state.activeEffects?.[index]||[]).find(effect=>effect.effect==='absorb');if(oldShield&&shield&&shield.remainingCapacity<oldShield.remainingCapacity)messages.push(`Player ${index+1} shield absorbed ${oldShield.remainingCapacity-shield.remainingCapacity} damage.`);previousEffects[index].forEach(effect=>{if(!(state.activeEffects?.[index]||[]).some(current=>current.id===effect.id))messages.push(`${POWER_UP_CATALOG[effect.id]?.label||'Effect'} expired for Player ${index+1}.`);});const weapon=state.equippedWeapons?.[index]||'shell';if(weapon!==previousWeapons[index])messages.push(`Player ${index+1} selected ${WEAPON_REGISTRY[weapon]?.label||'Standard shell'}.`);previousHealth[index]=health;previousEffects[index]=(state.activeEffects?.[index]||[]).map(effect=>({...effect}));previousWeapons[index]=weapon;});previousAnnouncement=state.announcement||'';if(messages.length)document.querySelector('#pickup-announcement').textContent=messages.join(' ');}
function syncControls(){const locked=state.phase!=='aiming'||(mode==='online'&&(mySide!==state.activePlayer||state.paused));document.querySelectorAll('[data-action],[data-move-direction]').forEach(button=>button.disabled=locked);fire.disabled=locked;fullscreenFire.disabled=locked;syncInventory(locked);rematch.hidden=state.phase!=='game-over';shareButton.hidden=state.phase!=='game-over';}
function syncControls(){const locked=state.phase!=='aiming'||(mode==='online'&&(mySide!==state.activePlayer||state.paused));document.querySelectorAll('[data-action],[data-move-direction]').forEach(button=>button.disabled=locked);fire.disabled=locked;fullscreenFire.disabled=locked;syncInventory(locked);const gameOver=state.phase==='game-over';resultLayer.hidden=!gameOver;if(gameOver){document.querySelector('#result-title').textContent=state.draw?'Draw!':`Player ${state.winner+1} wins!`;document.querySelector('#result-summary').textContent=state.draw?`Both tanks fell after ${state.shots} shots.`:`Victory secured in ${state.shots} shots with ${state.hits} hits.`;}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move keyboard focus into the result dialog

When a keyboard user ends the match from the Fire button, unhiding this aria-modal dialog leaves focus on the now-disabled Fire control outside it, and forward tab order proceeds to other outside controls because the result card occurs earlier in the DOM. Screen-reader users are therefore not reliably notified that the modal opened, and keyboard navigation does not enter its Rematch/Share actions as expected. Detect the game-over transition, focus the rematch button, and keep focus within the modal while it is displayed.

Useful? React with 👍 / 👎.

@bashmohandes
bashmohandes merged commit 6d0d83e into master Aug 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant