```
Refactor pack system to faction-specific packs with slot-based drops - Split standard pack into 5 faction-specific packs (imperial, raider, bloodthirsty, xeno, righteous) - Each pack has 3 guaranteed common slots + 1 special slot with tiered rarity distribution - Packs unlock by completing their respective campaign missions - Updated StoreScene to display faction colors, unlock status, and support pack opening animations Fix bloodpact skill trigger timing and mechanics - Changed bloodpact from preBattle to preAttack trigger (now fires per attack instead of once per turn) - Reduced HP cost to 50% of value (was 100%), maintaining same ATK gain - Moved bloodpact processing from _collectPreBattleFires to _buildPendingAttacks - Added death check for self-killed cards during preAttack phase Update drain skill description and trigger - Changed drain from preBattle to preAttack trigger - Updated description to clarify it targets the opposing card and its neighbors Add fusion activation video and enhanced reveal effects - Added forge2.mp4 for fusion animation (swaps with idle forge during activation) - Implemented 3-phase energy vortex effect: particle streams, lightning bolts, convergence flash - Faction-colored effects based on selected stack's faction Update attack spritesheet and card data - Updated attacks.png/psd with new bloodpact visual (frame 24) - Changed all bloodpact skill triggers in cards.json from preBattle to preAttack ```
This commit is contained in:
parent
b03f7f9f5e
commit
217bf1148e
Binary file not shown.
|
Before Width: | Height: | Size: 4.4 MiB After Width: | Height: | Size: 4.5 MiB |
Binary file not shown.
Binary file not shown.
|
|
@ -1893,7 +1893,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 2,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1906,7 +1906,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 3,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1919,7 +1919,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 5,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1932,7 +1932,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 7,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1945,7 +1945,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 10,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2247,7 +2247,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 3,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "bloodrage",
|
||||
|
|
@ -2265,7 +2265,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 5,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "bloodrage",
|
||||
|
|
@ -2283,7 +2283,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 7,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "bloodrage",
|
||||
|
|
@ -2301,7 +2301,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 10,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "bloodrage",
|
||||
|
|
@ -2319,7 +2319,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 15,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "bloodrage",
|
||||
|
|
@ -2449,7 +2449,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 4,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "siphon",
|
||||
|
|
@ -2467,7 +2467,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 6,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "siphon",
|
||||
|
|
@ -2485,7 +2485,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 9,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "siphon",
|
||||
|
|
@ -2503,7 +2503,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 14,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "siphon",
|
||||
|
|
@ -2521,7 +2521,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 20,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "siphon",
|
||||
|
|
@ -2752,7 +2752,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 5,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "drain",
|
||||
|
|
@ -2770,7 +2770,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 8,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "drain",
|
||||
|
|
@ -2788,7 +2788,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 11,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "drain",
|
||||
|
|
@ -2806,7 +2806,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 17,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "drain",
|
||||
|
|
@ -2824,7 +2824,7 @@
|
|||
{
|
||||
"name": "bloodpact",
|
||||
"value": 25,
|
||||
"trigger": "preBattle"
|
||||
"trigger": "preAttack"
|
||||
},
|
||||
{
|
||||
"name": "drain",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,77 @@
|
|||
[
|
||||
{
|
||||
"id": "standard_pack",
|
||||
"name": "Standard Pack",
|
||||
"cost": 500,
|
||||
"cardCount": 3,
|
||||
"dropRates": {
|
||||
"common": 0.60,
|
||||
"rare": 0.30,
|
||||
"epic": 0.08,
|
||||
"legendary": 0.02
|
||||
},
|
||||
"cardPool": ["imperial", "raider"],
|
||||
"description": "A standard booster pack containing cards from both factions.",
|
||||
"artKey": "pack_standard"
|
||||
"id": "pack_imperial",
|
||||
"name": "Imperial Standard Pack",
|
||||
"faction": "imperial",
|
||||
"cost": 400,
|
||||
"unlockCondition": null,
|
||||
"artKey": "pack_imperial",
|
||||
"description": "Standard imperial booster pack.",
|
||||
"slots": [
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["imperial"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["imperial"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["imperial"] },
|
||||
{ "dropRates": { "common": 0.35, "rare": 0.50, "epic": 0.10, "legendary": 0.05 }, "cardPool": ["imperial"], "special": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pack_raider",
|
||||
"name": "Raider Standard Pack",
|
||||
"faction": "raider",
|
||||
"cost": 400,
|
||||
"unlockCondition": "campaign_raider",
|
||||
"artKey": "pack_raider",
|
||||
"description": "Standard raider booster pack.",
|
||||
"slots": [
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["raider"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["raider"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["raider"] },
|
||||
{ "dropRates": { "common": 0.35, "rare": 0.50, "epic": 0.10, "legendary": 0.05 }, "cardPool": ["raider"], "special": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pack_bloodthirsty",
|
||||
"name": "Bloodthirsty Standard Pack",
|
||||
"faction": "bloodthirsty",
|
||||
"cost": 400,
|
||||
"unlockCondition": "campaign_bloodthirsty",
|
||||
"artKey": "pack_bloodthirsty",
|
||||
"description": "Standard bloodthirsty booster pack.",
|
||||
"slots": [
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["bloodthirsty"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["bloodthirsty"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["bloodthirsty"] },
|
||||
{ "dropRates": { "common": 0.35, "rare": 0.50, "epic": 0.10, "legendary": 0.05 }, "cardPool": ["bloodthirsty"], "special": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pack_xeno",
|
||||
"name": "Xeno Standard Pack",
|
||||
"faction": "xeno",
|
||||
"cost": 400,
|
||||
"unlockCondition": "campaign_xeno",
|
||||
"artKey": "pack_xeno",
|
||||
"description": "Standard xeno booster pack.",
|
||||
"slots": [
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["xeno"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["xeno"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["xeno"] },
|
||||
{ "dropRates": { "common": 0.35, "rare": 0.50, "epic": 0.10, "legendary": 0.05 }, "cardPool": ["xeno"], "special": true }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pack_righteous",
|
||||
"name": "Righteous Standard Pack",
|
||||
"faction": "righteous",
|
||||
"cost": 400,
|
||||
"unlockCondition": "campaign_righteous",
|
||||
"artKey": "pack_righteous",
|
||||
"description": "Standard righteous booster pack.",
|
||||
"slots": [
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["righteous"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["righteous"] },
|
||||
{ "dropRates": { "common": 1.0 }, "cardPool": ["righteous"] },
|
||||
{ "dropRates": { "common": 0.35, "rare": 0.50, "epic": 0.10, "legendary": 0.05 }, "cardPool": ["righteous"], "special": true }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -149,14 +149,14 @@
|
|||
},
|
||||
{
|
||||
"name": "drain",
|
||||
"description": "Before battle: deal value damage to the opposing card or commander, ignoring armor. Heal self for the same amount.",
|
||||
"trigger": "preBattle",
|
||||
"description": "Before battle: deal value damage to the opposing card and it's neighbors or commander, ignoring armor. Heal self for the same amount.",
|
||||
"trigger": "preAttack",
|
||||
"category": "offense"
|
||||
},
|
||||
{
|
||||
"name": "bloodpact",
|
||||
"description": "Before battle: lose value HP, permanently gain +value ATK.",
|
||||
"trigger": "preBattle",
|
||||
"description": "She demands blood sacrafice! Sacrafice value HP x .5, permanently gain +value ATK.",
|
||||
"trigger": "preAttack",
|
||||
"category": "offense"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -354,7 +354,7 @@ export class CombatEngine {
|
|||
this._rupturePhase();
|
||||
this._jamPhase();
|
||||
this._preBattlePhase();
|
||||
this._deathCheck(); // bloodpact can self-kill cards
|
||||
this._deathCheck();
|
||||
this._buildPendingAttacks();
|
||||
this.playerGoesFirst = !this.playerGoesFirst; // flip initiative for next turn
|
||||
return [...this.events];
|
||||
|
|
@ -367,7 +367,6 @@ export class CombatEngine {
|
|||
const protectFires = [];
|
||||
const enfeebeFires = [];
|
||||
const jamFires = [];
|
||||
const bloodpactFires = [];
|
||||
const drainFires = [];
|
||||
const liveAllies = allies.filter(c => c.currentHP > 0);
|
||||
const liveEnemies = enemies.filter(c => c.currentHP > 0);
|
||||
|
|
@ -413,10 +412,6 @@ export class CombatEngine {
|
|||
this._log(`${card.name} jam suppresses ${t.card.name} skills by ${t.reductions.map(r => r.amount).join('/')}`);
|
||||
}
|
||||
}
|
||||
if (s.name === 'bloodpact' && ctx.bloodpactCard) {
|
||||
bloodpactFires.push({ skill: 'bloodpact', source: card, value: ctx.bloodpactValue });
|
||||
this._log(`${card.name} bloodpact: -${ctx.bloodpactValue} HP, +${ctx.bloodpactValue} ATK`);
|
||||
}
|
||||
if (s.name === 'drain' && ctx.drainTarget) {
|
||||
const hpBefore = ctx.drainTarget.currentHP + ctx.drainDamage;
|
||||
const targetIsCommander = ctx.drainTarget === enemyCommander;
|
||||
|
|
@ -445,7 +440,7 @@ export class CombatEngine {
|
|||
}
|
||||
}
|
||||
}
|
||||
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, bloodpactFires, drainFires };
|
||||
return { buffs, siegeFires, protectFires, enfeebeFires, jamFires, drainFires };
|
||||
}
|
||||
|
||||
// Emit the 8-step pre-battle sequence and process preBattle skills.
|
||||
|
|
@ -466,8 +461,8 @@ export class CombatEngine {
|
|||
// Steps 3–4: commander offensive buffs
|
||||
const firstCmdFires = this._collectPreBattleFires([firstCmd], firstAllies, otherAllies, otherCmd, [otherCmd]);
|
||||
const otherCmdFires = this._collectPreBattleFires([otherCmd], otherAllies, firstAllies, firstCmd, [firstCmd]);
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: firstSide, card: firstCmd, buffs: firstCmdFires.buffs, siegeFires: firstCmdFires.siegeFires, protectFires: firstCmdFires.protectFires, enfeebeFires: firstCmdFires.enfeebeFires, jamFires: firstCmdFires.jamFires, bloodpactFires: firstCmdFires.bloodpactFires, drainFires: firstCmdFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, buffs: otherCmdFires.buffs, siegeFires: otherCmdFires.siegeFires, protectFires: otherCmdFires.protectFires, enfeebeFires: otherCmdFires.enfeebeFires, jamFires: otherCmdFires.jamFires, bloodpactFires: otherCmdFires.bloodpactFires, drainFires: otherCmdFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: firstSide, card: firstCmd, buffs: firstCmdFires.buffs, siegeFires: firstCmdFires.siegeFires, protectFires: firstCmdFires.protectFires, enfeebeFires: firstCmdFires.enfeebeFires, jamFires: firstCmdFires.jamFires, drainFires: firstCmdFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'commander', side: otherSide, card: otherCmd, buffs: otherCmdFires.buffs, siegeFires: otherCmdFires.siegeFires, protectFires: otherCmdFires.protectFires, enfeebeFires: otherCmdFires.enfeebeFires, jamFires: otherCmdFires.jamFires, drainFires: otherCmdFires.drainFires });
|
||||
|
||||
// Steps 5–6: lane defensive buffs (placeholder)
|
||||
this.events.push({ type: 'preBattle', phase: 'defensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: [], siegeFires: [], protectFires: [], enfeebeFires: [], jamFires: [] });
|
||||
|
|
@ -476,8 +471,8 @@ export class CombatEngine {
|
|||
// Steps 7–8: lane offensive buffs + siege fires + protect fires + enfeeble fires + jam fires
|
||||
const firstLaneFires = this._collectPreBattleFires(firstLanes, firstAllies, otherAllies, otherCmd, otherLanes);
|
||||
const otherLaneFires = this._collectPreBattleFires(otherLanes, otherAllies, firstAllies, firstCmd, firstLanes);
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: firstLaneFires.buffs, siegeFires: firstLaneFires.siegeFires, protectFires: firstLaneFires.protectFires, enfeebeFires: firstLaneFires.enfeebeFires, jamFires: firstLaneFires.jamFires, bloodpactFires: firstLaneFires.bloodpactFires, drainFires: firstLaneFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: otherLaneFires.buffs, siegeFires: otherLaneFires.siegeFires, protectFires: otherLaneFires.protectFires, enfeebeFires: otherLaneFires.enfeebeFires, jamFires: otherLaneFires.jamFires, bloodpactFires: otherLaneFires.bloodpactFires, drainFires: otherLaneFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: firstSide, cards: firstLanes, buffs: firstLaneFires.buffs, siegeFires: firstLaneFires.siegeFires, protectFires: firstLaneFires.protectFires, enfeebeFires: firstLaneFires.enfeebeFires, jamFires: firstLaneFires.jamFires, drainFires: firstLaneFires.drainFires });
|
||||
this.events.push({ type: 'preBattle', phase: 'offensive', target: 'lanes', side: otherSide, cards: otherLanes, buffs: otherLaneFires.buffs, siegeFires: otherLaneFires.siegeFires, protectFires: otherLaneFires.protectFires, enfeebeFires: otherLaneFires.enfeebeFires, jamFires: otherLaneFires.jamFires, drainFires: otherLaneFires.drainFires });
|
||||
}
|
||||
|
||||
// Build the ordered list of attacks for this turn (called by beginCommit).
|
||||
|
|
@ -585,6 +580,13 @@ export class CombatEngine {
|
|||
preAttackFires.push({ skill: 'siphon', attacker: pending.attacker, target: pending.target, heal: ctx.siphonHeal });
|
||||
this._log(`${pending.attacker.name} siphons ${ctx.siphonHeal} HP`);
|
||||
}
|
||||
if (s.name === 'bloodpact' && ctx.bloodpactCard) {
|
||||
preAttackFires.push({ skill: 'bloodpact', value: ctx.bloodpactValue, hpCost: ctx.bloodpactHPCost });
|
||||
this._log(`${pending.attacker.name} bloodpact: -${ctx.bloodpactHPCost} HP, +${ctx.bloodpactValue} ATK`);
|
||||
if (pending.attacker.currentHP <= 0) {
|
||||
this.events.push({ type: 'death', card: pending.attacker, side: pending.side });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit preAttack event (inserted before any death events from above)
|
||||
|
|
|
|||
|
|
@ -403,11 +403,13 @@ export class SkillProcessor {
|
|||
}
|
||||
|
||||
bloodpact(skill, attacker, defender, allies, enemies, context) {
|
||||
attacker.currentHP -= skill.value;
|
||||
const hpCost = Math.ceil(skill.value * 0.5);
|
||||
attacker.currentHP -= hpCost;
|
||||
attacker.currentAttack += skill.value;
|
||||
if (context) {
|
||||
context.bloodpactCard = attacker;
|
||||
context.bloodpactValue = skill.value;
|
||||
context.bloodpactHPCost = hpCost;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,21 +14,30 @@ export class PackManager {
|
|||
return [...this.packs.values()];
|
||||
}
|
||||
|
||||
isPackUnlocked(pack, save, campaigns) {
|
||||
if (!pack.unlockCondition) return true;
|
||||
const campaign = campaigns.find(c => c.id === pack.unlockCondition);
|
||||
if (!campaign) return false;
|
||||
if (campaign.missions.length === 0) return false;
|
||||
const completed = save.campaignProgress?.completedMissions || [];
|
||||
return campaign.missions.every(m => completed.includes(m));
|
||||
}
|
||||
|
||||
openPack(packId, save) {
|
||||
const pack = this.getPack(packId);
|
||||
if (!pack) return null;
|
||||
const rng = new RNG(Date.now() ^ (Math.random() * 0xffffffff));
|
||||
const results = [];
|
||||
for (let i = 0; i < pack.cardCount; i++) {
|
||||
const rarity = this._rollRarity(pack.dropRates, rng);
|
||||
for (const slot of pack.slots) {
|
||||
const rarity = this._rollRarity(slot.dropRates, rng);
|
||||
const pool = this.cardManager.getAllCards().filter(c =>
|
||||
c.rarity === rarity &&
|
||||
c.type !== 'commander' &&
|
||||
pack.cardPool.includes(c.faction)
|
||||
slot.cardPool.includes(c.faction)
|
||||
);
|
||||
if (pool.length === 0) continue;
|
||||
const card = pool[rng.nextInt(0, pool.length - 1)];
|
||||
results.push(card);
|
||||
results.push({ ...card, _slotSpecial: !!slot.special });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,6 +715,7 @@ export class BattleScene extends Phaser.Scene {
|
|||
else if (group.skill === 'strike') this._animateStrikeFire(event.attacker, group, cb);
|
||||
else if (group.skill === 'pierce') this._animatePierceFire(event.attacker, group, cb);
|
||||
else if (group.skill === 'siphon') this._animateSiphonFire(event.attacker, group, cb);
|
||||
else if (group.skill === 'bloodpact') this._animateBloodpactFire(event.attacker, group, cb);
|
||||
else cb();
|
||||
}
|
||||
};
|
||||
|
|
@ -1513,18 +1514,15 @@ export class BattleScene extends Phaser.Scene {
|
|||
const hasProtect = event.protectFires?.length > 0;
|
||||
const hasEnfeeble = event.enfeebeFires?.length > 0;
|
||||
const hasJam = event.jamFires?.length > 0;
|
||||
const hasBloodpact = event.bloodpactFires?.length > 0;
|
||||
const hasDrain = event.drainFires?.length > 0;
|
||||
|
||||
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasBloodpact || hasDrain) {
|
||||
this._processBloodpactFires(event.bloodpactFires || [], () => {
|
||||
this._processDrainFires(event.drainFires || [], () => {
|
||||
this._processBuffAnimations(event.buffs || [], () => {
|
||||
this._processSiegeFires(event.siegeFires || [], () => {
|
||||
this._processProtectFires(event.protectFires || [], () => {
|
||||
this._processEnfeebeFires(event.enfeebeFires || [], () => {
|
||||
this._processJamFires(event.jamFires || [], onComplete);
|
||||
});
|
||||
if (hasBuffs || hasSiege || hasProtect || hasEnfeeble || hasJam || hasDrain) {
|
||||
this._processDrainFires(event.drainFires || [], () => {
|
||||
this._processBuffAnimations(event.buffs || [], () => {
|
||||
this._processSiegeFires(event.siegeFires || [], () => {
|
||||
this._processProtectFires(event.protectFires || [], () => {
|
||||
this._processEnfeebeFires(event.enfeebeFires || [], () => {
|
||||
this._processJamFires(event.jamFires || [], onComplete);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1594,26 +1592,84 @@ export class BattleScene extends Phaser.Scene {
|
|||
next(0);
|
||||
}
|
||||
|
||||
_animateBloodpactFire(bloodpactFire, onComplete) {
|
||||
_animateBloodpactFire(attacker, bloodpactFire, onComplete) {
|
||||
const _lookup = id => id && (this.cardObjects.get(id) || this.commanderObjects?.get(id));
|
||||
const sourceObj = _lookup(bloodpactFire.source.instanceId);
|
||||
const sourceObj = _lookup(attacker.instanceId);
|
||||
if (!sourceObj?.scene) { onComplete(); return; }
|
||||
|
||||
this.statusText.setText(`${bloodpactFire.source.name} blood pact: -${bloodpactFire.value} HP, +${bloodpactFire.value} ATK!`);
|
||||
const atkGain = bloodpactFire.value;
|
||||
const hpCost = bloodpactFire.hpCost ?? bloodpactFire.value;
|
||||
this.statusText.setText(`${attacker.name} blood pact: -${hpCost} HP, +${atkGain} ATK!`);
|
||||
this.sound.play('sfx_damage', { volume: 0.6 });
|
||||
|
||||
// Red flash for HP loss
|
||||
// Bloodpact sprite (frame 24) overlaid on the card
|
||||
const BASE_SCALE = 160 / 460;
|
||||
const sprite = this.add.sprite(sourceObj.x, sourceObj.y, 'attacks', 24)
|
||||
.setScale(BASE_SCALE)
|
||||
.setDepth(35)
|
||||
.setAlpha(1);
|
||||
|
||||
// Red glow around the sprite
|
||||
if (sprite.postFX) {
|
||||
sprite.postFX.addGlow(0xff0000, 10, 0, false, 0.2, 16);
|
||||
}
|
||||
|
||||
// Red flash on card
|
||||
sourceObj.flash(0xff0000);
|
||||
this.time.delayedCall(400, () => {
|
||||
if (!sourceObj.scene) { onComplete(); return; }
|
||||
|
||||
// Pulse the sprite larger and back 3 times while stats animate
|
||||
const pulseScale = BASE_SCALE * 1.6;
|
||||
this.tweens.chain({
|
||||
targets: sprite,
|
||||
tweens: [
|
||||
{ scaleX: pulseScale, scaleY: pulseScale, duration: 180, ease: 'Quad.Out' },
|
||||
{ scaleX: BASE_SCALE, scaleY: BASE_SCALE, duration: 180, ease: 'Quad.In' },
|
||||
{ scaleX: pulseScale, scaleY: pulseScale, duration: 180, ease: 'Quad.Out' },
|
||||
{ scaleX: BASE_SCALE, scaleY: BASE_SCALE, duration: 180, ease: 'Quad.In' },
|
||||
{ scaleX: pulseScale, scaleY: pulseScale, duration: 180, ease: 'Quad.Out' },
|
||||
{ scaleX: BASE_SCALE, scaleY: BASE_SCALE, duration: 180, ease: 'Quad.In' }
|
||||
]
|
||||
});
|
||||
|
||||
// After first pulse, refresh stats and show floating numbers
|
||||
this.time.delayedCall(200, () => {
|
||||
if (!sourceObj.scene) return;
|
||||
sourceObj.refresh();
|
||||
// Check if card died from bloodpact
|
||||
if (bloodpactFire.source.currentHP <= 0) {
|
||||
this._playCardDeath(sourceObj, bloodpactFire.source, onComplete);
|
||||
} else {
|
||||
// Show ATK gain animation
|
||||
sourceObj.animateBerserkGain(bloodpactFire.value, onComplete);
|
||||
}
|
||||
|
||||
// Floating -HP (red, offset left)
|
||||
const hpLabel = this.add.text(sourceObj.x - 28, sourceObj.y + 20, `-${hpCost} HP`, {
|
||||
fontSize: '16px', color: '#ff4444', fontStyle: 'bold', fontFamily: 'Audiowide',
|
||||
stroke: '#000000', strokeThickness: 3
|
||||
}).setOrigin(0.5).setDepth(50);
|
||||
this.tweens.add({ targets: hpLabel, y: hpLabel.y - 50, alpha: 0, duration: 700, ease: 'Power2',
|
||||
onComplete: () => hpLabel.destroy() });
|
||||
|
||||
// Floating +ATK (orange, offset right)
|
||||
const atkLabel = this.add.text(sourceObj.x + 28, sourceObj.y + 20, `+${atkGain} ATK`, {
|
||||
fontSize: '16px', color: '#ffaa00', fontStyle: 'bold', fontFamily: 'Audiowide',
|
||||
stroke: '#000000', strokeThickness: 3
|
||||
}).setOrigin(0.5).setDepth(50);
|
||||
this.tweens.add({ targets: atkLabel, y: atkLabel.y - 50, alpha: 0, duration: 700, ease: 'Power2',
|
||||
onComplete: () => atkLabel.destroy() });
|
||||
});
|
||||
|
||||
// Fade sprite out after pulses complete (~1.1s), then resolve
|
||||
this.time.delayedCall(1100, () => {
|
||||
if (!sprite.scene) return;
|
||||
this.tweens.add({
|
||||
targets: sprite,
|
||||
alpha: 0,
|
||||
duration: 250,
|
||||
onComplete: () => {
|
||||
if (sprite.scene) sprite.destroy();
|
||||
if (!sourceObj.scene) { onComplete(); return; }
|
||||
if (attacker.currentHP <= 0) {
|
||||
this._playCardDeath(sourceObj, attacker, onComplete);
|
||||
} else {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export class FusionScene extends Phaser.Scene {
|
|||
|
||||
preload() {
|
||||
this.load.video('forge_idle', 'assets/video/forge.mp4');
|
||||
this.load.video('forge_activate', 'assets/video/forge2.mp4');
|
||||
this.load.video('corridors', 'assets/video/corridors.mp4');
|
||||
}
|
||||
|
||||
|
|
@ -132,13 +133,21 @@ export class FusionScene extends Phaser.Scene {
|
|||
this._videoRect = this.add.rectangle(VIDEO_CX, VIDEO_CY, VIDEO_W, VIDEO_H, 0x0a0a1a)
|
||||
.setStrokeStyle(2, 0x553300);
|
||||
|
||||
// Looping forge video
|
||||
// Looping idle forge video
|
||||
this._forgeVideo = this.add.video(VIDEO_CX, VIDEO_CY, 'forge_idle');
|
||||
this._forgeVideo.setMute(true);
|
||||
this._forgeVideo.on('created', () => {
|
||||
if (this._forgeVideo.scene) this._forgeVideo.setDisplaySize(VIDEO_W, VIDEO_H);
|
||||
});
|
||||
this._forgeVideo.play(true);
|
||||
|
||||
// One-shot fusion activation video (hidden until fuse)
|
||||
this._forgeActivateVideo = this.add.video(VIDEO_CX, VIDEO_CY, 'forge_activate');
|
||||
this._forgeActivateVideo.setMute(true);
|
||||
this._forgeActivateVideo.on('created', () => {
|
||||
if (this._forgeActivateVideo.scene) this._forgeActivateVideo.setDisplaySize(VIDEO_W, VIDEO_H);
|
||||
});
|
||||
this._forgeActivateVideo.setVisible(false);
|
||||
}
|
||||
|
||||
// ── Preview Panel ──────────────────────────────────────────────────────────
|
||||
|
|
@ -633,23 +642,211 @@ export class FusionScene extends Phaser.Scene {
|
|||
}
|
||||
|
||||
_playFusionAnimation(onComplete) {
|
||||
// Placeholder animation: flash and pulse the video rect
|
||||
this.tweens.chain({
|
||||
targets: this._videoRect,
|
||||
tweens: [
|
||||
{ fillColor: 0xffffff, duration: 150 },
|
||||
{ fillColor: 0xffd700, duration: 200 },
|
||||
{ fillColor: 0x0a0a1a, duration: 200 },
|
||||
{ fillColor: 0xffd700, duration: 200 },
|
||||
{ fillColor: 0x0a0a1a, duration: 200 },
|
||||
{ fillColor: 0xffd700, duration: 200 },
|
||||
{ fillColor: 0x0a0a1a, duration: 200 }
|
||||
],
|
||||
onComplete: () => {
|
||||
this.cameras.main.shake(200, 0.005);
|
||||
this.time.delayedCall(300, onComplete);
|
||||
}
|
||||
const FACTION_COLORS = {
|
||||
imperial: 0x2244aa, raider: 0xaa2222, bloodthirsty: 0x882244,
|
||||
xeno: 0x22aa44, righteous: 0xaaaa22
|
||||
};
|
||||
const factionColor = FACTION_COLORS[this._selectedStack?.cardDef?.faction] || 0xffd700;
|
||||
let completed = false;
|
||||
const finish = () => {
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
this._cleanupVortexEffects();
|
||||
// Swap back to idle video
|
||||
this._forgeActivateVideo.setVisible(false);
|
||||
this._forgeVideo.setVisible(true);
|
||||
this._forgeVideo.play(true);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
// Swap to activation video
|
||||
this._forgeVideo.stop();
|
||||
this._forgeVideo.setVisible(false);
|
||||
this._forgeActivateVideo.setVisible(true);
|
||||
this._forgeActivateVideo.play(false);
|
||||
this._forgeActivateVideo.once('complete', finish);
|
||||
// Safety timeout in case video complete event doesn't fire
|
||||
this.time.delayedCall(3200, finish);
|
||||
|
||||
// Start energy vortex effects simultaneously
|
||||
this._startVortexEffects(factionColor);
|
||||
}
|
||||
|
||||
// ── Energy Vortex Effects ─────────────────────────────────────────────────
|
||||
|
||||
_startVortexEffects(color) {
|
||||
const { width, height } = this.scale;
|
||||
this._vortexObjects = [];
|
||||
this._vortexEmitters = [];
|
||||
this._vortexTimers = [];
|
||||
|
||||
// Dark overlay
|
||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0)
|
||||
.setDepth(50);
|
||||
this.tweens.add({ targets: overlay, alpha: 0.7, duration: 300 });
|
||||
this._vortexObjects.push(overlay);
|
||||
|
||||
// Forge glow halo
|
||||
const halo = this.add.rectangle(VIDEO_CX, VIDEO_CY, VIDEO_W + 40, VIDEO_H + 40)
|
||||
.setStrokeStyle(6, color, 0.3)
|
||||
.setFillStyle(0x000000, 0)
|
||||
.setDepth(51);
|
||||
this.tweens.add({
|
||||
targets: halo,
|
||||
strokeAlpha: { from: 0.3, to: 0.9 },
|
||||
duration: 400, yoyo: true, repeat: 6, ease: 'Sine.InOut'
|
||||
});
|
||||
this._vortexObjects.push(halo);
|
||||
|
||||
// Phase 1: Particle streams from 4 edges converging on forge
|
||||
const edgePositions = [
|
||||
{ x: 0, y: height / 2 }, // left
|
||||
{ x: width, y: height / 2 }, // right
|
||||
{ x: width / 2, y: 0 }, // top
|
||||
{ x: width / 2, y: height } // bottom
|
||||
];
|
||||
|
||||
edgePositions.forEach(pos => {
|
||||
const emitter = this.add.particles(pos.x, pos.y, 'fusion_particle', {
|
||||
speed: { min: 200, max: 400 },
|
||||
lifespan: 1500,
|
||||
scale: { start: 0.6, end: 0 },
|
||||
alpha: { start: 0.8, end: 0.1 },
|
||||
blendMode: 'ADD',
|
||||
frequency: 30,
|
||||
tint: color,
|
||||
moveToX: VIDEO_CX,
|
||||
moveToY: VIDEO_CY,
|
||||
quantity: 2
|
||||
});
|
||||
emitter.setDepth(52);
|
||||
this._vortexEmitters.push(emitter);
|
||||
this._vortexObjects.push(emitter);
|
||||
});
|
||||
|
||||
// Subtle camera shake for entire duration
|
||||
this.cameras.main.shake(3000, 0.002);
|
||||
|
||||
// Phase 2 (at 1s): Lightning bolts + intensify particles
|
||||
const phase2Timer = this.time.delayedCall(1000, () => {
|
||||
// Intensify particle streams
|
||||
this._vortexEmitters.forEach(e => {
|
||||
e.frequency = 15;
|
||||
e.speed = { min: 400, max: 600 };
|
||||
});
|
||||
|
||||
// Start lightning bolt loop
|
||||
this._lightningInterval = this.time.addEvent({
|
||||
delay: 250,
|
||||
repeat: 7,
|
||||
callback: () => this._spawnLightningBolt(color)
|
||||
});
|
||||
this._vortexTimers.push(this._lightningInterval);
|
||||
});
|
||||
this._vortexTimers.push(phase2Timer);
|
||||
|
||||
// Phase 3 (at 2s): Convergence — max particles + stronger shake
|
||||
const phase3Timer = this.time.delayedCall(2000, () => {
|
||||
this._vortexEmitters.forEach(e => {
|
||||
e.frequency = 8;
|
||||
e.speed = { min: 600, max: 800 };
|
||||
});
|
||||
this.cameras.main.shake(1000, 0.008);
|
||||
});
|
||||
this._vortexTimers.push(phase3Timer);
|
||||
|
||||
// Final flash at 2.7s
|
||||
const flashTimer = this.time.delayedCall(2700, () => {
|
||||
const flash = this.add.rectangle(width / 2, height / 2, width, height, 0xffffff, 0)
|
||||
.setDepth(55);
|
||||
this._vortexObjects.push(flash);
|
||||
this.tweens.add({
|
||||
targets: flash,
|
||||
alpha: { from: 0, to: 0.9 },
|
||||
duration: 150,
|
||||
yoyo: true,
|
||||
onComplete: () => {
|
||||
if (flash.scene) flash.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
this._vortexTimers.push(flashTimer);
|
||||
}
|
||||
|
||||
_spawnLightningBolt(color) {
|
||||
const { width, height } = this.scale;
|
||||
const gfx = this.add.graphics().setDepth(53);
|
||||
this._vortexObjects.push(gfx);
|
||||
|
||||
// Random start point on screen edge
|
||||
const edges = [
|
||||
{ x: Math.random() * width, y: 0 },
|
||||
{ x: Math.random() * width, y: height },
|
||||
{ x: 0, y: Math.random() * height },
|
||||
{ x: width, y: Math.random() * height }
|
||||
];
|
||||
const start = edges[Math.floor(Math.random() * edges.length)];
|
||||
const endX = VIDEO_CX + (Math.random() - 0.5) * VIDEO_W * 0.6;
|
||||
const endY = VIDEO_CY + (Math.random() - 0.5) * VIDEO_H * 0.6;
|
||||
|
||||
// Generate jagged path
|
||||
const segments = 8 + Math.floor(Math.random() * 6);
|
||||
const points = [{ x: start.x, y: start.y }];
|
||||
for (let i = 1; i <= segments; i++) {
|
||||
const t = i / segments;
|
||||
const baseX = start.x + (endX - start.x) * t;
|
||||
const baseY = start.y + (endY - start.y) * t;
|
||||
const jitter = (1 - t) * 80;
|
||||
points.push({
|
||||
x: baseX + (Math.random() - 0.5) * jitter,
|
||||
y: baseY + (Math.random() - 0.5) * jitter
|
||||
});
|
||||
}
|
||||
|
||||
// Draw thick faction-colored glow
|
||||
gfx.lineStyle(5, color, 0.5);
|
||||
gfx.beginPath();
|
||||
gfx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < points.length; i++) gfx.lineTo(points[i].x, points[i].y);
|
||||
gfx.strokePath();
|
||||
|
||||
// Draw thin white core
|
||||
gfx.lineStyle(2, 0xffffff, 0.9);
|
||||
gfx.beginPath();
|
||||
gfx.moveTo(points[0].x, points[0].y);
|
||||
for (let i = 1; i < points.length; i++) gfx.lineTo(points[i].x, points[i].y);
|
||||
gfx.strokePath();
|
||||
|
||||
// Fade and destroy
|
||||
this.tweens.add({
|
||||
targets: gfx,
|
||||
alpha: 0,
|
||||
duration: 150,
|
||||
delay: 80,
|
||||
onComplete: () => { if (gfx.scene) gfx.destroy(); }
|
||||
});
|
||||
}
|
||||
|
||||
_cleanupVortexEffects() {
|
||||
// Stop emitters
|
||||
if (this._vortexEmitters) {
|
||||
this._vortexEmitters.forEach(e => { if (e.stop) e.stop(); });
|
||||
this._vortexEmitters = [];
|
||||
}
|
||||
// Cancel timers
|
||||
if (this._vortexTimers) {
|
||||
this._vortexTimers.forEach(t => { if (t.remove) t.remove(); });
|
||||
this._vortexTimers = [];
|
||||
}
|
||||
if (this._lightningInterval) {
|
||||
this._lightningInterval.remove();
|
||||
this._lightningInterval = null;
|
||||
}
|
||||
// Destroy all vortex objects
|
||||
if (this._vortexObjects) {
|
||||
this._vortexObjects.forEach(o => { if (o && o.scene) o.destroy(); });
|
||||
this._vortexObjects = [];
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reveal ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,4 +1,28 @@
|
|||
import { SaveManager } from '../managers/SaveManager.js';
|
||||
import { CardObject } from '../objects/CardObject.js';
|
||||
|
||||
const FACTION_COLORS = {
|
||||
imperial: 0x2244aa,
|
||||
raider: 0xaa2222,
|
||||
bloodthirsty: 0x882244,
|
||||
xeno: 0x22aa44,
|
||||
righteous: 0xaaaa22
|
||||
};
|
||||
|
||||
const RARITY_COLORS = {
|
||||
common: 0x888888,
|
||||
rare: 0x4488ff,
|
||||
epic: 0xaa44ff,
|
||||
legendary: 0xffaa00
|
||||
};
|
||||
|
||||
const FACTION_LABELS = {
|
||||
imperial: 'IMPERIAL',
|
||||
raider: 'RAIDER',
|
||||
bloodthirsty: 'BLOODTHIRSTY',
|
||||
xeno: 'XENO',
|
||||
righteous: 'RIGHTEOUS'
|
||||
};
|
||||
|
||||
export class StoreScene extends Phaser.Scene {
|
||||
constructor() { super('StoreScene'); }
|
||||
|
|
@ -9,8 +33,14 @@ export class StoreScene extends Phaser.Scene {
|
|||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
const save = this.registry.get('save');
|
||||
const packManager = this.registry.get('packManager');
|
||||
this.save = this.registry.get('save');
|
||||
this.packManager = this.registry.get('packManager');
|
||||
this.cardManager = this.registry.get('cardManager');
|
||||
this.campaigns = this.registry.get('campaigns');
|
||||
this.state = 'SHOPPING';
|
||||
this._revealObjects = [];
|
||||
this._emitters = [];
|
||||
this._packElements = [];
|
||||
|
||||
// Video background
|
||||
const bg = this.add.video(width / 2, height / 2, 'corridors');
|
||||
|
|
@ -35,139 +65,498 @@ export class StoreScene extends Phaser.Scene {
|
|||
scanlines.fillStyle(0x000000, 0.22);
|
||||
scanlines.fillRect(0, y, width, 1);
|
||||
}
|
||||
this.add.text(width / 2, 45, 'Card Store', { fontSize: '42px', color: '#d4af37', fontFamily: 'RaiderCrusader' }).setOrigin(0.5);
|
||||
|
||||
// Header
|
||||
this.add.text(width / 2, 45, 'Card Store', {
|
||||
fontSize: '42px', color: '#d4af37', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5);
|
||||
this.add.text(width / 2, 90, 'Buy packs to expand your collection', {
|
||||
fontSize: '20px', color: '#888888', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
this.goldText = this.add.text(width - 20, 20, `Gold: ${save.gold}`, {
|
||||
this.goldText = this.add.text(width - 20, 20, `Gold: ${this.save.gold}`, {
|
||||
fontSize: '24px', color: '#ffd700', fontFamily: 'Audiowide'
|
||||
}).setOrigin(1, 0);
|
||||
|
||||
const packs = packManager.getAllPacks();
|
||||
packs.forEach((pack, i) => {
|
||||
const x = width / 2 + (i - packs.length / 2 + 0.5) * 440;
|
||||
this._renderPack(pack, x, 350);
|
||||
});
|
||||
// Generate particle texture
|
||||
this._generateParticleTexture();
|
||||
|
||||
this.revealArea = [];
|
||||
// Render packs
|
||||
this._renderAllPacks();
|
||||
|
||||
// Back button
|
||||
this._makeBackButton();
|
||||
}
|
||||
|
||||
_renderPack(pack, x, y) {
|
||||
// Pack visual
|
||||
const packBg = this.add.rectangle(x, y, 380, 340, 0x1a3a5c)
|
||||
.setStrokeStyle(3, 0xffd700);
|
||||
// ── Pack Grid ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Pack art (procedural)
|
||||
this.add.rectangle(x, y - 60, 180, 150, 0x0d2240).setStrokeStyle(1, 0x4488ff);
|
||||
this.add.text(x, y - 60, 'PACK', { fontSize: '38px', color: '#4488ff', fontFamily: 'RaiderCrusader' }).setOrigin(0.5);
|
||||
_renderAllPacks() {
|
||||
const packs = this.packManager.getAllPacks();
|
||||
const { width } = this.scale;
|
||||
const packW = 320;
|
||||
const gap = 30;
|
||||
const totalW = packs.length * packW + (packs.length - 1) * gap;
|
||||
const startX = width / 2 - totalW / 2 + packW / 2;
|
||||
|
||||
this.add.text(x, y + 45, pack.name, { fontSize: '24px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
this.add.text(x, y + 75, `${pack.cardCount} cards per pack`, { fontSize: '18px', color: '#aaaaaa', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
this.add.text(x, y + 100, `Cost: ${pack.cost} Gold`, { fontSize: '20px', color: '#ffd700', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
|
||||
// Drop rates display
|
||||
const rateLines = Object.entries(pack.dropRates).map(([r, v]) =>
|
||||
`${r.charAt(0).toUpperCase() + r.slice(1)}: ${Math.floor(v * 100)}%`
|
||||
).join(' ');
|
||||
this.add.text(x, y + 128, rateLines, {
|
||||
fontSize: '14px', color: '#888888', wordWrap: { width: 360 }, align: 'center', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
|
||||
const buyBtn = this.add.rectangle(x, y + 170, 240, 55, 0x224422)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x44aa44);
|
||||
const buyTxt = this.add.text(x, y + 170, 'Buy Pack', { fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
|
||||
buyBtn.on('pointerover', () => buyBtn.setFillStyle(0x336633));
|
||||
buyBtn.on('pointerout', () => buyBtn.setFillStyle(0x224422));
|
||||
buyBtn.on('pointerdown', () => this._buyPack(pack));
|
||||
packs.forEach((pack, i) => {
|
||||
const x = startX + i * (packW + gap);
|
||||
this._renderPack(pack, x, 400);
|
||||
});
|
||||
}
|
||||
|
||||
_buyPack(pack) {
|
||||
const save = this.registry.get('save');
|
||||
const packManager = this.registry.get('packManager');
|
||||
const { width, height } = this.scale;
|
||||
_renderPack(pack, x, y) {
|
||||
const unlocked = this.packManager.isPackUnlocked(pack, this.save, this.campaigns);
|
||||
const factionColor = FACTION_COLORS[pack.faction] || 0x333333;
|
||||
const elements = [];
|
||||
|
||||
if (!SaveManager.spendGold(save, pack.cost)) {
|
||||
// Pack background
|
||||
const packBg = this.add.rectangle(x, y, 300, 440, 0x0a0f1a)
|
||||
.setStrokeStyle(3, unlocked ? factionColor : 0x333333);
|
||||
elements.push(packBg);
|
||||
|
||||
// Placeholder pack art
|
||||
const artBg = this.add.rectangle(x, y - 100, 220, 180, factionColor, unlocked ? 0.8 : 0.2)
|
||||
.setStrokeStyle(2, unlocked ? 0xffffff : 0x444444, unlocked ? 0.3 : 0.1);
|
||||
elements.push(artBg);
|
||||
|
||||
// Faction icon text on art
|
||||
const artLabel = this.add.text(x, y - 115, FACTION_LABELS[pack.faction] || '?', {
|
||||
fontSize: '22px', color: unlocked ? '#ffffff' : '#444444',
|
||||
fontStyle: 'bold', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5);
|
||||
elements.push(artLabel);
|
||||
|
||||
const artSub = this.add.text(x, y - 80, 'BOOSTER', {
|
||||
fontSize: '14px', color: unlocked ? '#aaaaaa' : '#333333', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
elements.push(artSub);
|
||||
|
||||
// Decorative lines on art
|
||||
const artGfx = this.add.graphics();
|
||||
const lineColor = unlocked ? factionColor : 0x333333;
|
||||
const lineAlpha = unlocked ? 0.5 : 0.15;
|
||||
artGfx.lineStyle(1, lineColor, lineAlpha);
|
||||
artGfx.strokeRect(x - 100, y - 180, 200, 160);
|
||||
artGfx.lineStyle(1, lineColor, lineAlpha * 0.5);
|
||||
artGfx.strokeRect(x - 90, y - 170, 180, 140);
|
||||
elements.push(artGfx);
|
||||
|
||||
// Pack name
|
||||
const nameText = this.add.text(x, y + 10, pack.name, {
|
||||
fontSize: '18px', color: unlocked ? '#ffffff' : '#555555',
|
||||
fontFamily: 'Audiowide', align: 'center',
|
||||
wordWrap: { width: 280 }
|
||||
}).setOrigin(0.5);
|
||||
elements.push(nameText);
|
||||
|
||||
// Card count
|
||||
const countText = this.add.text(x, y + 40, `${pack.slots.length} cards per pack`, {
|
||||
fontSize: '14px', color: unlocked ? '#aaaaaa' : '#444444', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
elements.push(countText);
|
||||
|
||||
// Cost
|
||||
const costText = this.add.text(x, y + 65, `Cost: ${pack.cost} Gold`, {
|
||||
fontSize: '18px', color: unlocked ? '#ffd700' : '#554400', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
elements.push(costText);
|
||||
|
||||
// Special slot indicator
|
||||
const specialCount = pack.slots.filter(s => s.special).length;
|
||||
if (specialCount > 0) {
|
||||
const specialText = this.add.text(x, y + 90,
|
||||
`${specialCount} special chance card${specialCount > 1 ? 's' : ''}`, {
|
||||
fontSize: '13px', color: unlocked ? '#ffd700' : '#443300', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
elements.push(specialText);
|
||||
}
|
||||
|
||||
if (unlocked) {
|
||||
// Buy button
|
||||
const btnBg = this.add.rectangle(x, y + 170, 240, 55, 0x224422)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(2, 0x44aa44);
|
||||
const btnText = this.add.text(x, y + 170, 'Buy Pack', {
|
||||
fontSize: '20px', color: '#44ff44', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
btnBg.on('pointerover', () => btnBg.setFillStyle(0x336633));
|
||||
btnBg.on('pointerout', () => btnBg.setFillStyle(0x224422));
|
||||
btnBg.on('pointerdown', () => {
|
||||
if (this.state !== 'SHOPPING') return;
|
||||
this._buyPack(pack);
|
||||
});
|
||||
elements.push(btnBg, btnText);
|
||||
} else {
|
||||
// Locked overlay
|
||||
const campaign = this.campaigns.find(c => c.id === pack.unlockCondition);
|
||||
const campaignName = campaign ? campaign.name : 'previous campaign';
|
||||
const lockText = this.add.text(x, y + 145, '🔒 LOCKED', {
|
||||
fontSize: '20px', color: '#aa4444', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
const unlockText = this.add.text(x, y + 175,
|
||||
`Complete "${campaignName}"`, {
|
||||
fontSize: '13px', color: '#666666', fontFamily: 'Audiowide',
|
||||
align: 'center', wordWrap: { width: 260 }
|
||||
}).setOrigin(0.5);
|
||||
elements.push(lockText, unlockText);
|
||||
|
||||
// Dim everything
|
||||
elements.forEach(el => {
|
||||
if (el.setAlpha) el.setAlpha(el.alpha * 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
this._packElements.push(...elements);
|
||||
}
|
||||
|
||||
// ── Purchase + Reveal ─────────────────────────────────────────────────────
|
||||
|
||||
_buyPack(pack) {
|
||||
if (!SaveManager.spendGold(this.save, pack.cost)) {
|
||||
this._showMsg('Not enough gold!', '#ff4444');
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = packManager.openPack(pack.id, save);
|
||||
const cards = this.packManager.openPack(pack.id, this.save);
|
||||
if (!cards || cards.length === 0) {
|
||||
this._showMsg('Pack is empty!', '#ff8800');
|
||||
// Refund
|
||||
this.save.gold += pack.cost;
|
||||
SaveManager.save(this.save);
|
||||
this._showMsg('No cards available in this pack!', '#ff8800');
|
||||
return;
|
||||
}
|
||||
cards.forEach(card => SaveManager.addCard(save, card.id));
|
||||
this.registry.set('save', save);
|
||||
this.goldText.setText(`Gold: ${save.gold}`);
|
||||
|
||||
// Clear previous reveal
|
||||
this.revealArea.forEach(o => o.destroy());
|
||||
this.revealArea = [];
|
||||
// Add cards to collection
|
||||
cards.forEach(card => SaveManager.addCard(this.save, card.id));
|
||||
this.registry.set('save', this.save);
|
||||
this.goldText.setText(`Gold: ${this.save.gold}`);
|
||||
|
||||
// Show revealed cards with animation
|
||||
const totalW = cards.length * 300 + (cards.length - 1) * 20;
|
||||
const startX = width / 2 - totalW / 2 + 140;
|
||||
const rarityColors = { common: 0x888888, rare: 0x4488ff, epic: 0xaa44ff, legendary: 0xffaa00 };
|
||||
const factionColors = { imperial: 0x2244aa, raider: 0xaa2222 };
|
||||
// Sort: non-special left, special right
|
||||
cards.sort((a, b) => (a._slotSpecial ? 1 : 0) - (b._slotSpecial ? 1 : 0));
|
||||
|
||||
// Label
|
||||
const label = this.add.text(width / 2, 700, 'You received:', {
|
||||
fontSize: '22px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
this.revealArea.push(label);
|
||||
this.tweens.add({ targets: label, alpha: 1, duration: 300 });
|
||||
this.state = 'REVEALING';
|
||||
this._startReveal(cards, pack);
|
||||
}
|
||||
|
||||
_startReveal(cards, pack) {
|
||||
const { width, height } = this.scale;
|
||||
const CARD_W = 260;
|
||||
const CARD_H = 357;
|
||||
const GAP = 50;
|
||||
const totalW = cards.length * CARD_W + (cards.length - 1) * GAP;
|
||||
const startX = width / 2 - totalW / 2 + CARD_W / 2;
|
||||
const cardY = height / 2 - 40;
|
||||
const factionColor = FACTION_COLORS[pack.faction] || 0x333333;
|
||||
|
||||
// Dark overlay
|
||||
const overlay = this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0)
|
||||
.setDepth(100).setInteractive();
|
||||
this.tweens.add({ targets: overlay, alpha: 0.9, duration: 300 });
|
||||
this._revealObjects.push(overlay);
|
||||
|
||||
// "Opening pack..." text
|
||||
const openText = this.add.text(width / 2, cardY - CARD_H / 2 - 60,
|
||||
`Opening ${pack.name}...`, {
|
||||
fontSize: '32px', color: '#ffd700', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(103).setAlpha(0);
|
||||
this.tweens.add({ targets: openText, alpha: 1, duration: 300 });
|
||||
this._revealObjects.push(openText);
|
||||
|
||||
// Create card backs
|
||||
const cardBacks = [];
|
||||
const cardFronts = [];
|
||||
|
||||
cards.forEach((card, i) => {
|
||||
const x = startX + i * 320;
|
||||
const y = 860;
|
||||
const cx = startX + i * (CARD_W + GAP);
|
||||
|
||||
const bg = this.add.rectangle(x, y, 280, 210, factionColors[card.faction] || 0x1a3a5c)
|
||||
.setStrokeStyle(3, rarityColors[card.rarity] || 0x888888)
|
||||
.setAlpha(0);
|
||||
const nameT = this.add.text(x, y - 70, card.name, {
|
||||
fontSize: '20px', color: '#ffffff', wordWrap: { width: 260 }, align: 'center', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
const rarityT = this.add.text(x, y - 30, card.rarity.toUpperCase(), {
|
||||
fontSize: '16px',
|
||||
color: '#' + (rarityColors[card.rarity] || 0x888888).toString(16).padStart(6, '0'),
|
||||
fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
const lv1 = card.levels[0];
|
||||
const statsT = this.add.text(x, y + 4, `ATK:${lv1.attack} HP:${lv1.health} ARM:${lv1.armor}`, {
|
||||
fontSize: '16px', color: '#aaaaff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
const skillT = this.add.text(x, y + 34, lv1.skills.map(s => s.name).join(', '), {
|
||||
fontSize: '14px', color: '#ffcc44', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
const newT = this.add.text(x, y + 65, (save.collection[card.id] || 0) === 1 ? 'NEW!' : `x${save.collection[card.id]}`, {
|
||||
fontSize: '16px', color: (save.collection[card.id] || 0) === 1 ? '#44ff44' : '#aaaaaa', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0);
|
||||
// Card back container
|
||||
const backContainer = this.add.container(cx, cardY).setDepth(101);
|
||||
|
||||
this.revealArea.push(bg, nameT, rarityT, statsT, skillT, newT);
|
||||
// Back rectangle
|
||||
const backRect = this.add.rectangle(0, 0, CARD_W, CARD_H, 0x1a1a2e)
|
||||
.setStrokeStyle(3, factionColor);
|
||||
backContainer.add(backRect);
|
||||
|
||||
// Cross-hatch pattern
|
||||
const hatch = this.add.graphics();
|
||||
const hatchAlpha = 0.15;
|
||||
hatch.lineStyle(1, factionColor, hatchAlpha);
|
||||
for (let d = -CARD_H; d < CARD_W + CARD_H; d += 18) {
|
||||
hatch.lineBetween(
|
||||
Math.max(-CARD_W / 2, d - CARD_H / 2), Math.max(-CARD_H / 2, -d + CARD_W / 2 - CARD_H),
|
||||
Math.min(CARD_W / 2, d + CARD_H / 2), Math.min(CARD_H / 2, -d + CARD_W / 2 + CARD_H)
|
||||
);
|
||||
}
|
||||
backContainer.add(hatch);
|
||||
|
||||
// Center emblem
|
||||
const emblemCircle = this.add.circle(0, -15, 50, factionColor, 0.4)
|
||||
.setStrokeStyle(3, factionColor, 0.8);
|
||||
backContainer.add(emblemCircle);
|
||||
const questionMark = this.add.text(0, -18, '?', {
|
||||
fontSize: '52px', color: '#ffffff', fontFamily: 'RaiderCrusader'
|
||||
}).setOrigin(0.5).setAlpha(0.7);
|
||||
backContainer.add(questionMark);
|
||||
|
||||
// Faction label on back
|
||||
const backLabel = this.add.text(0, 90, FACTION_LABELS[pack.faction] || '', {
|
||||
fontSize: '16px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setAlpha(0.4);
|
||||
backContainer.add(backLabel);
|
||||
|
||||
// Special golden border
|
||||
if (card._slotSpecial) {
|
||||
const goldBorder = this.add.rectangle(0, 0, CARD_W + 8, CARD_H + 8)
|
||||
.setStrokeStyle(4, 0xffd700).setFillStyle(0x000000, 0);
|
||||
backContainer.addAt(goldBorder, 0);
|
||||
this.tweens.add({
|
||||
targets: goldBorder,
|
||||
strokeAlpha: { from: 0.5, to: 1 },
|
||||
duration: 600, yoyo: true, repeat: -1, ease: 'Sine.InOut'
|
||||
});
|
||||
}
|
||||
|
||||
// Entrance animation
|
||||
backContainer.setAlpha(0).setScale(0.8);
|
||||
this.tweens.add({
|
||||
targets: [bg, nameT, rarityT, statsT, skillT, newT],
|
||||
alpha: 1, duration: 400, delay: i * 350,
|
||||
ease: 'Power2'
|
||||
targets: backContainer,
|
||||
alpha: 1, scaleX: 1, scaleY: 1,
|
||||
duration: 300, delay: i * 100, ease: 'Back.Out'
|
||||
});
|
||||
|
||||
cardBacks.push(backContainer);
|
||||
this._revealObjects.push(backContainer);
|
||||
|
||||
// Prepare card front (hidden initially)
|
||||
const save = this.save;
|
||||
const instance = this.cardManager.createInstance(card.id, save.level || 1);
|
||||
const cardObj = new CardObject(this, cx, cardY, instance, {
|
||||
width: CARD_W, height: CARD_H, disableTooltip: true
|
||||
});
|
||||
cardObj.setDepth(101).setAlpha(0);
|
||||
cardObj.scaleX = 0;
|
||||
cardFronts.push({ cardObj, card, x: cx, y: cardY });
|
||||
this._revealObjects.push(cardObj);
|
||||
});
|
||||
|
||||
// Begin sequential reveal after delay
|
||||
const initialDelay = 600 + cards.length * 100;
|
||||
this.time.delayedCall(initialDelay, () => {
|
||||
this._revealCards(cardBacks, cardFronts, 0);
|
||||
});
|
||||
}
|
||||
|
||||
_revealCards(backs, fronts, index) {
|
||||
if (index >= backs.length) {
|
||||
// All revealed — show collect button after delay
|
||||
this.time.delayedCall(400, () => this._showCollectButton());
|
||||
return;
|
||||
}
|
||||
|
||||
const back = backs[index];
|
||||
const { cardObj, card, x, y } = fronts[index];
|
||||
const CARD_W = 260;
|
||||
const CARD_H = 357;
|
||||
|
||||
// Flip: back scaleX → 0
|
||||
this.tweens.add({
|
||||
targets: back,
|
||||
scaleX: 0,
|
||||
duration: 150,
|
||||
ease: 'Quad.In',
|
||||
onComplete: () => {
|
||||
back.setVisible(false);
|
||||
|
||||
// Show front, flip in
|
||||
cardObj.setAlpha(1);
|
||||
this.tweens.add({
|
||||
targets: cardObj,
|
||||
scaleX: 1,
|
||||
duration: 150,
|
||||
ease: 'Quad.Out',
|
||||
onComplete: () => {
|
||||
// Pop effect
|
||||
this.tweens.add({
|
||||
targets: cardObj,
|
||||
scaleX: 1.2, scaleY: 1.2,
|
||||
duration: 150,
|
||||
ease: 'Quad.Out',
|
||||
onComplete: () => {
|
||||
this.tweens.add({
|
||||
targets: cardObj,
|
||||
scaleX: 1, scaleY: 1,
|
||||
duration: 200,
|
||||
ease: 'Back.Out'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Rarity glow
|
||||
const glowColor = RARITY_COLORS[card.rarity] || 0x888888;
|
||||
if (cardObj.postFX) {
|
||||
cardObj.postFX.addGlow(glowColor, 8, 0, false, 0.2, 12);
|
||||
}
|
||||
|
||||
// Particle emitter
|
||||
const hw = CARD_W / 2;
|
||||
const hh = CARD_H / 2;
|
||||
const emitter = this.add.particles(x, y, 'store_particle', {
|
||||
speed: { min: 30, max: 80 },
|
||||
lifespan: 1400,
|
||||
scale: { start: 0.8, end: 0 },
|
||||
alpha: { start: 0.8, end: 0 },
|
||||
blendMode: 'ADD',
|
||||
frequency: 50,
|
||||
tint: glowColor,
|
||||
emitZone: {
|
||||
type: 'edge',
|
||||
source: new Phaser.Geom.Rectangle(-hw, -hh, CARD_W, CARD_H),
|
||||
quantity: 48
|
||||
}
|
||||
});
|
||||
emitter.setDepth(102);
|
||||
this._emitters.push(emitter);
|
||||
this._revealObjects.push(emitter);
|
||||
|
||||
// "NEW!" badge
|
||||
const ownedCount = this.save.collection[card.id] || 0;
|
||||
if (ownedCount === 1) {
|
||||
const newBadge = this.add.text(x, y - hh - 25, 'NEW!', {
|
||||
fontSize: '22px', color: '#44ff44', fontStyle: 'bold',
|
||||
stroke: '#000000', strokeThickness: 4, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(103).setAlpha(0);
|
||||
this.tweens.add({ targets: newBadge, alpha: 1, duration: 200 });
|
||||
this._revealObjects.push(newBadge);
|
||||
}
|
||||
|
||||
// Rarity label below card
|
||||
const rarityHex = '#' + (glowColor).toString(16).padStart(6, '0');
|
||||
const rarityLabel = this.add.text(x, y + hh + 18, card.rarity.toUpperCase(), {
|
||||
fontSize: '16px', color: rarityHex, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(103).setAlpha(0);
|
||||
this.tweens.add({ targets: rarityLabel, alpha: 1, duration: 200 });
|
||||
this._revealObjects.push(rarityLabel);
|
||||
|
||||
// Card name below rarity
|
||||
const cardName = this.add.text(x, y + hh + 42, card.name, {
|
||||
fontSize: '16px', color: '#ffffff', fontFamily: 'Audiowide',
|
||||
align: 'center', wordWrap: { width: CARD_W + 20 }
|
||||
}).setOrigin(0.5).setDepth(103).setAlpha(0);
|
||||
this.tweens.add({ targets: cardName, alpha: 1, duration: 200 });
|
||||
this._revealObjects.push(cardName);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Reveal next card after delay
|
||||
this.time.delayedCall(500, () => {
|
||||
this._revealCards(backs, fronts, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
_showCollectButton() {
|
||||
const { width, height } = this.scale;
|
||||
this.state = 'COLLECTING';
|
||||
|
||||
const btnBg = this.add.rectangle(width / 2, height - 80, 320, 65, 0x2a2200)
|
||||
.setStrokeStyle(3, 0xffd700)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setDepth(103).setAlpha(0);
|
||||
const btnText = this.add.text(width / 2, height - 80, 'ADD TO COLLECTION', {
|
||||
fontSize: '22px', color: '#ffd700', fontStyle: 'bold', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(103).setAlpha(0);
|
||||
|
||||
this.tweens.add({ targets: [btnBg, btnText], alpha: 1, duration: 300 });
|
||||
this._revealObjects.push(btnBg, btnText);
|
||||
|
||||
btnBg.on('pointerover', () => btnBg.setFillStyle(0x443300));
|
||||
btnBg.on('pointerout', () => btnBg.setFillStyle(0x2a2200));
|
||||
btnBg.on('pointerdown', () => this._collectCards());
|
||||
}
|
||||
|
||||
_collectCards() {
|
||||
if (this.state !== 'COLLECTING') return;
|
||||
this.state = 'DISMISSING';
|
||||
|
||||
// Find all CardObjects and emitters in reveal objects
|
||||
const cardObjects = this._revealObjects.filter(o => o instanceof CardObject);
|
||||
const nonCards = this._revealObjects.filter(o => !(o instanceof CardObject));
|
||||
|
||||
// Cards fly off screen
|
||||
cardObjects.forEach((card, i) => {
|
||||
this.tweens.add({
|
||||
targets: card,
|
||||
y: -300,
|
||||
scaleX: 0.3, scaleY: 0.3,
|
||||
alpha: 0,
|
||||
duration: 500,
|
||||
delay: i * 100,
|
||||
ease: 'Quad.In'
|
||||
});
|
||||
});
|
||||
|
||||
// Stop emitters
|
||||
this._emitters.forEach(e => {
|
||||
if (e.stop) e.stop();
|
||||
});
|
||||
|
||||
// Fade everything else
|
||||
const flyDuration = 500 + cardObjects.length * 100;
|
||||
this.time.delayedCall(200, () => {
|
||||
nonCards.forEach(obj => {
|
||||
if (obj.setAlpha && obj !== this._revealObjects[0]) {
|
||||
this.tweens.add({ targets: obj, alpha: 0, duration: 300 });
|
||||
}
|
||||
});
|
||||
// Fade overlay
|
||||
this.tweens.add({ targets: this._revealObjects[0], alpha: 0, duration: 400 });
|
||||
});
|
||||
|
||||
// Cleanup after all animations
|
||||
this.time.delayedCall(flyDuration + 200, () => {
|
||||
this._revealObjects.forEach(obj => {
|
||||
if (obj && obj.destroy) obj.destroy();
|
||||
});
|
||||
this._revealObjects = [];
|
||||
this._emitters = [];
|
||||
this.state = 'SHOPPING';
|
||||
this.goldText.setText(`Gold: ${this.save.gold}`);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
_generateParticleTexture() {
|
||||
if (this.textures.exists('store_particle')) return;
|
||||
const gfx = this.make.graphics({ add: false });
|
||||
gfx.fillStyle(0xffffff);
|
||||
gfx.fillCircle(4, 4, 4);
|
||||
gfx.generateTexture('store_particle', 8, 8);
|
||||
gfx.destroy();
|
||||
}
|
||||
|
||||
_showMsg(msg, color) {
|
||||
const { width } = this.scale;
|
||||
const t = this.add.text(width / 2, 660, msg, { fontSize: '24px', color, fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
this.time.delayedCall(2000, () => t.destroy());
|
||||
const t = this.add.text(width / 2, 660, msg, {
|
||||
fontSize: '24px', color, fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5).setDepth(150);
|
||||
this.tweens.add({
|
||||
targets: t, alpha: 0, duration: 500, delay: 1500,
|
||||
onComplete: () => t.destroy()
|
||||
});
|
||||
}
|
||||
|
||||
_makeBackButton() {
|
||||
const bg = this.add.rectangle(80, 1040, 160, 45, 0x333333)
|
||||
.setInteractive({ useHandCursor: true })
|
||||
.setStrokeStyle(1, 0x888888);
|
||||
this.add.text(80, 1040, 'Back', { fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide' }).setOrigin(0.5);
|
||||
bg.on('pointerdown', () => this.scene.start('MainMenuScene'));
|
||||
this.add.text(80, 1040, 'Back', {
|
||||
fontSize: '18px', color: '#ffffff', fontFamily: 'Audiowide'
|
||||
}).setOrigin(0.5);
|
||||
bg.on('pointerdown', () => {
|
||||
if (this.state !== 'SHOPPING') return;
|
||||
this.scene.start('MainMenuScene');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue