diff --git a/assets/images/dungeonboss-deckbacks.png b/assets/images/dungeonboss-deckbacks.png new file mode 100644 index 0000000..3e18ee1 Binary files /dev/null and b/assets/images/dungeonboss-deckbacks.png differ diff --git a/assets/images/dungeonboss-deckbacks.psd b/assets/images/dungeonboss-deckbacks.psd new file mode 100644 index 0000000..7432684 Binary files /dev/null and b/assets/images/dungeonboss-deckbacks.psd differ diff --git a/data/dungeonboss-artwork.json b/data/dungeonboss-artwork.json index eaeadb3..e0a4a66 100644 --- a/data/dungeonboss-artwork.json +++ b/data/dungeonboss-artwork.json @@ -10,12 +10,18 @@ " art-window aspect the renderer uses).", " 3. Map each card id to its 0-based frame index in the matching map below.", "Sheets: rooms are landscape windows; heroes/spells portrait; bosses larger", - "portrait. Ids not present in a map (or maps left empty) stay procedural." + "portrait. Ids not present in a map (or maps left empty) stay procedural.", + "deckBackSheet is different: it's the FULL card back (not an art window", + "inset into a procedural frame), used by the small deck-pile display on the", + "left side of the board. 4 frames in a 2x2 grid, 300x420 each, row-major:", + "0=Room Deck, 1=Spell Deck, 2=Hero Deck, 3=Epic Hero Deck. Until a path is", + "set, each pile falls back to a procedural back design with a unique glyph." ], "roomSheet": { "key": "dungeonboss-rooms", "path": "assets/images/dungeonboss-rooms.png", "frameWidth": 300, "frameHeight": 200 }, "heroSheet": { "key": "dungeonboss-heroes", "path": "assets/images/dungeonboss-heroes.png", "frameWidth": 300, "frameHeight": 225 }, "spellSheet": { "key": "dungeonboss-spells", "path": "assets/images/dungeonboss-spells.png", "frameWidth": 300, "frameHeight": 160 }, "bossSheet": { "key": "dungeonboss-bosses", "path": "assets/images/dungeonboss-bosses.png", "frameWidth": 300, "frameHeight": 275 }, + "deckBackSheet": { "key": "dungeonboss-deckbacks", "path": "assets/images/dungeonboss-deckbacks.png", "frameWidth": 300, "frameHeight": 420 }, "rooms": { "darkaltar": 0, "opengrave": 1, "specterssanctum": 2, "succubusspa": 3, @@ -47,5 +53,8 @@ "bosses": { "draculord": 0, "cleopatra": 1, "kingcroak": 2, "xyzax": 3, "cerebellus": 4, "seducia": 5, "gorgona": 6, "robobo": 7 + }, + "deckBacks": { + "room": 0, "spell": 1, "hero": 2, "epic": 3 } } diff --git a/src/games/dungeonboss/DungeonBossGame.js b/src/games/dungeonboss/DungeonBossGame.js index bbece67..c5501bc 100644 --- a/src/games/dungeonboss/DungeonBossGame.js +++ b/src/games/dungeonboss/DungeonBossGame.js @@ -107,10 +107,10 @@ export default class DungeonBossGame extends Phaser.Scene { buildStaticUi() { new Button(this, 90, GAME_HEIGHT - 36, 'Leave', () => this.scene.start('GameMenu'), { width: 130, fontSize: 18, variant: 'ghost' }).setDepth(DEPTH.ui); - this.deckText = this.add.text(20, 280, '', { + this.deckText = this.add.text(20, 360, '', { fontFamily: '"Julius Sans One"', fontSize: '16px', color: '#9e9080', lineSpacing: 6, }).setDepth(DEPTH.ui); - this.promptText = this.add.text(GAME_WIDTH / 2, 262, '', { + this.promptText = this.add.text(GAME_WIDTH / 2, 830, '', { fontFamily: 'Righteous', fontSize: '24px', color: C.goldHex, backgroundColor: '#120e16dd', padding: { x: 16, y: 6 }, }).setOrigin(0.5).setDepth(DEPTH.ui); @@ -118,12 +118,12 @@ export default class DungeonBossGame extends Phaser.Scene { // portraits (static; boards re-render around them) const panels = this.oppPanelCenters(); this._oppPortraits = this.opponents.map((opp, i) => - createOpponentPortrait(this, opp, panels[i].x - 245, panels[i].y - 8, 36, DEPTH.ui)); - createPlayerPortrait(this, 80, 700, 40, DEPTH.ui, 'DungeonBossGame'); + createOpponentPortrait(this, opp, panels[i].x - 265, panels[i].y - 88, 36, DEPTH.ui)); + createPlayerPortrait(this, 180, 870, 40, DEPTH.ui, 'DungeonBossGame'); } oppPanelCenters() { - const y = 138; + const y = 218; // panels are 560 wide; the 3-up layout previously spaced centers exactly // 560 apart, leaving zero gap edge-to-edge — add a little breathing room. const xs = this.opponents.length === 1 ? [960] @@ -456,6 +456,64 @@ export default class DungeonBossGame extends Phaser.Scene { return cont; } + // Small "how many are left in this deck" pile — two offset shadow cards + // behind a top card, whose face uses deckBackSheet art if supplied + // (see dungeonboss-artwork.json) or a procedural glyph-per-deck fallback. + drawDeckPile(x, y, w, h, kind, count, parent) { + const cont = this.add.container(x, y); + for (let i = 2; i >= 1; i--) { + const g = this.add.graphics(); + g.fillStyle(0x1a1420, 1); g.fillRoundedRect(-w / 2 + i * 3, -h / 2 + i * 3, w, h, 5); + g.lineStyle(1.5, 0x4a3a5c, 0.7); g.strokeRoundedRect(-w / 2 + i * 3, -h / 2 + i * 3, w, h, 5); + cont.add(g); + } + const art = this.artFor('deckBack', kind); + if (art) { + cont.add(this.add.image(0, 0, art.key, art.frame).setDisplaySize(w, h)); + const b = this.add.graphics(); + b.lineStyle(2, 0x4a3a5c, 1); b.strokeRoundedRect(-w / 2, -h / 2, w, h, 5); + cont.add(b); + } else { + const g = this.add.graphics(); + g.fillStyle(0x241a2e, 1); g.fillRoundedRect(-w / 2, -h / 2, w, h, 5); + g.lineStyle(2, 0x4a3a5c, 1); g.strokeRoundedRect(-w / 2, -h / 2, w, h, 5); + g.lineStyle(1, 0x4a3a5c, 0.6); g.strokeRoundedRect(-w / 2 + 4, -h / 2 + 4, w - 8, h - 8, 3); + cont.add(g); + const glyph = { room: '⌂', spell: '✦', hero: '⚔', epic: '♛' }[kind] || '✦'; + cont.add(this.add.text(0, 0, glyph, { fontSize: `${Math.round(h * 0.34)}px`, color: '#5c4a70' }).setOrigin(0.5)); + } + if (count <= 0) { + cont.add(this.add.text(0, 0, '✕', { fontSize: `${Math.round(h * 0.5)}px`, color: '#c0392b' }).setOrigin(0.5)); + cont.setAlpha(0.45); + } + (parent || this.boardLayer).add(cont); + return cont; + } + + // Left-side "deck piles" area: small representations of the four draw + // decks with remaining counts, sitting in the gap between the deck-count + // text (above) and the player portrait cluster (below). + renderDeckPiles() { + const gs = this.gs; + const piles = [ + { kind: 'room', label: 'Room Deck', count: gs.decks.rooms.length }, + { kind: 'spell', label: 'Spell Deck', count: gs.decks.spells.length }, + { kind: 'hero', label: 'Hero', count: gs.decks.heroes.length }, + { kind: 'epic', label: 'Epic Hero', count: gs.decks.epics.length }, + ]; + const x = 60, startY = 520, rowH = 80, w = 46, h = 64; + piles.forEach((pile, i) => { + const y = startY + i * rowH; + this.drawDeckPile(x, y, w, h, pile.kind, pile.count, this.boardLayer); + this.boardLayer.add(this.add.text(x + w / 2 + 14, y - 9, pile.label, { + fontFamily: '"Julius Sans One"', fontSize: '14px', color: '#c9bfae', + }).setOrigin(0, 0.5)); + this.boardLayer.add(this.add.text(x + w / 2 + 14, y + 11, `${pile.count} left`, { + fontFamily: '"Julius Sans One"', fontSize: '12px', color: '#9e9080', + }).setOrigin(0, 0.5)); + }); + } + finishCard(cont, w, h, opts) { if (opts.deactivated) { const ov = this.add.graphics(); @@ -545,7 +603,7 @@ export default class DungeonBossGame extends Phaser.Scene { } townPos(i, n) { const pitch = Math.min(124, 1100 / Math.max(1, n)); - return { x: GAME_WIDTH / 2 - ((n - 1) * pitch) / 2 + i * pitch, y: 408 }; + return { x: GAME_WIDTH / 2 - ((n - 1) * pitch) / 2 + i * pitch, y: 458 }; } // ── full re-render ────────────────────────────────────────────────────────── @@ -566,6 +624,7 @@ export default class DungeonBossGame extends Phaser.Scene { this.renderTown(); this.renderHumanBoard(); this.renderHand(); + this.renderDeckPiles(); this.deckText.setText([ `Rooms ${gs.decks.rooms.length}`, `Spells ${gs.decks.spells.length}`, @@ -653,7 +712,7 @@ export default class DungeonBossGame extends Phaser.Scene { renderTown() { const gs = this.gs; const n = gs.town.length; - const label = this.add.text(GAME_WIDTH / 2, 318, `— TOWN ${n ? '' : '(empty)'} —`, { + const label = this.add.text(GAME_WIDTH / 2, 368, `— TOWN ${n ? '' : '(empty)'} —`, { fontFamily: 'Righteous', fontSize: '18px', color: '#9e9080', }).setOrigin(0.5); this.townLayer.add(label); @@ -679,14 +738,14 @@ export default class DungeonBossGame extends Phaser.Scene { }); // souls & wounds beside the player portrait const sg = this.add.graphics(); - sg.fillStyle(C.soul, 1); sg.fillCircle(60, 780, 13); + sg.fillStyle(C.soul, 1); sg.fillCircle(160, 950, 13); this.boardLayer.add(sg); - this._soulsPos.set(seat, { x: 60, y: 780 }); - this.boardLayer.add(this.add.text(82, 780, `${p.souls}/${SOULS_TO_WIN} souls`, { + this._soulsPos.set(seat, { x: 160, y: 950 }); + this.boardLayer.add(this.add.text(182, 950, `${p.souls}/${SOULS_TO_WIN} souls`, { fontFamily: 'Righteous', fontSize: '20px', color: C.goldHex, }).setOrigin(0, 0.5)); for (let wI = 0; wI < WOUNDS_TO_DIE; wI++) { - this.boardLayer.add(this.add.text(52 + wI * 26, 818, '☠', { + this.boardLayer.add(this.add.text(152 + wI * 26, 988, '☠', { fontSize: '21px', color: wI < p.wounds ? '#c0392b' : '#3a3242', }).setOrigin(0.5)); } @@ -745,12 +804,8 @@ export default class DungeonBossGame extends Phaser.Scene { this.boardLayer.add(g); } } - // entrance arch + queued heroes + // queued heroes at entrance const entR = this.humanSlotRect(Math.max(p.dungeon.length, 1) - 0.35); - this.boardLayer.add(this.add.text(entR.x - 130, 700, '⛩', { fontSize: '44px' }).setOrigin(0.5).setAlpha(0.8)); - this.boardLayer.add(this.add.text(entR.x - 130, 748, 'ENTRANCE', { - fontFamily: '"Julius Sans One"', fontSize: '12px', color: '#9e9080', - }).setOrigin(0.5)); p.entrance.forEach((hero, hi) => { const hx = entR.x - 130 - hi * 24; const t = this.heroToken(hero, hx, 640); @@ -1271,7 +1326,7 @@ export default class DungeonBossGame extends Phaser.Scene { addActionButton(label, cb) { this._actionButtons = this._actionButtons || []; const i = this._actionButtons.length; - const b = new Button(this, GAME_WIDTH / 2 + 470 + (i % 2) * 160, 262 + Math.floor(i / 2) * 48, label, cb, + const b = new Button(this, GAME_WIDTH / 2 + 470 + (i % 2) * 160, 830 + Math.floor(i / 2) * 48, label, cb, { width: 150, fontSize: 18 }); b.setDepth(DEPTH.ui); this._actionButtons.push(b); diff --git a/src/games/dungeonboss/sprites.md b/src/games/dungeonboss/sprites.md index 67f9171..4f1f840 100644 --- a/src/games/dungeonboss/sprites.md +++ b/src/games/dungeonboss/sprites.md @@ -238,7 +238,35 @@ just make each one visually distinct and readable at the small on-board size. --- -## 5. Menu icon — `game-icons.png` frame **77** *(shared sheet)* +## 5. Deck-back sheet — `dungeonboss-deckbacks.png` + +| | | +|---|---| +| **Path** | `public/assets/images/dungeonboss-deckbacks.png` | +| **Frame size** | **300 × 420 px** (aspect ≈ 0.71 : 1, portrait — a full card back, not an art window) | +| **Sheet size** | **2 columns × 2 rows = 600 × 840 px** holds all 4 frames exactly | +| **Status** | ✅ Art already dropped in and wired (`deckBackSheet.path` is set) | +| **JSON** | `deckBackSheet` (set `path`) + `deckBacks` map (already filled in for all 4 ids) | + +Unlike the four sheets above, this one is **not** an illustration inset into a +procedurally-drawn card frame — draw the **entire card back**, border and all, +since it's shown as-is (just scaled) in the small "decks remaining" pile +display on the left side of the board, below the deck-count text. Until art +is supplied, each pile falls back to a procedural back design (dark plate, +double border, one glyph per deck: ⌂ room, ✦ spell, ⚔ hero, ♛ epic hero). + +### Frame map + +| Frame | id | Pile | +|---:|---|---| +| 0 | `room` | Room Deck | +| 1 | `spell` | Spell Deck | +| 2 | `hero` | Hero Deck | +| 3 | `epic` | Epic Hero Deck | + +--- + +## 6. Menu icon — `game-icons.png` frame **77** *(shared sheet)* | | | |---|---| @@ -262,8 +290,9 @@ rather than a new file. - [x] `dungeonboss-spells.png` — 1200×640 (4×4 grid), 300×160 per frame, 16 frames (0–15); `spellSheet.path` set in `dungeonboss-artwork.json`. - [x] `dungeonboss-heroes.png` — 2100×1350 (7×6 grid), 300×225 per frame, 41 frames (0–40); `heroSheet.path` set in `dungeonboss-artwork.json`. - [x] `dungeonboss-bosses.png` — 1200×550 (4×2 grid), 300×275 per frame, 8 frames (0–7); `bossSheet.path` set in `dungeonboss-artwork.json`. +- [x] `dungeonboss-deckbacks.png` — 600×840 (2×2 grid), 300×420 per frame, 4 frames (0–3); `deckBackSheet.path` set in `dungeonboss-artwork.json`. - [ ] `game-icons.png` frame 77 — 44×44 menu icon. -The frame→id mappings for all four sheets are already filled in in +The frame→id mappings for all five sheets are already filled in in `public/data/dungeonboss-artwork.json` — once you drop the PNGs in and set the -four `path` fields, art appears with no code changes. +`path` fields, art appears with no code changes. diff --git a/src/scenes/PreloadScene.js b/src/scenes/PreloadScene.js index 2750f91..7484a39 100644 --- a/src/scenes/PreloadScene.js +++ b/src/scenes/PreloadScene.js @@ -230,7 +230,7 @@ export default class PreloadScene extends Phaser.Scene { // Dungeon Boss drop-in spritesheets, same contract as Spire Climb's. const dbArt = this.cache.json.get('dungeonboss-artwork'); - const dbSheets = [dbArt?.roomSheet, dbArt?.heroSheet, dbArt?.spellSheet, dbArt?.bossSheet] + const dbSheets = [dbArt?.roomSheet, dbArt?.heroSheet, dbArt?.spellSheet, dbArt?.bossSheet, dbArt?.deckBackSheet] .filter((s) => s && s.path && s.key && !this.textures.exists(s.key)); // Star Wars Deckbuilder drop-in spritesheets, same contract.