1
0
Fork 0

feat(qualification): render civilian targets

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-04 12:04:44 -04:00
parent dc9d2f4605
commit 6c7a9fb5e2

View file

@ -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([]);