diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx index 26977a207a..9ecd639a82 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/tools/CreateAgent/CreateAgent.tsx @@ -4,7 +4,6 @@ import { Button } from "@/components/atoms/Button/Button"; import { Text } from "@/components/atoms/Text/Text"; import { BookOpenIcon, - CheckFatIcon, PencilSimpleIcon, WarningDiamondIcon, } from "@phosphor-icons/react"; @@ -24,6 +23,7 @@ import { ClarificationQuestionsCard, ClarifyingQuestion, } from "./components/ClarificationQuestionsCard"; +import sparklesImg from "./components/MiniGame/assets/sparkles.png"; import { MiniGame } from "./components/MiniGame/MiniGame"; import { AccordionIcon, @@ -83,7 +83,8 @@ function getAccordionMeta(output: CreateAgentToolOutput) { ) { return { icon, - title: "Creating agent, this may take a few minutes. Sit back and relax.", + title: + "Creating agent, this may take a few minutes. Play while you wait.", expanded: true, }; } @@ -167,16 +168,22 @@ export function CreateAgentTool({ part }: Props) { {isAgentSavedOutput(output) && (
- - {output.message} + Agent{" "} + + {output.agent_name} + {" "} + has been saved to your library!
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 [{children}]; +} + 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}

+ )} + +
+ )} +
); } 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";