Repository navigation
Add Battle Tanks power‑up achievements and authoritative per‑player statistics - #69
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2d0879205a
ℹ️ 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".
| canvas.addEventListener('pointerdown',event=>{if(mode==='online'||state.phase!=='aiming')return;dragging=true;canvas.setPointerCapture(event.pointerId);});canvas.addEventListener('pointermove',event=>{if(!dragging||state.phase!=='aiming')return;const rect=canvas.getBoundingClientRect(),logicalX=(event.clientX-rect.left)*WIDTH/rect.width,tank=state.tanks[state.activePlayer],bounds=tankBounds(state,state.activePlayer);tank.x=Math.max(bounds.min,Math.min(bounds.max,logicalX-TANK_W/2));tank.y=tankYAt(state,tank);collectPickup(state,state.activePlayer);sync();render();});canvas.addEventListener('pointerup',()=>dragging=false);canvas.addEventListener('pointercancel',()=>dragging=false); | ||
| function optionalResultStatistics(statistics,side){if(!statistics)return {};const weapons=statistics.weapons?.[side],splashDamage=statistics.splashDamage?.[side],healing=statistics.healing?.[side],powerUps=statistics.powerUps?.[side];return { ...(weapons&&typeof weapons==='object'?{weapons}:{ }), ...(Number.isSafeInteger(splashDamage)?{splashDamage}:{ }), ...(Number.isSafeInteger(healing)?{healing}:{ }), ...(Number.isSafeInteger(powerUps)?{powerUps}:{ }) };} | ||
| async function submitResult(){if(state.resultSubmitted||state.phase!=='game-over'||state.draw||!window.Arcade?.user())return;state.resultSubmitted=true;const damageTaken=state.damageTaken?.[0]||0,details={mode:'local',winner:state.winner+1,turns:state.shots,shots:state.shots,hits:state.hits,seconds:Math.max(1,Math.min(7200,Math.round((Date.now()-state.startedAt)/1000))),damageTaken,...optionalResultStatistics(state.statistics,0)};try{await window.Arcade.record({game:'battletanks',won:state.winner===0,details});}catch{/* Saving an arcade result must never block a local rematch. */}} | ||
| function optionalResultStatistics(statistics,side){if(!statistics)return {};const result={},fields=['powerUpsAcquired','powerUpsUsed','shieldDamageAbsorbed','healthRestored','invisibilityActivations','laserRicochetHits','laserSelfDamage','homingHits','heavyProjectileMaxDamage','poweredHits'],weapons=statistics.weapons?.[side];if(weapons&&typeof weapons==='object')result.weapons=weapons;for(const name of fields)if(Number.isSafeInteger(statistics[name]?.[side]))result[name]=statistics[name][side];const types=statistics.powerUpTypesUsed?.[side];result.powerUpTypesUsed=Array.isArray(types)?[...new Set(types.filter(id=>POWER_UP_CATALOG[id]))]:[];return result;} |
There was a problem hiding this comment.
Serialize Set-based power-up types for local results
In local matches, state.statistics.powerUpTypesUsed[side] is a Set, but this helper accepts only arrays and therefore always submits powerUpTypesUsed: []. A signed-in player who legitimately uses three distinct types locally can never unlock the new “Full Deck” achievement; handle the core Set here as well as the array form received in snapshots.
Useful? React with 👍 / 👎.
| if (powerUpsUsed > powerUpsAcquired || powerUpTypesUsed.length > powerUpsUsed || counters.invisibilityActivations > powerUpsUsed || counters.healthRestored > powerUpsUsed * 35 || counters.shieldDamageAbsorbed > powerUpsUsed * 60 || counters.invisibilityActivations > powerUpTypesUsed.includes('invisibility') * powerUpsUsed || (details.mode === 'local' && counters.invisibilityActivations !== 0)) throw new Error('Invalid Battle Tanks power-up statistics.'); | ||
| if (counters.laserRicochetHits > (optional.weapons?.laser || 0) || counters.homingHits > (optional.weapons?.homing || 0) || counters.heavyProjectileMaxDamage > 0 && !(optional.weapons?.['heavy-shell'] > 0)) throw new Error('Invalid Battle Tanks weapon statistics.'); |
There was a problem hiding this comment.
Require power-up use before accepting powered hits
For an untrusted local submission, poweredHits is constrained only by shots, so a payload with powerUpsAcquired: 0, powerUpsUsed: 0, an empty type list, and poweredHits: 2 is accepted and unlocks “Power Player” on a win even though that state is impossible in the game. Add a cross-field invariant tying positive powered hits to at least one used power-up or applicable powered weapon/effect.
Useful? React with 👍 / 👎.
Motivation
Description
lengthGte,includes,gt) so array membership and compound boundaries can be expressed declaratively inserver/achievements.js.battle-tanks/scripts/game.js, includingpowerUpsAcquired,powerUpsUsed,powerUpTypesUsed(internal Set, serialized bounded array),shieldDamageAbsorbed,healthRestored,invisibilityActivations,laserRicochetHits,laserSelfDamage,homingHits,heavyProjectileMaxDamage, andpoweredHits; these counters are updated only by pickup/activation, healing, shield absorption, weapon resolution and reset on match/rematch.snapshotto be used by server rooms and result normalization.submitResultinbattle-tanks/scripts/app.jswhile forcinginvisibilityActivationsto0for local results to prevent local spoofing.server/accounts.js, including known ID validation, deduplication, bounded lengths, numeric range checks, and cross‑field invariants (for examplepowerUpsUsed <= powerUpsAcquired,powerUpTypesUsed.length <= powerUpsUsed, and localinvisibilityActivations === 0), and preserved the trusted online path for authoritative results.server/battle-tanks-rooms.js: extendedroom.statsto track per‑side power‑up aggregates, introducedsyncPowerStatisticsto fillroom.statsfromroom.game.statistics, and included these fields whenfinishcallsrecordResultso only server‑derived values are persisted for online matches.tests/achievements.test.js,tests/accounts.test.js,tests/battle-tanks.test.js, andtests/battle-tanks-rooms.test.js.Testing
npm test(full test suite); all automated tests passed (155tests passing).npm run check(static checks); it completed successfully.tests/achievements.test.js,tests/accounts.test.js,tests/battle-tanks.test.js, andtests/battle-tanks-rooms.test.js, and they were executed as part of the test runs and validated the new achievement boundaries, server validation, authoritative room statistics, and reset/rematch behavior.Codex Task