Battle Tanks: exclude online-only power-ups locally, continue pickups, improve fullscreen controls and result UI - #75
Conversation
There was a problem hiding this comment.
💡 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".
| :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}} |
There was a problem hiding this comment.
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.`;}} |
There was a problem hiding this comment.
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 👍 / 👎.
Motivation
Description
onlineOnlyitems from spawning whenstate.onlineModeis false by introducingLOCAL_PICKUP_IDSand guardingspawnPickupfor requested/auto picks. (battle-tanks/scripts/game.js).SPAWN_EVERY_TURNSfor long matches by changingadvancePickupSchedule. (battle-tanks/scripts/game.js).room.game.onlineMode = trueand preserveonlineModeacross rematches by carrying it throughresetMatch. (server/battle-tanks-rooms.js,battle-tanks/scripts/game.js).app.jsto populate and toggle the result card content and visibility. (battle-tanks/index.html,battle-tanks/scripts/app.js).battle-tanks/styles.css).onlineModeon rematch. (tests/battle-tanks.test.js,tests/battle-tanks-rooms.test.js).Testing
npm run checkand it completed successfully with no syntax errors reported.npm test, all automated tests passed (166 tests reported passing).git diff --checkto ensure there are no whitespace or diff-check problems and it succeeded.Codex Task