diff --git a/src/components/frontend/qualification/AimTrainerCanvas.ts b/src/components/frontend/qualification/AimTrainerCanvas.ts index a406f2e..fe4ef0b 100644 --- a/src/components/frontend/qualification/AimTrainerCanvas.ts +++ b/src/components/frontend/qualification/AimTrainerCanvas.ts @@ -37,6 +37,7 @@ export type AimTrainerGameState = { startedAt: number; headshots: number; torsoHits: number; + civilianHits: number; shotsFired: number; despawnedUnhitTargets: number; feedback: AimTrainerPointFeedback[]; @@ -51,6 +52,7 @@ export type AimTrainerLiveStats = { readonly shotsFired: number; readonly headshots: number; readonly torsoHits: number; + readonly civilianHits: number; readonly despawnedUnhitTargets: number; }; @@ -61,6 +63,7 @@ export function createAimTrainerGameState(): AimTrainerGameState { startedAt: 0, headshots: 0, torsoHits: 0, + civilianHits: 0, shotsFired: 0, despawnedUnhitTargets: 0, feedback: [], @@ -77,6 +80,7 @@ export function emptyAimTrainerLiveStats(): AimTrainerLiveStats { shotsFired: 0, headshots: 0, torsoHits: 0, + civilianHits: 0, despawnedUnhitTargets: 0, }; } @@ -89,11 +93,13 @@ export function getAimTrainerLiveStats(game: AimTrainerGameState): AimTrainerLiv game.torsoHits, game.shotsFired, game.despawnedUnhitTargets, + game.civilianHits, ), accuracy: calculateAimTrainerAccuracy(hits, game.shotsFired, game.despawnedUnhitTargets), shotsFired: game.shotsFired, headshots: game.headshots, torsoHits: game.torsoHits, + civilianHits: game.civilianHits, despawnedUnhitTargets: game.despawnedUnhitTargets, }; } @@ -141,12 +147,37 @@ function drawRange(ctx: CanvasRenderingContext2D): void { ctx.fillText("QUALIFICATION COURSE", AIM_TRAINER_CANVAS_WIDTH - 18, 16); } +const STANDARD_TARGET_PALETTE = { + torsoStops: ["#9b956d", "#706f52", "#4d5542"] as const, + lowerShade: "rgba(29, 38, 30, 0.42)", + straps: "rgba(217, 207, 151, 0.32)", + torsoOutline: "rgba(226, 217, 166, 0.6)", + headFill: "#afa678", + headShade: "rgba(48, 55, 40, 0.48)", + headOutline: "rgba(226, 217, 166, 0.72)", + crosshair: "rgba(235, 226, 175, 0.32)", + boundsBox: "rgba(211, 202, 149, 0.12)", +}; + +const CIVILIAN_TARGET_PALETTE = { + torsoStops: ["#8fb4dd", "#5b87b8", "#3a6191"] as const, + lowerShade: "rgba(16, 32, 52, 0.42)", + straps: "rgba(235, 245, 255, 0.35)", + torsoOutline: "rgba(190, 215, 240, 0.6)", + headFill: "#b7d0ec", + headShade: "rgba(30, 58, 92, 0.48)", + headOutline: "rgba(190, 215, 240, 0.72)", + crosshair: "rgba(215, 235, 255, 0.35)", + boundsBox: "rgba(167, 197, 232, 0.14)", +}; + function drawTarget( ctx: CanvasRenderingContext2D, activeTarget: AimTrainerActiveTarget, frame: AimTrainerSceneFrame, ): void { const { target, spawnedAt } = activeTarget; + const palette = target.kind === "civilian" ? CIVILIAN_TARGET_PALETTE : STANDARD_TARGET_PALETTE; const age = Math.max(0, frame.now - spawnedAt); const fadeDuration = Math.min( AIM_TRAINER_TARGET_FADE_DURATION_MS, @@ -173,35 +204,35 @@ function drawTarget( ctx.shadowColor = "transparent"; const torso = ctx.createLinearGradient(-target.torsoW / 2, 0, target.torsoW / 2, target.torsoH); - torso.addColorStop(0, "#9b956d"); - torso.addColorStop(0.5, "#706f52"); - torso.addColorStop(1, "#4d5542"); + torso.addColorStop(0, palette.torsoStops[0]); + torso.addColorStop(0.5, palette.torsoStops[1]); + torso.addColorStop(1, palette.torsoStops[2]); ctx.fillStyle = torso; ctx.fillRect(-target.torsoW / 2, 0, target.torsoW, target.torsoH); - ctx.fillStyle = "rgba(29, 38, 30, 0.42)"; + ctx.fillStyle = palette.lowerShade; ctx.fillRect(-target.torsoW / 2, target.torsoH * 0.54, target.torsoW, target.torsoH * 0.46); - ctx.fillStyle = "rgba(217, 207, 151, 0.32)"; + ctx.fillStyle = palette.straps; ctx.fillRect(-target.torsoW * 0.28, target.torsoH * 0.16, target.torsoW * 0.56, 3); ctx.fillRect(-target.torsoW * 0.28, target.torsoH * 0.42, target.torsoW * 0.56, 3); - ctx.strokeStyle = "rgba(226, 217, 166, 0.6)"; + ctx.strokeStyle = palette.torsoOutline; ctx.lineWidth = 1.5; ctx.strokeRect(-target.torsoW / 2, 0, target.torsoW, target.torsoH); - ctx.fillStyle = "#afa678"; + ctx.fillStyle = palette.headFill; ctx.beginPath(); ctx.arc(0, -target.headRadius, target.headRadius, 0, Math.PI * 2); ctx.fill(); - ctx.fillStyle = "rgba(48, 55, 40, 0.48)"; + ctx.fillStyle = palette.headShade; ctx.beginPath(); ctx.arc(0, -target.headRadius - 2, target.headRadius * 0.72, Math.PI, Math.PI * 2); ctx.fill(); - ctx.strokeStyle = "rgba(226, 217, 166, 0.72)"; + ctx.strokeStyle = palette.headOutline; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, -target.headRadius, target.headRadius, 0, Math.PI * 2); ctx.stroke(); - ctx.strokeStyle = "rgba(235, 226, 175, 0.32)"; + ctx.strokeStyle = palette.crosshair; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, -target.headRadius - target.headRadius - 9); @@ -213,7 +244,7 @@ function drawTarget( ctx.stroke(); ctx.restore(); - ctx.strokeStyle = "rgba(211, 202, 149, 0.12)"; + ctx.strokeStyle = palette.boundsBox; ctx.setLineDash([4, 5]); ctx.strokeRect(torsoX, target.y, target.torsoW * pulse, target.torsoH); ctx.setLineDash([]);