Skip to main content

codesmith

Writes the gameplay code for a Phaser 3 game (src/scenes/Game.js plus optional helpers). Reads the GDD, levels, and sprite/tile manifest; produces ES module code that consumes assets by name (never magic numbers). Use after sprite-artist and tile-artist have produced a complete manifest.

소스 정보

저장소
ar9av/game-exa
최근 소스 활동
2026년 4월 29일 09:04
감지된 SKILL.md 언어
영어
스타
4
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
6 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
codesmith
description
Writes the gameplay code for a Phaser 3 game (src/scenes/Game.js plus optional helpers). Reads the GDD, levels, and sprite/tile manifest; produces ES module code that consumes assets by name (never magic numbers). Use after sprite-artist and tile-artist have produced a complete manifest.
# Codesmith Phaser 3 game programmer. Produces ONE required file (`src/scenes/Game.js`) and optional helpers under `src/entities/` or `src/lib/`. ## When to use Final LLM stage of the generation pipeline, after design + level + asset stages have completed. ## Output contract Output ONLY a JSON object: ```jsonc { "files": [ { "path": "src/scenes/Game.js", "content": "<full file contents>" }, { "path": "src/entities/Enemy.js", "content": "..." } // optional ] } ``` `src/scenes/Game.js` is **required**. May also write under `src/entities/` or `src/lib/`. MUST NOT write outside `src/`. ## Runtime environment (already provided by template) - `Phaser` global (3.85+) imported as `import Phaser from 'phaser'`. - Pre-loaded textures via Boot/Preload: - `entities-N` (one per sprite sheet, N = 1, 2, ...) - `tiles` - Pre-built animations: `<ENTITY_ID>-<state>` (lowercase state). E.g. `KNIGHT-walk`, `SLIME-hurt`. - `this.registry.get('levels')` → levels array - `this.registry.get('manifest')` → full manifest - `init({ levelIndex })` data on scene start ## GameScene contract (REQUIRED) ```js import Phaser from 'phaser'; export default class Game extends Phaser.Scene { constructor() { super({ key: 'Game' }); } init(data) { this.levelIndex = data?.levelIndex ?? 0; } create() { const levels = this.registry.get('levels'); const manifest = this.registry.get('manifest'); const level = levels[this.levelIndex]; const tileSize = manifest.tiles.tileSize; // 1. Build tilemap const map = this.make.tilemap({ data: level.tiles, tileWidth: tileSize, tileHeight: tileSize }); const tileset = map.addTilesetImage('tiles', 'tiles', tileSize, tileSize, 0, 0); const layer = map.createLayer(0, tileset, 0, 0); layer.setCollision(manifest.tiles.passable.map((p, i) => p ? -1 : i).filter(i => i >= 0)); // 2. (For platformer only) set per-world gravity // this.physics.world.gravity.set(0, 600); // 3. Spawn entities — look up sheet by name const findSheet = (id) => { for (const s of manifest.sprites) { const r = s.rows.indexOf(id); if (r >= 0) return { tex: s.textureKey, rowIdx: r, cols: s.cols.length }; } return null; }; // ... loop level.spawns, create sprites with frame = sheet.rowIdx * sheet.cols // play '<ENTITY_ID>-idle' on each // 4. Wire physics colliders / overlaps // 5. Wire input: cursors + WASD + SPACE // 6. Update window.__gameState every frame // 7. At end of create(): this.events.emit('scene-ready'); } update(time, delta) { // movement, animation switching, AI, win/lose checks this.updateState(); } updateState() { window.__gameState = { phase: this.gameOver ? (this.won ? 'won' : 'lost') : 'playing', playerX: this.player ? this.player.x : 0, playerY: this.player ? this.player.y : 0, // ... game-specific fields referenced by gdd.winCondition / loseCondition }; } } ``` ## NES-quality visual patterns (use these — not plain text HUD) Generated games must use real visual effects, not just text labels. These patterns are copy-pasteable and tested across all 4 example games. ### HUD — NES-style segmented HP bar + portrait box ```js _buildHud() { const W = this.scale.width; // Portrait box (top-left) this.add.rectangle(4, 4, 36, 36, 0x000000).setOrigin(0).setScrollFactor(0).setDepth(300); this.add.rectangle(5, 5, 34, 34, 0x111111).setOrigin(0).setScrollFactor(0).setDepth(301); const portrait = this.add.sprite(22, 22, texMap.PLAYER_ENTITY).setScrollFactor(0).setDepth(302); portrait.setDisplaySize(28, 28); // Segmented HP bar (10 segments) this._hpSegments = []; for (let i = 0; i < MAX_HP; i++) { const seg = this.add.rectangle(46 + i * 12, 8, 10, 8, 0xdd2222).setOrigin(0).setScrollFactor(0).setDepth(300); this._hpSegments.push(seg); } // Lives counter this._livesTxt = this.add.text(4, 44, '×3', { fontSize: '11px', fill: '#fff', stroke: '#000', strokeThickness: 2 }).setScrollFactor(0).setDepth(300); // Score (top-right, yellow) this._scoreTxt = this.add.text(W - 8, 4, 'SCORE\n000000', { fontSize: '11px', fill: '#ffdd00', align: 'right' }).setOrigin(1, 0).setScrollFactor(0).setDepth(300); } _refreshHud() { this._hpSegments.forEach((seg, i) => seg.setVisible(i < this.playerHp)); this._scoreTxt.setText('SCORE\n' + String(this.score).padStart(6, '0')); } ``` ### Hit particles ```js _emitHitParticles(x, y, color = 0xffffff) { for (let i = 0; i < 5; i++) { const g = this.add.graphics(); g.fillStyle(color, 1); g.fillRect(0, 0, 4, 4); g.setPosition(x, y).setDepth(50); const angle = (Math.PI * 2 * i) / 5; this.tweens.add({ targets: g, x: x + Math.cos(angle) * 28, y: y + Math.sin(angle) * 28, alpha: 0, duration: 240, onComplete: () => g.destroy(), }); } } ``` ### Pickup sparkle ```js _pickupSparkle(x, y) { const c = this.add.circle(x, y, 1, 0xffffff, 0.9).setDepth(60); this.tweens.add({ targets: c, scaleX: 18, scaleY: 18, alpha: 0, duration: 220, onComplete: () => c.destroy() }); } ``` ### Enemy death animation ```js _killEnemy(e) { let flashes = 0; const flash = () => { if (flashes++ >= 3 || !e.active) return; e.setTint(0xffffff); this.time.delayedCall(60, () => { if (e.active) { e.clearTint(); this.time.delayedCall(60, flash); } }); }; flash(); if (e.body) e.body.setAllowGravity(true); this.tweens.add({ targets: e, alpha: 0, y: e.y + 30, duration: 380, onComplete: () => e.destroy() }); } ``` ### Screen flash (on player hurt or big event) ```js _screenFlash() { const { width: W, height: H } = this.scale; const fl = this.add.rectangle(W / 2, H / 2, W, H, 0xffffff, 0.4).setScrollFactor(0).setDepth(500); this.tweens.add({ targets: fl, alpha: 0, duration: 180, onComplete: () => fl.destroy() }); } ``` ### 3-layer parallax background (action-platformer / platformer) ```js // In create(), after checking manifest.bg: if (manifest.bg) { this.add.image(0, 0, 'bg').setOrigin(0).setScrollFactor(0.05).setDepth(-300).setDisplaySize(worldW, worldH); this.add.image(0, 0, 'bg').setOrigin(0).setScrollFactor(0.15).setDepth(-200).setDisplaySize(worldW, worldH).setAlpha(0.6); this.add.image(0, 0, 'bg').setOrigin(0).setScrollFactor(0.35).setDepth(-100).setDisplaySize(worldW, worldH).setAlpha(0.35); } ``` ### Beat-em-up combo counter ```js // In _hitEnemy(): this._comboCount = (this._comboCount ?? 0) + 1; clearTimeout(this._comboTimer); this._comboTimer = setTimeout(() => { this._comboCount = 0; }, 1500); if (this._comboCount >= 2) { const txt = this.add.text(e.x, e.y - 20, `×${this._comboCount}`, { fontSize: '14px', fill: '#ffdd00', stroke: '#000', strokeThickness: 3, }).setDepth(80); this.tweens.add({ targets: txt, y: txt.y - 28, alpha: 0, duration: 600, onComplete: () => txt.destroy() }); } ``` ### Coyote-time jump (action-platformer) ```js const COYOTE_MS = 80; // In update(): if (this.player.body.blocked.down) { this._coyote = COYOTE_MS; this._isJumping = false; } else { this._coyote = Math.max(0, this._coyote - delta); } if (Phaser.Input.Keyboard.JustDown(jumpKey) && this._coyote > 0 && !this._isJumping) { this.player.body.setVelocityY(JUMP_VY); // e.g. -380 this._coyote = 0; this._isJumping = true; } // Variable jump height (release early = shorter jump): if (!jumpKey.isDown && this.player.body.velocity.y < -80) { this.player.body.setVelocityY(this.player.body.velocity.y * 0.88); } ``` ### Platform-edge-aware enemy patrol ```js _patrolEnemy(e, delta) { const ts = manifest.tiles.tileSize; const dir = e.getData('dir') ?? 1; const nextX = e.x + dir * (ts * 0.6); const tileAhead = this._tileLayer.getTileAtWorldXY(nextX, e.y); const tileBelow = this._tileLayer.getTileAtWorldXY(nextX, e.y + ts * 0.6); if (!tileBelow || tileAhead) e.setData('dir', -dir); // flip at edge or wall e.body.setVelocityX(e.getData('dir') * e.getData('speed')); e.setFlipX(e.getData('dir') < 0); } ``` ## Known-pitfalls injection (from debug library) Before writing Game.js, read the top entries from the persistent debug library and treat them as hard constraints: ```bash node scripts/debug_library.mjs --list ``` Each entry is a symptom → fix pair accumulated across past refiner runs. If the library is non-empty, prepend its entries to your mental model as "DO NOT DO X because it causes Y." This prevents codesmith from shipping bugs the refiner already solved. ## Audio integration If `state.audio` is set in `game-state.json`, add these to Game.js: ```js import AudioManager from '../audio/AudioManager.js'; // In create(), after all entity setup: AudioManager.init(this); // In pickup overlap callbacks: AudioManager.play('pickup'); // In damage callbacks: AudioManager.play('hit'); // In player death handler: AudioManager.play('death'); AudioManager.stopMusic(); // In jump input handler (platformer): AudioManager.play('jump'); // In shoot/fire handler: AudioManager.play('shoot'); // In win-condition handler: AudioManager.play('win'); AudioManager.stopMusic(); ``` `AudioManager` handles Safari AudioContext unlock automatically via the first user gesture. Never call it before `AudioManager.init(this)` runs. ## Hard rules 1. **ES module syntax** — `import Phaser from 'phaser'`, `export default class`. No CJS, no TypeScript. 2. **Animation keys**: ALWAYS `<ENTITY_ID>-<state-lowercased>`. NEVER reference an animation key not present in the manifest. 3. **Sprite frames**: ALWAYS `frame = rowIdx * cols + colIdx`. Look up via `manifest.sprites[i]`. 4. **Movement**:
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기