diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx
index 53cfcf2731..688b5b8b4b 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/MiniGame.tsx
@@ -2,20 +2,78 @@
import { useMiniGame } from "./useMiniGame";
+function Key({ children }: { children: React.ReactNode }) {
+ return
;
+}
+
export function MiniGame() {
- const { canvasRef } = useMiniGame();
+ const {
+ canvasRef,
+ activeMode,
+ showOverlay,
+ score,
+ highScore,
+ onContinue,
+ } = useMiniGame();
+
+ const isRunActive =
+ activeMode === "run" || activeMode === "idle" || activeMode === "over";
+ const isBossActive =
+ activeMode === "boss" ||
+ activeMode === "boss-intro" ||
+ activeMode === "boss-defeated";
+
+ let overlayText: string | undefined;
+ let buttonLabel = "Continue";
+ if (activeMode === "idle") {
+ buttonLabel = "Start";
+ } else if (activeMode === "boss-intro") {
+ overlayText = "Face the bandit!";
+ } else if (activeMode === "boss-defeated") {
+ overlayText = "Great job, keep on going";
+ } else if (activeMode === "over") {
+ overlayText = `Score: ${score} / Record: ${highScore}`;
+ buttonLabel = "Retry";
+ }
return (
-
-
+
+
+ {isBossActive ? (
+ <>
+ Duel mode: ←→ to move · Z to attack ·{" "}
+ X to block · Space to jump
+ >
+ ) : (
+ <>
+ Run mode: Space to jump
+ >
+ )}
+
+
+
+ {showOverlay && (
+
+ {overlayText && (
+
{overlayText}
+ )}
+
+ {buttonLabel}
+
+
+ )}
+
);
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png
new file mode 100644
index 0000000000..af199cfcb9
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-attack.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png
new file mode 100644
index 0000000000..169ccb7d98
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-idle.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png
new file mode 100644
index 0000000000..9119dcb778
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/archer-shoot.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png
new file mode 100644
index 0000000000..c5259f423b
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/attack.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png
new file mode 100644
index 0000000000..064c170add
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/guard.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png
new file mode 100644
index 0000000000..b8ebdc7294
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/idle.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png
new file mode 100644
index 0000000000..a6ba2f3452
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/run.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png
new file mode 100644
index 0000000000..befa6f253e
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/sparkles.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png
new file mode 100644
index 0000000000..655a141adf
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-1.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png
new file mode 100644
index 0000000000..fe6d67bafd
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-2.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png
new file mode 100644
index 0000000000..162140b90d
Binary files /dev/null and b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/assets/tree-3.png differ
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts
index e91f1766ca..5cc53a8da2 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/components/MiniGame/useMiniGame.ts
@@ -1,4 +1,13 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
+import runSheet from "./assets/run.png";
+import idleSheet from "./assets/idle.png";
+import attackSheet from "./assets/attack.png";
+import tree1Sheet from "./assets/tree-1.png";
+import tree2Sheet from "./assets/tree-2.png";
+import tree3Sheet from "./assets/tree-3.png";
+import archerIdleSheet from "./assets/archer-idle.png";
+import archerAttackSheet from "./assets/archer-attack.png";
+import guardSheet from "./assets/guard.png";
/* ------------------------------------------------------------------ */
/* Constants */
@@ -12,24 +21,64 @@ const SPEED_INCREMENT = 0.0008;
const SPAWN_MIN = 70;
const SPAWN_MAX = 130;
const CHAR_SIZE = 18;
+const CHAR_SPRITE_SIZE = 67;
const CHAR_X = 50;
const GROUND_PAD = 20;
const STORAGE_KEY = "copilot-minigame-highscore";
+// Character sprite sheets (each frame is 192x192)
+const SPRITE_FRAME_SIZE = 192;
+const RUN_FRAMES = 6;
+const IDLE_FRAMES = 8;
+const ATTACK_FRAMES = 4;
+const ANIM_SPEED = 8;
+const ATTACK_ANIM_SPEED = 6;
+const ATTACK_RANGE = 40;
+const ATTACK_HIT_FRAME = 2;
+const GUARD_FRAMES = 6;
+const GUARD_ANIM_SPEED = 8;
+
+// Tree sprite sheets: 8 frames each, 192px wide per frame
+const TREE_FRAMES = 8;
+const TREE_ANIM_SPEED = 10;
+const TREE_CONFIGS = [
+ { frameW: 192, frameH: 256, renderW: 40, renderH: 61, hitW: 16, hitH: 50 },
+ { frameW: 192, frameH: 192, renderW: 38, renderH: 52, hitW: 16, hitH: 40 },
+ { frameW: 192, frameH: 192, renderW: 32, renderH: 40, hitW: 14, hitH: 30 },
+] as const;
+
// Colors
const COLOR_BG = "#E8EAF6";
const COLOR_CHAR = "#263238";
-const COLOR_BOSS = "#F50057";
// Boss
const BOSS_SIZE = 36;
+const BOSS_SPRITE_SIZE = 70;
const BOSS_ENTER_SPEED = 2;
const BOSS_LEAVE_SPEED = 3;
-const BOSS_SHOOT_COOLDOWN = 90;
-const BOSS_SHOTS_TO_EVADE = 5;
-const BOSS_INTERVAL = 20; // every N score
-const PROJ_SPEED = 4.5;
-const PROJ_SIZE = 12;
+const BOSS_HP = 1;
+const MOVE_SPEED = 3;
+const BOSS_CHASE_SPEED = 2.2;
+const BOSS_RETREAT_SPEED = 2;
+const BOSS_ATTACK_RANGE = 50;
+const BOSS_IDLE_TIME = 166;
+const BOSS_RETREAT_TIME = 166;
+
+// Archer sprite sheets
+const ARCHER_IDLE_FRAMES = 6;
+const ARCHER_ATTACK_FRAMES = 4;
+const ARCHER_FRAME_SIZE = 192;
+const ARCHER_ANIM_SPEED = 8;
+const ARCHER_ATTACK_ANIM_SPEED = 6;
+const ARCHER_ATTACK_HIT_FRAME = 2;
+
+// Death animation
+const DEATH_PARTICLE_COUNT = 15;
+const DEATH_ANIM_DURATION = 40;
+
+// Attack effect
+const ATTACK_EFFECT_COUNT = 8;
+const ATTACK_EFFECT_DURATION = 15;
/* ------------------------------------------------------------------ */
/* Types */
@@ -40,27 +89,38 @@ interface Obstacle {
width: number;
height: number;
scored: boolean;
-}
-
-interface Projectile {
- x: number;
- y: number;
- speed: number;
- evaded: boolean;
- type: "low" | "high";
+ treeType: 0 | 1 | 2;
}
interface BossState {
phase: "inactive" | "entering" | "fighting" | "leaving";
x: number;
+ y: number;
+ vy: number;
targetX: number;
- shotsEvaded: number;
- cooldown: number;
- projectiles: Projectile[];
- bob: number;
+ hp: number;
+ action: "idle" | "chase" | "retreat" | "attack";
+ actionTimer: number;
+ attackFrame: number;
+ attackHit: boolean;
+}
+
+interface Particle {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+ life: number;
+}
+
+interface DeathAnim {
+ particles: Particle[];
+ type: "boss" | "player";
+ timer: number;
}
interface GameState {
+ charX: number;
charY: number;
vy: number;
obstacles: Obstacle[];
@@ -74,6 +134,31 @@ interface GameState {
groundY: number;
boss: BossState;
bossThreshold: number;
+ bossesDefeated: number;
+ paused: boolean;
+ nextTreeType: 0 | 1 | 2;
+ attacking: boolean;
+ attackFrame: number;
+ attackHit: boolean;
+ guarding: boolean;
+ guardFrame: number;
+ deathAnim: DeathAnim | null;
+ attackEffects: Particle[];
+}
+
+interface KeyState {
+ left: boolean;
+ right: boolean;
+}
+
+interface Sprites {
+ run: HTMLImageElement;
+ idle: HTMLImageElement;
+ attack: HTMLImageElement;
+ guard: HTMLImageElement;
+ trees: HTMLImageElement[];
+ archerIdle: HTMLImageElement;
+ archerAttack: HTMLImageElement;
}
/* ------------------------------------------------------------------ */
@@ -100,20 +185,24 @@ function writeHighScore(score: number) {
}
}
-function makeBoss(): BossState {
+function makeBoss(groundY: number): BossState {
return {
phase: "inactive",
x: 0,
+ y: groundY - BOSS_SIZE,
+ vy: 0,
targetX: 0,
- shotsEvaded: 0,
- cooldown: 0,
- projectiles: [],
- bob: 0,
+ hp: BOSS_HP,
+ action: "idle",
+ actionTimer: BOSS_IDLE_TIME,
+ attackFrame: 0,
+ attackHit: false,
};
}
function makeState(groundY: number): GameState {
return {
+ charX: CHAR_X,
charY: groundY - CHAR_SIZE,
vy: 0,
obstacles: [],
@@ -125,62 +214,110 @@ function makeState(groundY: number): GameState {
running: false,
over: false,
groundY,
- boss: makeBoss(),
- bossThreshold: BOSS_INTERVAL,
+ boss: makeBoss(groundY),
+ bossThreshold: 10,
+ bossesDefeated: 0,
+ paused: false,
+ nextTreeType: 0,
+ attacking: false,
+ attackFrame: 0,
+ attackHit: false,
+ guarding: false,
+ guardFrame: 0,
+ deathAnim: null,
+ attackEffects: [],
};
}
-function gameOver(s: GameState) {
- s.running = false;
- s.over = true;
- if (s.score > s.highScore) {
- s.highScore = s.score;
- writeHighScore(s.score);
+function spawnParticles(x: number, y: number): Particle[] {
+ const particles: Particle[] = [];
+ for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) {
+ const angle = Math.random() * Math.PI * 2;
+ const speed = 1 + Math.random() * 3;
+ particles.push({
+ x,
+ y,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed - 2,
+ life: DEATH_ANIM_DURATION,
+ });
}
+ return particles;
}
-/* ------------------------------------------------------------------ */
-/* Projectile collision — shared between fighting & leaving phases */
-/* ------------------------------------------------------------------ */
+function startPlayerDeath(s: GameState) {
+ s.deathAnim = {
+ particles: spawnParticles(
+ s.charX + CHAR_SIZE / 2,
+ s.charY + CHAR_SIZE / 2,
+ ),
+ type: "player",
+ timer: DEATH_ANIM_DURATION,
+ };
+}
-/** Returns true if the player died. */
-function tickProjectiles(s: GameState): boolean {
- const boss = s.boss;
-
- for (const p of boss.projectiles) {
- p.x -= p.speed;
-
- if (!p.evaded && p.x + PROJ_SIZE < CHAR_X) {
- p.evaded = true;
- boss.shotsEvaded++;
- }
-
- // Collision
- if (
- !p.evaded &&
- CHAR_X + CHAR_SIZE > p.x &&
- CHAR_X < p.x + PROJ_SIZE &&
- s.charY + CHAR_SIZE > p.y &&
- s.charY < p.y + PROJ_SIZE
- ) {
- gameOver(s);
- return true;
- }
- }
-
- boss.projectiles = boss.projectiles.filter((p) => p.x + PROJ_SIZE > -20);
- return false;
+function startBossDeath(s: GameState) {
+ s.deathAnim = {
+ particles: spawnParticles(
+ s.boss.x + BOSS_SIZE / 2,
+ s.boss.y + BOSS_SIZE / 2,
+ ),
+ type: "boss",
+ timer: DEATH_ANIM_DURATION,
+ };
}
/* ------------------------------------------------------------------ */
/* Update */
/* ------------------------------------------------------------------ */
-function update(s: GameState, canvasWidth: number) {
- if (!s.running) return;
+function update(s: GameState, canvasWidth: number, keys: KeyState) {
+ if (!s.running || s.paused) return;
s.frame++;
+ // ---- Attack effects ---- //
+ for (const p of s.attackEffects) {
+ p.x += p.vx;
+ p.y += p.vy;
+ p.vy += 0.08;
+ p.life--;
+ }
+ s.attackEffects = s.attackEffects.filter((p) => p.life > 0);
+
+ // ---- Death animation ---- //
+ if (s.deathAnim) {
+ s.deathAnim.timer--;
+ for (const p of s.deathAnim.particles) {
+ p.x += p.vx;
+ p.y += p.vy;
+ p.vy += 0.1;
+ p.life--;
+ }
+ if (s.deathAnim.timer <= 0) {
+ if (s.deathAnim.type === "player") {
+ s.deathAnim = null;
+ s.running = false;
+ s.over = true;
+ if (s.score > s.highScore) {
+ s.highScore = s.score;
+ writeHighScore(s.score);
+ }
+ } else {
+ s.deathAnim = null;
+ s.score += 10;
+ s.bossesDefeated++;
+ if (s.bossesDefeated === 1) {
+ s.bossThreshold = s.score + 15;
+ } else {
+ s.bossThreshold = s.score + 20;
+ }
+ s.paused = true;
+ }
+ }
+ return;
+ }
+
// Speed only ramps during regular play
if (s.boss.phase === "inactive") {
s.speed = BASE_SPEED + s.frame * SPEED_INCREMENT;
@@ -194,86 +331,217 @@ function update(s: GameState, canvasWidth: number) {
s.vy = 0;
}
+ // ---- Attack animation ---- //
+ if (s.attacking) {
+ s.attackFrame++;
+
+ if (
+ !s.attackHit &&
+ Math.floor(s.attackFrame / ATTACK_ANIM_SPEED) === ATTACK_HIT_FRAME &&
+ s.boss.phase === "fighting" &&
+ s.charX + CHAR_SIZE + ATTACK_RANGE >= s.boss.x
+ ) {
+ s.boss.hp--;
+ s.attackHit = true;
+ }
+
+ if (s.attackFrame >= ATTACK_FRAMES * ATTACK_ANIM_SPEED) {
+ s.attacking = false;
+ s.attackFrame = 0;
+ s.attackHit = false;
+ }
+ }
+
+ // ---- Guard animation ---- //
+ if (s.guarding) {
+ s.guardFrame++;
+ if (s.guardFrame >= GUARD_FRAMES * GUARD_ANIM_SPEED) {
+ s.guardFrame = GUARD_FRAMES * GUARD_ANIM_SPEED - 1;
+ }
+ }
+
+ // ---- Horizontal movement during boss fight ---- //
+ if (s.boss.phase !== "inactive") {
+ if (keys.left) {
+ s.charX = Math.max(10, s.charX - MOVE_SPEED);
+ }
+ if (keys.right) {
+ s.charX = Math.min(canvasWidth - CHAR_SIZE - 10, s.charX + MOVE_SPEED);
+ }
+ } else {
+ s.charX = CHAR_X;
+ }
+
// ---- Trigger boss ---- //
- if (s.boss.phase === "inactive" && s.score >= s.bossThreshold) {
+ const isOnGround = s.charY + CHAR_SIZE >= s.groundY;
+ if (
+ s.boss.phase === "inactive" &&
+ s.score >= s.bossThreshold &&
+ s.obstacles.length === 0 &&
+ isOnGround
+ ) {
s.boss.phase = "entering";
s.boss.x = canvasWidth + 10;
+ s.boss.y = s.groundY - BOSS_SIZE;
+ s.boss.vy = 0;
s.boss.targetX = canvasWidth - BOSS_SIZE - 40;
- s.boss.shotsEvaded = 0;
- s.boss.cooldown = BOSS_SHOOT_COOLDOWN;
- s.boss.projectiles = [];
- s.obstacles = [];
+ s.boss.hp = BOSS_HP;
+ s.boss.action = "idle";
+ s.boss.actionTimer = BOSS_IDLE_TIME;
+ s.boss.attackFrame = 0;
+ s.boss.attackHit = false;
+
+ if (s.bossesDefeated === 0) {
+ s.paused = true;
+ }
}
// ---- Boss: entering ---- //
if (s.boss.phase === "entering") {
- s.boss.bob = Math.sin(s.frame * 0.05) * 3;
s.boss.x -= BOSS_ENTER_SPEED;
if (s.boss.x <= s.boss.targetX) {
s.boss.x = s.boss.targetX;
s.boss.phase = "fighting";
}
- return; // no obstacles while entering
+ return;
}
// ---- Boss: fighting ---- //
if (s.boss.phase === "fighting") {
- s.boss.bob = Math.sin(s.frame * 0.05) * 3;
-
- // Shoot
- s.boss.cooldown--;
- if (s.boss.cooldown <= 0) {
- const isLow = Math.random() < 0.5;
- s.boss.projectiles.push({
- x: s.boss.x - PROJ_SIZE,
- y: isLow ? s.groundY - 14 : s.groundY - 70,
- speed: PROJ_SPEED,
- evaded: false,
- type: isLow ? "low" : "high",
- });
- s.boss.cooldown = BOSS_SHOOT_COOLDOWN;
+ // Boss physics
+ s.boss.vy += GRAVITY;
+ s.boss.y += s.boss.vy;
+ if (s.boss.y + BOSS_SIZE >= s.groundY) {
+ s.boss.y = s.groundY - BOSS_SIZE;
+ s.boss.vy = 0;
}
- if (tickProjectiles(s)) return;
-
// Boss defeated?
- if (s.boss.shotsEvaded >= BOSS_SHOTS_TO_EVADE) {
- s.boss.phase = "leaving";
- s.score += 5; // bonus
- s.bossThreshold = s.score + BOSS_INTERVAL;
+ if (s.boss.hp <= 0) {
+ startBossDeath(s);
+ return;
+ }
+
+ // Boss AI
+ if (s.boss.action === "attack") {
+ s.boss.attackFrame++;
+ const hitFrame = Math.floor(
+ s.boss.attackFrame / ARCHER_ATTACK_ANIM_SPEED,
+ );
+
+ // Spawn yellow attack effect at hit frame
+ if (
+ s.boss.attackFrame === ARCHER_ATTACK_HIT_FRAME * ARCHER_ATTACK_ANIM_SPEED
+ ) {
+ const effectX = s.boss.x - 5;
+ const effectY = s.boss.y + BOSS_SIZE / 2;
+ for (let i = 0; i < ATTACK_EFFECT_COUNT; i++) {
+ const angle = Math.PI + (Math.random() - 0.5) * 1.2;
+ const speed = 2 + Math.random() * 3;
+ s.attackEffects.push({
+ x: effectX,
+ y: effectY,
+ vx: Math.cos(angle) * speed,
+ vy: Math.sin(angle) * speed - 1,
+ life: ATTACK_EFFECT_DURATION,
+ });
+ }
+ }
+
+ if (!s.boss.attackHit && hitFrame === ARCHER_ATTACK_HIT_FRAME) {
+ const dist = s.boss.x - (s.charX + CHAR_SIZE);
+ if (dist < BOSS_ATTACK_RANGE && dist > -BOSS_SIZE) {
+ s.boss.attackHit = true;
+ if (!s.guarding) {
+ startPlayerDeath(s);
+ return;
+ }
+ }
+ }
+
+ if (
+ s.boss.attackFrame >=
+ ARCHER_ATTACK_FRAMES * ARCHER_ATTACK_ANIM_SPEED
+ ) {
+ s.boss.action = "retreat";
+ s.boss.actionTimer = BOSS_RETREAT_TIME;
+ s.boss.attackFrame = 0;
+ s.boss.attackHit = false;
+ }
+ } else {
+ s.boss.actionTimer--;
+
+ if (s.boss.action === "chase") {
+ if (s.boss.x > s.charX + CHAR_SIZE) {
+ s.boss.x -= BOSS_CHASE_SPEED;
+ } else {
+ s.boss.x += BOSS_CHASE_SPEED;
+ }
+
+ // Occasional jump
+ if (s.boss.y + BOSS_SIZE >= s.groundY && Math.random() < 0.008) {
+ s.boss.vy = JUMP_FORCE * 0.7;
+ }
+
+ // Close enough to attack
+ const dist = Math.abs(s.boss.x - (s.charX + CHAR_SIZE));
+ if (dist < BOSS_ATTACK_RANGE) {
+ s.boss.action = "attack";
+ s.boss.attackFrame = 0;
+ s.boss.attackHit = false;
+ }
+ } else if (s.boss.action === "retreat") {
+ s.boss.x += BOSS_RETREAT_SPEED;
+ if (s.boss.x > canvasWidth - BOSS_SIZE - 10) {
+ s.boss.x = canvasWidth - BOSS_SIZE - 10;
+ }
+ }
+
+ // Timer expired → next action
+ if (s.boss.actionTimer <= 0) {
+ if (s.boss.action === "idle" || s.boss.action === "retreat") {
+ s.boss.action = "chase";
+ s.boss.actionTimer = 999;
+ } else {
+ s.boss.action = "idle";
+ s.boss.actionTimer = BOSS_IDLE_TIME;
+ }
+ }
}
return;
}
// ---- Boss: leaving ---- //
if (s.boss.phase === "leaving") {
- s.boss.bob = Math.sin(s.frame * 0.05) * 3;
s.boss.x += BOSS_LEAVE_SPEED;
-
- // Still check in-flight projectiles
- if (tickProjectiles(s)) return;
-
if (s.boss.x > canvasWidth + 50) {
- s.boss = makeBoss();
+ s.boss = makeBoss(s.groundY);
+ s.charX = CHAR_X;
s.nextSpawn = s.frame + randInt(SPAWN_MIN / 2, SPAWN_MAX / 2);
}
return;
}
// ---- Regular obstacle play ---- //
- if (s.frame >= s.nextSpawn) {
+ // Stop spawning trees if enough are queued to reach boss threshold
+ const unscoredCount = s.obstacles.filter((o) => !o.scored).length;
+ if (s.score + unscoredCount < s.bossThreshold && s.frame >= s.nextSpawn) {
+ const tt = s.nextTreeType;
+ const cfg = TREE_CONFIGS[tt];
s.obstacles.push({
x: canvasWidth + 10,
- width: randInt(10, 16),
- height: randInt(20, 48),
+ width: cfg.hitW,
+ height: cfg.hitH,
scored: false,
+ treeType: tt,
});
+ s.nextTreeType = (Math.floor(Math.random() * 3)) as 0 | 1 | 2;
s.nextSpawn = s.frame + randInt(SPAWN_MIN, SPAWN_MAX);
}
for (const o of s.obstacles) {
o.x -= s.speed;
- if (!o.scored && o.x + o.width < CHAR_X) {
+ if (!o.scored && o.x + o.width < s.charX) {
o.scored = true;
s.score++;
}
@@ -284,11 +552,11 @@ function update(s: GameState, canvasWidth: number) {
for (const o of s.obstacles) {
const oY = s.groundY - o.height;
if (
- CHAR_X + CHAR_SIZE > o.x &&
- CHAR_X < o.x + o.width &&
+ s.charX + CHAR_SIZE > o.x &&
+ s.charX < o.x + o.width &&
s.charY + CHAR_SIZE > oY
) {
- gameOver(s);
+ startPlayerDeath(s);
return;
}
}
@@ -298,73 +566,82 @@ function update(s: GameState, canvasWidth: number) {
/* Drawing */
/* ------------------------------------------------------------------ */
-function drawBoss(ctx: CanvasRenderingContext2D, s: GameState, bg: string) {
- const bx = s.boss.x;
- const by = s.groundY - BOSS_SIZE + s.boss.bob;
+function drawBoss(
+ ctx: CanvasRenderingContext2D,
+ s: GameState,
+ sprites: Sprites,
+) {
+ const boss = s.boss;
+ const isAttacking = boss.action === "attack";
+ const sheet = isAttacking ? sprites.archerAttack : sprites.archerIdle;
+ const totalFrames = isAttacking ? ARCHER_ATTACK_FRAMES : ARCHER_IDLE_FRAMES;
+ const animSpeed = isAttacking ? ARCHER_ATTACK_ANIM_SPEED : ARCHER_ANIM_SPEED;
- // Body
- ctx.save();
- ctx.fillStyle = COLOR_BOSS;
- ctx.globalAlpha = 0.9;
- ctx.beginPath();
- ctx.roundRect(bx, by, BOSS_SIZE, BOSS_SIZE, 4);
- ctx.fill();
- ctx.restore();
+ let frameIndex: number;
+ if (isAttacking) {
+ frameIndex = Math.min(
+ Math.floor(boss.attackFrame / animSpeed),
+ totalFrames - 1,
+ );
+ } else {
+ frameIndex = Math.floor(s.frame / animSpeed) % totalFrames;
+ }
- // Eyes
- ctx.save();
- ctx.fillStyle = bg;
- const eyeY = by + 13;
- ctx.beginPath();
- ctx.arc(bx + 10, eyeY, 4, 0, Math.PI * 2);
- ctx.fill();
- ctx.beginPath();
- ctx.arc(bx + 26, eyeY, 4, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
+ const srcX = frameIndex * ARCHER_FRAME_SIZE;
+ const spriteDrawX = boss.x + (BOSS_SIZE - BOSS_SPRITE_SIZE) / 2;
+ const spriteDrawY = boss.y + BOSS_SIZE - BOSS_SPRITE_SIZE + 12;
- // Angry eyebrows
- ctx.save();
- ctx.strokeStyle = bg;
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(bx + 5, eyeY - 7);
- ctx.lineTo(bx + 14, eyeY - 4);
- ctx.stroke();
- ctx.beginPath();
- ctx.moveTo(bx + 31, eyeY - 7);
- ctx.lineTo(bx + 22, eyeY - 4);
- ctx.stroke();
- ctx.restore();
+ if (sheet.complete && sheet.naturalWidth > 0) {
+ ctx.drawImage(
+ sheet,
+ srcX,
+ 0,
+ ARCHER_FRAME_SIZE,
+ ARCHER_FRAME_SIZE,
+ spriteDrawX,
+ spriteDrawY,
+ BOSS_SPRITE_SIZE,
+ BOSS_SPRITE_SIZE,
+ );
+ } else {
+ ctx.save();
+ ctx.fillStyle = "#F50057";
+ ctx.globalAlpha = 0.9;
+ ctx.beginPath();
+ ctx.roundRect(boss.x, boss.y, BOSS_SIZE, BOSS_SIZE, 4);
+ ctx.fill();
+ ctx.restore();
+ }
+}
- // Zigzag mouth
+function drawParticles(ctx: CanvasRenderingContext2D, anim: DeathAnim) {
ctx.save();
- ctx.strokeStyle = bg;
- ctx.lineWidth = 1.5;
- ctx.beginPath();
- ctx.moveTo(bx + 10, by + 27);
- ctx.lineTo(bx + 14, by + 24);
- ctx.lineTo(bx + 18, by + 27);
- ctx.lineTo(bx + 22, by + 24);
- ctx.lineTo(bx + 26, by + 27);
- ctx.stroke();
+ for (const p of anim.particles) {
+ if (p.life <= 0) continue;
+ const alpha = p.life / DEATH_ANIM_DURATION;
+ const size = 2 + alpha * 3;
+ ctx.globalAlpha = alpha;
+ ctx.fillStyle = "#a855f7";
+ ctx.beginPath();
+ ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
+ ctx.fill();
+ }
ctx.restore();
}
-function drawProjectiles(ctx: CanvasRenderingContext2D, boss: BossState) {
+function drawAttackEffects(
+ ctx: CanvasRenderingContext2D,
+ effects: Particle[],
+) {
ctx.save();
- ctx.fillStyle = COLOR_BOSS;
- ctx.globalAlpha = 0.8;
- for (const p of boss.projectiles) {
- if (p.evaded) continue;
+ for (const p of effects) {
+ if (p.life <= 0) continue;
+ const alpha = p.life / ATTACK_EFFECT_DURATION;
+ const size = 1.5 + alpha * 2.5;
+ ctx.globalAlpha = alpha;
+ ctx.fillStyle = "#facc15";
ctx.beginPath();
- ctx.arc(
- p.x + PROJ_SIZE / 2,
- p.y + PROJ_SIZE / 2,
- PROJ_SIZE / 2,
- 0,
- Math.PI * 2,
- );
+ ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
@@ -376,7 +653,7 @@ function draw(
w: number,
h: number,
fg: string,
- started: boolean,
+ sprites: Sprites,
) {
ctx.fillStyle = COLOR_BG;
ctx.fillRect(0, 0, w, h);
@@ -392,39 +669,109 @@ function draw(
ctx.stroke();
ctx.restore();
- // Character
- ctx.save();
- ctx.fillStyle = COLOR_CHAR;
- ctx.globalAlpha = 0.85;
- ctx.beginPath();
- ctx.roundRect(CHAR_X, s.charY, CHAR_SIZE, CHAR_SIZE, 3);
- ctx.fill();
- ctx.restore();
+ // Character sprite (hidden during player death)
+ if (!s.deathAnim || s.deathAnim.type !== "player") {
+ const isJumping = s.charY + CHAR_SIZE < s.groundY;
+ let sheet: HTMLImageElement;
+ let totalFrames: number;
+ let frameIndex: number;
- // Eyes
- ctx.save();
- ctx.fillStyle = COLOR_BG;
- ctx.beginPath();
- ctx.arc(CHAR_X + 6, s.charY + 7, 2.5, 0, Math.PI * 2);
- ctx.fill();
- ctx.beginPath();
- ctx.arc(CHAR_X + 12, s.charY + 7, 2.5, 0, Math.PI * 2);
- ctx.fill();
- ctx.restore();
+ if (s.guarding) {
+ sheet = sprites.guard;
+ totalFrames = GUARD_FRAMES;
+ frameIndex = Math.min(
+ Math.floor(s.guardFrame / GUARD_ANIM_SPEED),
+ totalFrames - 1,
+ );
+ } else if (s.attacking) {
+ sheet = sprites.attack;
+ totalFrames = ATTACK_FRAMES;
+ frameIndex = Math.min(
+ Math.floor(s.attackFrame / ATTACK_ANIM_SPEED),
+ totalFrames - 1,
+ );
+ } else if (isJumping) {
+ sheet = sprites.idle;
+ totalFrames = IDLE_FRAMES;
+ frameIndex = Math.floor(s.frame / ANIM_SPEED) % totalFrames;
+ } else {
+ sheet = sprites.run;
+ totalFrames = RUN_FRAMES;
+ frameIndex = Math.floor(s.frame / ANIM_SPEED) % totalFrames;
+ }
- // Obstacles
- ctx.save();
- ctx.fillStyle = fg;
- ctx.globalAlpha = 0.55;
- for (const o of s.obstacles) {
- ctx.fillRect(o.x, s.groundY - o.height, o.width, o.height);
+ const srcX = frameIndex * SPRITE_FRAME_SIZE;
+ const drawX = s.charX + (CHAR_SIZE - CHAR_SPRITE_SIZE) / 2;
+ const drawY = s.charY + CHAR_SIZE - CHAR_SPRITE_SIZE + 15;
+
+ if (sheet.complete && sheet.naturalWidth > 0) {
+ ctx.drawImage(
+ sheet,
+ srcX,
+ 0,
+ SPRITE_FRAME_SIZE,
+ SPRITE_FRAME_SIZE,
+ drawX,
+ drawY,
+ CHAR_SPRITE_SIZE,
+ CHAR_SPRITE_SIZE,
+ );
+ } else {
+ ctx.save();
+ ctx.fillStyle = COLOR_CHAR;
+ ctx.globalAlpha = 0.85;
+ ctx.beginPath();
+ ctx.roundRect(s.charX, s.charY, CHAR_SIZE, CHAR_SIZE, 3);
+ ctx.fill();
+ ctx.restore();
+ }
}
- ctx.restore();
- // Boss + projectiles
- if (s.boss.phase !== "inactive") {
- drawBoss(ctx, s, COLOR_BG);
- drawProjectiles(ctx, s.boss);
+ // Tree obstacles
+ const treeFrame = Math.floor(s.frame / TREE_ANIM_SPEED) % TREE_FRAMES;
+ for (const o of s.obstacles) {
+ const cfg = TREE_CONFIGS[o.treeType];
+ const treeImg = sprites.trees[o.treeType];
+ if (treeImg.complete && treeImg.naturalWidth > 0) {
+ const treeSrcX = treeFrame * cfg.frameW;
+ const treeDrawX = o.x + (o.width - cfg.renderW) / 2;
+ const treeDrawY = s.groundY - cfg.renderH;
+ ctx.drawImage(
+ treeImg,
+ treeSrcX,
+ 0,
+ cfg.frameW,
+ cfg.frameH,
+ treeDrawX,
+ treeDrawY,
+ cfg.renderW,
+ cfg.renderH,
+ );
+ } else {
+ ctx.save();
+ ctx.fillStyle = fg;
+ ctx.globalAlpha = 0.55;
+ ctx.fillRect(o.x, s.groundY - o.height, o.width, o.height);
+ ctx.restore();
+ }
+ }
+
+ // Boss (hidden during boss death)
+ if (
+ s.boss.phase !== "inactive" &&
+ (!s.deathAnim || s.deathAnim.type !== "boss")
+ ) {
+ drawBoss(ctx, s, sprites);
+ }
+
+ // Attack effects
+ if (s.attackEffects.length > 0) {
+ drawAttackEffects(ctx, s.attackEffects);
+ }
+
+ // Death particles
+ if (s.deathAnim) {
+ drawParticles(ctx, s.deathAnim);
}
// Score HUD
@@ -435,37 +782,7 @@ function draw(
ctx.textAlign = "right";
ctx.fillText(`Score: ${s.score}`, w - 12, 20);
ctx.fillText(`Best: ${s.highScore}`, w - 12, 34);
- if (s.boss.phase === "fighting") {
- ctx.fillText(
- `Evade: ${s.boss.shotsEvaded}/${BOSS_SHOTS_TO_EVADE}`,
- w - 12,
- 48,
- );
- }
ctx.restore();
-
- // Prompts
- if (!started && !s.running && !s.over) {
- ctx.save();
- ctx.fillStyle = fg;
- ctx.globalAlpha = 0.5;
- ctx.font = "12px sans-serif";
- ctx.textAlign = "center";
- ctx.fillText("Click or press Space to play while you wait", w / 2, h / 2);
- ctx.restore();
- }
-
- if (s.over) {
- ctx.save();
- ctx.fillStyle = fg;
- ctx.globalAlpha = 0.7;
- ctx.font = "bold 13px sans-serif";
- ctx.textAlign = "center";
- ctx.fillText("Game Over", w / 2, h / 2 - 8);
- ctx.font = "11px sans-serif";
- ctx.fillText("Click or Space to restart", w / 2, h / 2 + 10);
- ctx.restore();
- }
}
/* ------------------------------------------------------------------ */
@@ -477,6 +794,13 @@ export function useMiniGame() {
const stateRef = useRef
(null);
const rafRef = useRef(0);
const startedRef = useRef(false);
+ const keysRef = useRef({ left: false, right: false });
+ const [activeMode, setActiveMode] = useState<
+ "idle" | "run" | "boss" | "over" | "boss-intro" | "boss-defeated"
+ >("idle");
+ const [showOverlay, setShowOverlay] = useState(true);
+ const [score, setScore] = useState(0);
+ const [highScore, setHighScore] = useState(0);
useEffect(() => {
const canvas = canvasRef.current;
@@ -494,40 +818,91 @@ export function useMiniGame() {
const style = getComputedStyle(canvas);
let fg = style.color || "#71717a";
+ // Load sprite sheets
+ const sprites: Sprites = {
+ run: new Image(),
+ idle: new Image(),
+ attack: new Image(),
+ guard: new Image(),
+ trees: [new Image(), new Image(), new Image()],
+ archerIdle: new Image(),
+ archerAttack: new Image(),
+ };
+ sprites.run.src = runSheet.src;
+ sprites.idle.src = idleSheet.src;
+ sprites.attack.src = attackSheet.src;
+ sprites.guard.src = guardSheet.src;
+ sprites.trees[0].src = tree1Sheet.src;
+ sprites.trees[1].src = tree2Sheet.src;
+ sprites.trees[2].src = tree3Sheet.src;
+ sprites.archerIdle.src = archerIdleSheet.src;
+ sprites.archerAttack.src = archerAttackSheet.src;
+
+ let prevPhase = "";
+
// -------------------------------------------------------------- //
- // Jump //
+ // Input //
// -------------------------------------------------------------- //
function jump() {
const s = stateRef.current;
- if (!s) return;
+ if (!s || !s.running || s.paused || s.over || s.deathAnim) return;
- if (s.over) {
- const hs = s.highScore;
- const gy = s.groundY;
- stateRef.current = makeState(gy);
- stateRef.current.highScore = hs;
- stateRef.current.running = true;
- startedRef.current = true;
- return;
- }
-
- if (!s.running) {
- s.running = true;
- startedRef.current = true;
- return;
- }
-
- // Only jump when on the ground
if (s.charY + CHAR_SIZE >= s.groundY) {
s.vy = JUMP_FORCE;
}
}
- function onKey(e: KeyboardEvent) {
+ function attack() {
+ const s = stateRef.current;
+ if (!s || !s.running || s.attacking || s.guarding || s.deathAnim) return;
+ s.attacking = true;
+ s.attackFrame = 0;
+ s.attackHit = false;
+ }
+
+ function guardStart() {
+ const s = stateRef.current;
+ if (!s || !s.running || s.attacking || s.deathAnim) return;
+ if (!s.guarding) {
+ s.guarding = true;
+ s.guardFrame = 0;
+ }
+ }
+
+ function guardEnd() {
+ const s = stateRef.current;
+ if (!s) return;
+ s.guarding = false;
+ s.guardFrame = 0;
+ }
+
+ function onKeyDown(e: KeyboardEvent) {
if (e.code === "Space" || e.key === " ") {
e.preventDefault();
jump();
}
+ if (e.code === "KeyZ") {
+ e.preventDefault();
+ attack();
+ }
+ if (e.code === "KeyX") {
+ e.preventDefault();
+ guardStart();
+ }
+ if (e.code === "ArrowLeft") {
+ e.preventDefault();
+ keysRef.current.left = true;
+ }
+ if (e.code === "ArrowRight") {
+ e.preventDefault();
+ keysRef.current.right = true;
+ }
+ }
+
+ function onKeyUp(e: KeyboardEvent) {
+ if (e.code === "ArrowLeft") keysRef.current.left = false;
+ if (e.code === "ArrowRight") keysRef.current.right = false;
+ if (e.code === "KeyX") guardEnd();
}
function onClick() {
@@ -544,15 +919,58 @@ export function useMiniGame() {
const ctx = canvas.getContext("2d");
if (!ctx) return;
- update(s, canvas.width);
- draw(ctx, s, canvas.width, canvas.height, fg, startedRef.current);
+ update(s, canvas.width, keysRef.current);
+ draw(ctx, s, canvas.width, canvas.height, fg, sprites);
+
+ // Update active mode on phase change
+ let phase: string;
+ if (s.over) phase = "over";
+ else if (!startedRef.current) phase = "idle";
+ else if (s.paused && s.boss.hp <= 0) phase = "boss-defeated";
+ else if (s.paused) phase = "boss-intro";
+ else if (s.boss.phase !== "inactive") phase = "boss";
+ else phase = "running";
+
+ if (phase !== prevPhase) {
+ prevPhase = phase;
+ switch (phase) {
+ case "idle":
+ setActiveMode("idle");
+ setShowOverlay(true);
+ break;
+ case "running":
+ setActiveMode("run");
+ setShowOverlay(false);
+ break;
+ case "boss-intro":
+ setActiveMode("boss-intro");
+ setShowOverlay(true);
+ break;
+ case "boss":
+ setActiveMode("boss");
+ setShowOverlay(false);
+ break;
+ case "boss-defeated":
+ setActiveMode("boss-defeated");
+ setShowOverlay(true);
+ break;
+ case "over":
+ setActiveMode("over");
+ setScore(s.score);
+ setHighScore(s.highScore);
+ setShowOverlay(true);
+ break;
+ }
+ }
+
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
canvas.addEventListener("click", onClick);
- canvas.addEventListener("keydown", onKey);
+ canvas.addEventListener("keydown", onKeyDown);
+ canvas.addEventListener("keyup", onKeyUp);
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
@@ -570,10 +988,42 @@ export function useMiniGame() {
return () => {
cancelAnimationFrame(rafRef.current);
canvas.removeEventListener("click", onClick);
- canvas.removeEventListener("keydown", onKey);
+ canvas.removeEventListener("keydown", onKeyDown);
+ canvas.removeEventListener("keyup", onKeyUp);
observer.disconnect();
};
}, []);
- return { canvasRef };
+ function onContinue() {
+ const s = stateRef.current;
+ if (!s) return;
+
+ if (s.over) {
+ // Restart after game over
+ const hs = s.highScore;
+ const gy = s.groundY;
+ stateRef.current = makeState(gy);
+ stateRef.current.highScore = hs;
+ stateRef.current.running = true;
+ startedRef.current = true;
+ } else if (!s.running) {
+ // Start game from idle
+ s.running = true;
+ startedRef.current = true;
+ } else if (s.boss.hp <= 0) {
+ // Boss defeated — reset boss, resume running
+ s.boss = makeBoss(s.groundY);
+ s.charX = CHAR_X;
+ s.nextSpawn = s.frame + randInt(SPAWN_MIN / 2, SPAWN_MAX / 2);
+ s.paused = false;
+ } else {
+ // Boss intro — unpause
+ s.paused = false;
+ }
+
+ setShowOverlay(false);
+ canvasRef.current?.focus();
+ }
+
+ return { canvasRef, activeMode, showOverlay, score, highScore, onContinue };
}
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx
index bd47eac051..03fdb8966f 100644
--- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx
+++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/helpers.tsx
@@ -136,7 +136,7 @@ export function getAnimationText(part: {
if (isOperationPendingOutput(output)) return "Agent creation in progress";
if (isOperationInProgressOutput(output))
return "Agent creation already in progress";
- if (isAgentSavedOutput(output)) return `Saved "${output.agent_name}"`;
+ if (isAgentSavedOutput(output)) return `Saved ${output.agent_name}`;
if (isAgentPreviewOutput(output)) return `Preview "${output.agent_name}"`;
if (isClarificationNeededOutput(output)) return "Needs clarification";
return "Error creating agent";