bump mission rewards and add first-clear card logic

- Increased gold rewards for several Imperial missions to improve progression pacing.
- Reformatted `missions.json` for consistency (multi-line arrays).
- Set default starting gold to 300 in `SaveManager`.
- Implemented first-clear-only card rewards in `BattleScene`; subsequent wins grant gold but no duplicate cards.
- Added consolation gold (50% of mission reward) on defeat.
- Fixed `CollectionScene` input priority and improved terminal typing speed and line wrapping.
This commit is contained in:
Brian Fertig 2026-03-22 12:51:43 -06:00
parent 126182aa19
commit 8b6ccba887
4 changed files with 385 additions and 73 deletions

View File

@ -46,7 +46,7 @@
]
},
"rewards": {
"gold": 150,
"gold": 400,
"cards": [
"imp_trooper_1"
]
@ -74,7 +74,7 @@
]
},
"rewards": {
"gold": 200,
"gold": 400,
"cards": [
"imp_gunner_1"
]
@ -101,7 +101,7 @@
]
},
"rewards": {
"gold": 250,
"gold": 500,
"cards": [
"imp_shieldwall_1"
]
@ -128,7 +128,7 @@
]
},
"rewards": {
"gold": 300,
"gold": 700,
"cards": [
"raider_berserker_1"
]
@ -362,7 +362,6 @@
},
"lore": "The tide rises. Every drop of blood spilled feeds the Countess. She is ancient, she is patient, and she has had enough of slumber."
},
{
"id": "mission_014",
"name": "Emergence",
@ -373,9 +372,20 @@
"unlockCondition": null,
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_drone_1", "xeno_drone_1", "xeno_drone_1", "xeno_spitter_1", "xeno_spitter_1"]
"cards": [
"xeno_drone_1",
"xeno_drone_1",
"xeno_drone_1",
"xeno_spitter_1",
"xeno_spitter_1"
]
},
"rewards": {
"gold": 150,
"cards": [
"xeno_drone_1"
]
},
"rewards": { "gold": 150, "cards": ["xeno_drone_1"] },
"lore": "The first chitin fragments were dismissed as natural formations. Then the formations moved."
},
{
@ -388,9 +398,21 @@
"unlockCondition": "mission_014",
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_drone_1", "xeno_spitter_1", "xeno_guard_1", "xeno_mantis_1", "xeno_msentinel_1", "xeno_horror_1"]
"cards": [
"xeno_drone_1",
"xeno_spitter_1",
"xeno_guard_1",
"xeno_mantis_1",
"xeno_msentinel_1",
"xeno_horror_1"
]
},
"rewards": {
"gold": 200,
"cards": [
"xeno_spitter_1"
]
},
"rewards": { "gold": 200, "cards": ["xeno_spitter_1"] },
"lore": "They move in patterns too precise for instinct. Something is directing them."
},
{
@ -403,9 +425,21 @@
"unlockCondition": "mission_015",
"opponent": {
"commander": "xeno_cmd_1",
"cards": ["xeno_worker_1", "xeno_guard_1", "xeno_mantis_1", "xeno_horror_1", "xeno_parasite_1", "xeno_hiveguard_1"]
"cards": [
"xeno_worker_1",
"xeno_guard_1",
"xeno_mantis_1",
"xeno_horror_1",
"xeno_parasite_1",
"xeno_hiveguard_1"
]
},
"rewards": {
"gold": 250,
"cards": [
"xeno_beetle_1"
]
},
"rewards": { "gold": 250, "cards": ["xeno_beetle_1"] },
"lore": "The tunnels go deeper than any mine. Deeper than any root. Something vast breathes in the dark."
},
{
@ -418,9 +452,22 @@
"unlockCondition": "mission_016",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_beetle_1", "xeno_mantis_1", "xeno_horror_1", "xeno_parasite_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1"]
"cards": [
"xeno_beetle_1",
"xeno_mantis_1",
"xeno_horror_1",
"xeno_parasite_1",
"xeno_hiveguard_1",
"xeno_wyrm_1",
"xeno_tyrant_1"
]
},
"rewards": {
"gold": 300,
"cards": [
"xeno_worker_1"
]
},
"rewards": { "gold": 300, "cards": ["xeno_worker_1"] },
"lore": "The Alpha moved differently from the others. With purpose. With malice."
},
{
@ -433,9 +480,22 @@
"unlockCondition": "mission_017",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_guard_1", "xeno_msentinel_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1"]
"cards": [
"xeno_guard_1",
"xeno_msentinel_1",
"xeno_hiveguard_1",
"xeno_wyrm_1",
"xeno_tyrant_1",
"xeno_queen_1",
"xeno_colossus_1"
]
},
"rewards": {
"gold": 350,
"cards": [
"xeno_guard_1"
]
},
"rewards": { "gold": 350, "cards": ["xeno_guard_1"] },
"lore": "Our shields corroded in minutes. Our armor lasted seconds. Only retreat lasted."
},
{
@ -448,9 +508,22 @@
"unlockCondition": "mission_018",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_mantis_1", "xeno_parasite_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1"]
"cards": [
"xeno_mantis_1",
"xeno_parasite_1",
"xeno_hiveguard_1",
"xeno_wyrm_1",
"xeno_tyrant_1",
"xeno_queen_1",
"xeno_colossus_1"
]
},
"rewards": {
"gold": 400,
"cards": [
"xeno_mantis_1"
]
},
"rewards": { "gold": 400, "cards": ["xeno_mantis_1"] },
"lore": "Every queen killed made the swarm fight harder. We began to understand the hive link."
},
{
@ -463,9 +536,22 @@
"unlockCondition": "mission_019",
"opponent": {
"commander": "xeno_cmd_2",
"cards": ["xeno_horror_1", "xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1", "xeno_apex_1"]
"cards": [
"xeno_horror_1",
"xeno_hiveguard_1",
"xeno_wyrm_1",
"xeno_tyrant_1",
"xeno_queen_1",
"xeno_colossus_1",
"xeno_apex_1"
]
},
"rewards": {
"gold": 450,
"cards": [
"xeno_horror_1"
]
},
"rewards": { "gold": 450, "cards": ["xeno_horror_1"] },
"lore": "It moved faster than anything that size should. The venom hit before we even saw it."
},
{
@ -478,9 +564,22 @@
"unlockCondition": "mission_020",
"opponent": {
"commander": "xeno_cmd_3",
"cards": ["xeno_hiveguard_1", "xeno_wyrm_1", "xeno_tyrant_1", "xeno_queen_1", "xeno_colossus_1", "xeno_apex_1", "xeno_leviathan_1"]
"cards": [
"xeno_hiveguard_1",
"xeno_wyrm_1",
"xeno_tyrant_1",
"xeno_queen_1",
"xeno_colossus_1",
"xeno_apex_1",
"xeno_leviathan_1"
]
},
"rewards": {
"gold": 500,
"cards": [
"xeno_hiveguard_1"
]
},
"rewards": { "gold": 500, "cards": ["xeno_hiveguard_1"] },
"lore": "It spoke through the swarm. Not words. Something older. The feeling of being consumed."
},
{
@ -493,9 +592,21 @@
"unlockCondition": null,
"opponent": {
"commander": "right_cmd_1",
"cards": ["right_acolyte_1", "right_initiate_1", "right_sentinel_1", "right_zealot_1", "right_priest_1", "right_warden_1"]
"cards": [
"right_acolyte_1",
"right_initiate_1",
"right_sentinel_1",
"right_zealot_1",
"right_priest_1",
"right_warden_1"
]
},
"rewards": {
"gold": 100,
"cards": [
"right_acolyte_1"
]
},
"rewards": { "gold": 100, "cards": ["right_acolyte_1"] },
"lore": "They marched out of a blizzard that shouldn't have existed on this world. Silent. Certain."
},
{
@ -508,9 +619,21 @@
"unlockCondition": "mission_022",
"opponent": {
"commander": "right_cmd_1",
"cards": ["right_pilgrim_1", "right_ward_1", "right_zealot_1", "right_crusader_1", "right_hacker_1", "right_warden_1"]
"cards": [
"right_pilgrim_1",
"right_ward_1",
"right_zealot_1",
"right_crusader_1",
"right_hacker_1",
"right_warden_1"
]
},
"rewards": {
"gold": 150,
"cards": [
"right_pilgrim_1"
]
},
"rewards": { "gold": 150, "cards": ["right_pilgrim_1"] },
"lore": "The signal was a prayer. It promised salvation through absolute zero."
},
{
@ -523,9 +646,21 @@
"unlockCondition": "mission_023",
"opponent": {
"commander": "right_cmd_2",
"cards": ["right_zealot_1", "right_priest_1", "right_warden_1", "right_hacker_1", "right_templar_1", "right_purifier_1"]
"cards": [
"right_zealot_1",
"right_priest_1",
"right_warden_1",
"right_hacker_1",
"right_templar_1",
"right_purifier_1"
]
},
"rewards": {
"gold": 200,
"cards": [
"right_sentinel_1"
]
},
"rewards": { "gold": 200, "cards": ["right_sentinel_1"] },
"lore": "Their Pontiff overcharged the vanguard. Two of our tanks melted before anyone fired back."
},
{
@ -538,9 +673,21 @@
"unlockCondition": "mission_024",
"opponent": {
"commander": "right_cmd_2",
"cards": ["right_hacker_1", "right_crusader_1", "right_templar_1", "right_purifier_1", "right_inquisitor_1", "right_judicator_1"]
"cards": [
"right_hacker_1",
"right_crusader_1",
"right_templar_1",
"right_purifier_1",
"right_inquisitor_1",
"right_judicator_1"
]
},
"rewards": {
"gold": 250,
"cards": [
"right_hacker_1"
]
},
"rewards": { "gold": 250, "cards": ["right_hacker_1"] },
"lore": "It copied our targeting protocols in milliseconds. We shot ourselves before we realized."
},
{
@ -553,9 +700,21 @@
"unlockCondition": "mission_025",
"opponent": {
"commander": "right_cmd_2",
"cards": ["right_templar_1", "right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1"]
"cards": [
"right_templar_1",
"right_purifier_1",
"right_inquisitor_1",
"right_judicator_1",
"right_prophet_1",
"right_archangel_1"
]
},
"rewards": {
"gold": 300,
"cards": [
"right_zealot_1"
]
},
"rewards": { "gold": 300, "cards": ["right_zealot_1"] },
"lore": "The cold came first. Then the smiting. Then silence."
},
{
@ -568,9 +727,21 @@
"unlockCondition": "mission_026",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
"cards": [
"right_purifier_1",
"right_inquisitor_1",
"right_judicator_1",
"right_prophet_1",
"right_archangel_1",
"right_seraph_1"
]
},
"rewards": {
"gold": 350,
"cards": [
"right_templar_1"
]
},
"rewards": { "gold": 350, "cards": ["right_templar_1"] },
"lore": "The Oracle calculated our defeat in 847 possible outcomes. It only needed one."
},
{
@ -583,9 +754,19 @@
"unlockCondition": "mission_027",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
"cards": [
"right_judicator_1",
"right_prophet_1",
"right_archangel_1",
"right_seraph_1"
]
},
"rewards": {
"gold": 400,
"cards": [
"right_purifier_1"
]
},
"rewards": { "gold": 400, "cards": ["right_purifier_1"] },
"lore": "The Prophet spoke once. The temperature dropped forty degrees. The battle had already begun."
},
{
@ -598,9 +779,21 @@
"unlockCondition": "mission_028",
"opponent": {
"commander": "right_cmd_3",
"cards": ["right_purifier_1", "right_inquisitor_1", "right_judicator_1", "right_prophet_1", "right_archangel_1", "right_seraph_1"]
"cards": [
"right_purifier_1",
"right_inquisitor_1",
"right_judicator_1",
"right_prophet_1",
"right_archangel_1",
"right_seraph_1"
]
},
"rewards": {
"gold": 450,
"cards": [
"right_prophet_1"
]
},
"rewards": { "gold": 450, "cards": ["right_prophet_1"] },
"lore": "It called us heretics. It called us flawed. It was wrong. We are survivors."
},
{
@ -613,9 +806,21 @@
"unlockCondition": null,
"opponent": {
"commander": "imp_cmd_2",
"cards": ["imp_shieldwall_1", "imp_juggernaut_1", "imp_mech_1", "imp_sentinel_1", "imp_praetorian_1", "imp_centurion_1"]
"cards": [
"imp_shieldwall_1",
"imp_juggernaut_1",
"imp_mech_1",
"imp_sentinel_1",
"imp_praetorian_1",
"imp_centurion_1"
]
},
"rewards": {
"gold": 200,
"cards": [
"imp_trooper_1"
]
},
"rewards": { "gold": 200, "cards": ["imp_trooper_1"] },
"lore": "They marched in perfect formation. No fear. No hesitation. Only iron certainty."
},
{
@ -628,9 +833,21 @@
"unlockCondition": "mission_030",
"opponent": {
"commander": "imp_cmd_2",
"cards": ["imp_mortar_1", "imp_sniper_1", "imp_juggernaut_1", "imp_sentinel_1", "imp_praetorian_1", "imp_dreadnought_1"]
"cards": [
"imp_mortar_1",
"imp_sniper_1",
"imp_juggernaut_1",
"imp_sentinel_1",
"imp_praetorian_1",
"imp_dreadnought_1"
]
},
"rewards": {
"gold": 250,
"cards": [
"imp_shieldwall_1"
]
},
"rewards": { "gold": 250, "cards": ["imp_shieldwall_1"] },
"lore": "The fires formed a perfect rectangle. Military precision, even in destruction."
},
{
@ -643,9 +860,21 @@
"unlockCondition": "mission_031",
"opponent": {
"commander": "imp_cmd_3",
"cards": ["imp_sniper_1", "imp_mech_1", "imp_sentinel_1", "imp_praetorian_1", "imp_dreadnought_1", "imp_orbital_1"]
"cards": [
"imp_sniper_1",
"imp_mech_1",
"imp_sentinel_1",
"imp_praetorian_1",
"imp_dreadnought_1",
"imp_orbital_1"
]
},
"rewards": {
"gold": 300,
"cards": [
"imp_gunner_1"
]
},
"rewards": { "gold": 300, "cards": ["imp_gunner_1"] },
"lore": "The Marshal's first order was always the same: 'Fire everything.'"
},
{
@ -658,9 +887,22 @@
"unlockCondition": "mission_032",
"opponent": {
"commander": "imp_cmd_3",
"cards": ["imp_mech_1", "imp_juggernaut_1", "imp_sentinel_1", "imp_praetorian_1", "imp_centurion_1", "imp_dreadnought_1", "imp_orbital_1"]
"cards": [
"imp_mech_1",
"imp_juggernaut_1",
"imp_sentinel_1",
"imp_praetorian_1",
"imp_centurion_1",
"imp_dreadnought_1",
"imp_orbital_1"
]
},
"rewards": {
"gold": 350,
"cards": [
"imp_sniper_1"
]
},
"rewards": { "gold": 350, "cards": ["imp_sniper_1"] },
"lore": "She called them her fist. They hit like a meteor."
},
{
@ -673,9 +915,22 @@
"unlockCondition": "mission_033",
"opponent": {
"commander": "imp_cmd_3",
"cards": ["imp_mortar_1", "imp_sentinel_1", "imp_praetorian_1", "imp_centurion_1", "imp_dreadnought_1", "imp_dreadnought_1", "imp_orbital_1"]
"cards": [
"imp_mortar_1",
"imp_sentinel_1",
"imp_praetorian_1",
"imp_centurion_1",
"imp_dreadnought_1",
"imp_dreadnought_1",
"imp_orbital_1"
]
},
"rewards": {
"gold": 400,
"cards": [
"imp_mortar_1"
]
},
"rewards": { "gold": 400, "cards": ["imp_mortar_1"] },
"lore": "The bombardment lasted four days. On the fifth, the Marshal walked in unopposed."
},
{
@ -688,9 +943,21 @@
"unlockCondition": "mission_034",
"opponent": {
"commander": "imp_cmd_4",
"cards": ["imp_sentinel_1", "imp_praetorian_1", "imp_centurion_1", "imp_dreadnought_1", "imp_orbital_1", "imp_mech_1"]
"cards": [
"imp_sentinel_1",
"imp_praetorian_1",
"imp_centurion_1",
"imp_dreadnought_1",
"imp_orbital_1",
"imp_mech_1"
]
},
"rewards": {
"gold": 450,
"cards": [
"imp_praetorian_1"
]
},
"rewards": { "gold": 450, "cards": ["imp_praetorian_1"] },
"lore": "When the Imperator spoke, allies grew stronger and enemies forgot how to hold a weapon."
},
{
@ -703,9 +970,22 @@
"unlockCondition": "mission_035",
"opponent": {
"commander": "imp_cmd_4",
"cards": ["imp_sentinel_1", "imp_centurion_1", "imp_centurion_1", "imp_dreadnought_1", "imp_dreadnought_1", "imp_orbital_1", "imp_praetorian_1"]
"cards": [
"imp_sentinel_1",
"imp_centurion_1",
"imp_centurion_1",
"imp_dreadnought_1",
"imp_dreadnought_1",
"imp_orbital_1",
"imp_praetorian_1"
]
},
"rewards": {
"gold": 500,
"cards": [
"imp_sentinel_1"
]
},
"rewards": { "gold": 500, "cards": ["imp_sentinel_1"] },
"lore": "He committed everything. Every mech, every dreadnought, every elite. He wanted no survivors."
},
{
@ -718,9 +998,21 @@
"unlockCondition": "mission_036",
"opponent": {
"commander": "imp_cmd_4",
"cards": ["imp_sentinel_1", "imp_centurion_1", "imp_dreadnought_1", "imp_orbital_1", "imp_orbital_1", "imp_centurion_1"]
"cards": [
"imp_sentinel_1",
"imp_centurion_1",
"imp_dreadnought_1",
"imp_orbital_1",
"imp_orbital_1",
"imp_centurion_1"
]
},
"rewards": {
"gold": 600,
"cards": [
"imp_dreadnought_1"
]
},
"rewards": { "gold": 600, "cards": ["imp_dreadnought_1"] },
"lore": "He smiled when we arrived. He had been waiting. This was always the plan."
}
]

View File

@ -5,7 +5,7 @@ export class SaveManager {
static defaultSave() {
return {
version: CURRENT_VERSION,
gold: 400,
gold: 300,
level: 1,
collection: {},
decks: [],

View File

@ -3821,9 +3821,13 @@ export class BattleScene extends Phaser.Scene {
if (won && this.missionData) {
const rewards = this.missionData.rewards;
const firstClear = !save.campaignProgress.completedMissions.includes(this.missionData.id);
SaveManager.addGold(save, rewards.gold);
SaveManager.completeMission(save, this.missionData.id);
if (rewards.cards) {
// Cards only awarded on first completion
if (firstClear && rewards.cards) {
rewards.cards.forEach(cardId => SaveManager.addCard(save, cardId));
}
@ -3845,7 +3849,7 @@ export class BattleScene extends Phaser.Scene {
fontSize: '36px', color: '#ffd700', fontFamily: 'RaiderCrusader'
}).setOrigin(0.5);
if (rewards.cards && rewards.cards.length > 0) {
if (firstClear && rewards.cards && rewards.cards.length > 0) {
const cardManager = this.registry.get('cardManager');
const cardNames = rewards.cards.map(id => {
const c = cardManager.getCard(id);
@ -3864,6 +3868,17 @@ export class BattleScene extends Phaser.Scene {
fontSize: '22px', color: '#ffdd44', fontFamily: 'Audiowide'
}).setOrigin(0.5);
}
} else if (!won && this.missionData) {
const consolationGold = Math.ceil(this.missionData.rewards.gold / 2);
SaveManager.addGold(save, consolationGold);
this.registry.set('save', save);
this.add.text(width / 2, height / 2 + 20, 'Defeat...', {
fontSize: '28px', color: '#aaaaaa', fontFamily: 'Audiowide'
}).setOrigin(0.5);
this.add.text(width / 2, height / 2 + 60, `+${consolationGold} Gold (consolation)`, {
fontSize: '22px', color: '#aa8800', fontFamily: 'Audiowide'
}).setOrigin(0.5);
} else if (!won) {
this.add.text(width / 2, height / 2 + 20, 'Better luck next time!', {
fontSize: '24px', color: '#aaaaaa', fontFamily: 'Audiowide'

View File

@ -100,6 +100,9 @@ export class CollectionScene extends Phaser.Scene {
this._makeBackButton();
this._setupScrolling();
// Ensure filter buttons and back button take input priority over scrolled cards
this.input.setTopOnly(true);
// Boot sequence
this.time.delayedCall(300, () => this._typeBootSequence());
}
@ -179,10 +182,11 @@ export class CollectionScene extends Phaser.Scene {
const bg = this.add.rectangle(x + w / 2, y + h / 2, w, h, color, 0.8)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x666688);
.setStrokeStyle(1, 0x666688)
.setDepth(50);
const text = this.add.text(x + w / 2, y + h / 2, displayLabel, {
fontSize: '14px', color: '#ffffff', fontFamily: 'Audiowide'
}).setOrigin(0.5);
}).setOrigin(0.5).setDepth(50);
bg.on('pointerdown', onClick);
bg.on('pointerover', () => bg.setStrokeStyle(2, 0xaaaacc));
@ -725,27 +729,27 @@ export class CollectionScene extends Phaser.Scene {
this._termContainer.add(textObj);
this._terminalTextObjects.push(textObj);
// Type character by character
// Type characters per tick (5 at a time for snappy display)
let charIdx = 0;
const typeChar = () => {
charIdx = Math.min(charIdx + 5, line.length);
textObj.setText(line.substring(0, charIdx));
// Keyclick roughly every 3 visible ticks
if (charIdx % 15 === 0) this._playKeyclick();
// Update cursor position
this._updateCursorPos(textObj);
if (charIdx >= line.length) {
// Line complete — move to next line after pause
this._currentTermLine++;
this._updateCursorPos(textObj);
// Line complete — advance by actual wrapped line count to prevent overlap
const wrappedCount = Math.max(1, Math.round(textObj.height / this._termLineH));
this._currentTermLine += wrappedCount;
const evt = this.time.delayedCall(24, () => this._typeLines(lines, lineIdx + 1));
this._typingEvents.push(evt);
return;
}
textObj.setText(line.substring(0, charIdx + 1));
charIdx++;
// Keyclick every 2-3 chars
if (charIdx % 3 === 0) this._playKeyclick();
// Update cursor position
this._updateCursorPos(textObj);
const evt = this.time.delayedCall(4, typeChar);
this._typingEvents.push(evt);
};
@ -797,8 +801,9 @@ export class CollectionScene extends Phaser.Scene {
_makeBackButton() {
const bg = this.add.rectangle(80, 1040, 140, 40, 0x333333)
.setInteractive({ useHandCursor: true })
.setStrokeStyle(1, 0x888888);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
.setStrokeStyle(1, 0x888888)
.setDepth(50);
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5).setDepth(50);
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
bg.on('pointerover', () => bg.setStrokeStyle(2, 0xaaaaaa));
bg.on('pointerout', () => bg.setStrokeStyle(1, 0x888888));