feat(deckbuilder): add unsaved changes warning and rebalance vampire cards

- **DeckBuilderScene**:
  - Introduced `hasUnsavedChanges` state to track modifications.
  - Added visual indicators: an "Unsaved Changes" badge in the deck panel and dynamic styling for the Save button.
  - Implemented a confirmation modal when attempting to leave the scene with unsaved changes.
  - Fixed scroll position restoration after re-rendering the card grid.

- **Data (cards.json)**:
  - Rebalanced Vampire cards by swapping `drain` and `rupture` values (reduced drain, increased rupture).
  - Increased `rupture` values across higher-tier vampire cards (3, 6, 10, 15) to enhance their late-game scaling.
This commit is contained in:
Brian Fertig 2026-03-24 19:30:35 -06:00
parent 52808806a8
commit d17379ca57
2 changed files with 66 additions and 11 deletions

View File

@ -5054,12 +5054,12 @@
"skills": [
{
"name": "drain",
"value": 2,
"value": 1,
"trigger": "preBattle"
},
{
"name": "rupture",
"value": 1,
"value": 2,
"trigger": "on_attack"
}
]
@ -5077,7 +5077,7 @@
},
{
"name": "rupture",
"value": 2,
"value": 3,
"trigger": "on_attack"
}
]
@ -5095,7 +5095,7 @@
},
{
"name": "rupture",
"value": 3,
"value": 6,
"trigger": "on_attack"
}
]
@ -5113,7 +5113,7 @@
},
{
"name": "rupture",
"value": 4,
"value": 10,
"trigger": "on_attack"
}
]
@ -5131,7 +5131,7 @@
},
{
"name": "rupture",
"value": 5,
"value": 15,
"trigger": "on_attack"
}
]

View File

@ -57,6 +57,7 @@ export class DeckBuilderScene extends Phaser.Scene {
this.scrollMask = null;
this.scrollY = 0;
this.maxScrollY = 0;
this.hasUnsavedChanges = false;
// Grid layout constants (shared)
this.CARD_W = 155;
@ -111,7 +112,6 @@ export class DeckBuilderScene extends Phaser.Scene {
// Create a container to hold all grid content
this.scrollContainer = this.add.container(0, 0);
this.scrollY = 0;
let cursorY = this.GRID_TOP;
@ -171,6 +171,10 @@ export class DeckBuilderScene extends Phaser.Scene {
const visibleH = this.SCROLL_BOTTOM - this.SCROLL_TOP;
this.maxScrollY = Math.max(0, totalContentH - visibleH);
// Clamp and restore scroll position
this.scrollY = Phaser.Math.Clamp(this.scrollY, 0, this.maxScrollY);
this.scrollContainer.y = -this.scrollY;
// Apply rectangular mask so content doesn't overflow into header or footer
const maskShape = this.make.graphics({ x: 0, y: 0 });
maskShape.fillStyle(0xffffff);
@ -291,6 +295,7 @@ export class DeckBuilderScene extends Phaser.Scene {
this.workingDeck.cards.push(card.id);
}
this.usedInDeck[card.id] = (this.usedInDeck[card.id] || 0) + 1;
this.hasUnsavedChanges = true;
this._renderCardGrid();
this._renderDeckPanel();
});
@ -327,6 +332,13 @@ export class DeckBuilderScene extends Phaser.Scene {
}).setOrigin(0.5);
this.deckPanelGroup.push(title);
if (this.hasUnsavedChanges) {
const unsavedBadge = this.add.text(panelCX, 112, '● UNSAVED CHANGES', {
fontSize: '13px', color: '#ffaa00', fontFamily: 'Audiowide'
}).setOrigin(0.5);
this.deckPanelGroup.push(unsavedBadge);
}
// ── Commander slot ──
const cmdCard = this.workingDeck.commander ? this.cardManager.getCard(this.workingDeck.commander) : null;
const cmdBg = this.add.rectangle(panelCX, 140, PANEL_W - 40, 46, cmdCard ? 0x1a3a5c : 0x1a1a2e)
@ -346,6 +358,7 @@ export class DeckBuilderScene extends Phaser.Scene {
const oldCmd = this.workingDeck.commander;
this.workingDeck.commander = null;
if (oldCmd) delete this.usedInDeck[oldCmd];
this.hasUnsavedChanges = true;
this._renderDeckPanel();
this._renderCardGrid();
});
@ -390,6 +403,7 @@ export class DeckBuilderScene extends Phaser.Scene {
this.workingDeck.cards.splice(idx, 1);
this.usedInDeck[cardId] = (this.usedInDeck[cardId] || 1) - 1;
if (this.usedInDeck[cardId] <= 0) delete this.usedInDeck[cardId];
this.hasUnsavedChanges = true;
this._renderDeckPanel();
this._renderCardGrid();
}
@ -430,10 +444,12 @@ export class DeckBuilderScene extends Phaser.Scene {
// ── Save / Clear buttons (recreated here to stay above the panel background) ──
const saveBtnY = this.scale.height - 60;
const saveBg = this.add.rectangle(panelCX - 110, saveBtnY, 190, 46, 0x224422)
.setInteractive({ useHandCursor: true }).setStrokeStyle(2, 0x44aa44);
const saveFill = this.hasUnsavedChanges ? 0x664400 : 0x224422;
const saveBorder = this.hasUnsavedChanges ? 0xffaa00 : 0x44aa44;
const saveBg = this.add.rectangle(panelCX - 110, saveBtnY, 190, 46, saveFill)
.setInteractive({ useHandCursor: true }).setStrokeStyle(2, saveBorder);
const saveTxt = this.add.text(panelCX - 110, saveBtnY, 'Save Deck', {
fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide'
fontSize: '18px', color: this.hasUnsavedChanges ? '#ffaa00' : '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5);
saveBg.on('pointerdown', () => {
const save = this.registry.get('save');
@ -441,7 +457,9 @@ export class DeckBuilderScene extends Phaser.Scene {
if (!validation.valid) { this._showMsg(validation.errors[0], '#ff4444'); return; }
SaveManager.saveDeck(save, this.workingDeck);
this.registry.set('save', save);
this.hasUnsavedChanges = false;
this._showMsg('Deck saved!', '#44ff44');
this._renderDeckPanel();
});
const clearBg = this.add.rectangle(panelCX + 110, saveBtnY, 190, 46, 0x442222)
@ -453,6 +471,7 @@ export class DeckBuilderScene extends Phaser.Scene {
this.workingDeck.commander = null;
this.workingDeck.cards = [];
this.usedInDeck = {};
this.hasUnsavedChanges = true;
this._renderDeckPanel();
this._renderCardGrid();
});
@ -562,7 +581,43 @@ export class DeckBuilderScene extends Phaser.Scene {
this.add.text(80, this.scale.height - 40, 'Back', {
fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5);
backBtn.on('pointerdown', () => this.scene.start('MainMenuScene'));
backBtn.on('pointerdown', () => {
if (this.hasUnsavedChanges) {
this._confirmLeave();
} else {
this.scene.start('MainMenuScene');
}
});
}
_confirmLeave() {
const { width, height } = this.scale;
const cx = width / 2;
const cy = height / 2;
const overlay = this.add.rectangle(cx, cy, width, height, 0x000000, 0.7).setDepth(50);
const box = this.add.rectangle(cx, cy, 420, 160, 0x0a0f1a).setStrokeStyle(2, 0xffaa00).setDepth(51);
const msg = this.add.text(cx, cy - 36, 'You have unsaved changes.\nLeave without saving?', {
fontSize: '17px', color: '#ffffff', fontFamily: 'Audiowide', align: 'center'
}).setOrigin(0.5).setDepth(52);
const leaveBg = this.add.rectangle(cx - 90, cy + 36, 150, 40, 0x442222)
.setInteractive({ useHandCursor: true }).setStrokeStyle(2, 0xaa4444).setDepth(51);
const leaveTxt = this.add.text(cx - 90, cy + 36, 'Leave', {
fontSize: '16px', color: '#ff6666', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(52);
const stayBg = this.add.rectangle(cx + 90, cy + 36, 150, 40, 0x224422)
.setInteractive({ useHandCursor: true }).setStrokeStyle(2, 0x44aa44).setDepth(51);
const stayTxt = this.add.text(cx + 90, cy + 36, 'Stay', {
fontSize: '16px', color: '#66ff66', fontFamily: 'Audiowide'
}).setOrigin(0.5).setDepth(52);
const modal = [overlay, box, msg, leaveBg, leaveTxt, stayBg, stayTxt];
const close = () => modal.forEach(o => { if (o.scene) o.destroy(); });
leaveBg.on('pointerdown', () => this.scene.start('MainMenuScene'));
stayBg.on('pointerdown', close);
}
_showMsg(msg, color) {