Dungeon Boss item positioning

This commit is contained in:
Brian Fertig 2026-07-04 12:34:37 -06:00
parent 57f9941dc4
commit 29cf5972b6
6 changed files with 115 additions and 22 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

View File

@ -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
}
}

View File

@ -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);

View File

@ -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 (015); `spellSheet.path` set in `dungeonboss-artwork.json`.
- [x] `dungeonboss-heroes.png` — 2100×1350 (7×6 grid), 300×225 per frame, 41 frames (040); `heroSheet.path` set in `dungeonboss-artwork.json`.
- [x] `dungeonboss-bosses.png` — 1200×550 (4×2 grid), 300×275 per frame, 8 frames (07); `bossSheet.path` set in `dungeonboss-artwork.json`.
- [x] `dungeonboss-deckbacks.png` — 600×840 (2×2 grid), 300×420 per frame, 4 frames (03); `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.

View File

@ -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.