Make camp supplies usable
This commit is contained in:
@@ -203,6 +203,35 @@ try {
|
|||||||
throw new Error(`Expected CampScene report after victory: ${JSON.stringify(campState)}`);
|
throw new Error(`Expected CampScene report after victory: ${JSON.stringify(campState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const campScene = window.__HEROS_GAME__?.scene.getScene('CampScene');
|
||||||
|
const liuBei = campScene?.campaign?.roster?.find((unit) => unit.id === 'liu-bei');
|
||||||
|
if (!liuBei) {
|
||||||
|
throw new Error('Expected Liu Bei in camp roster before supply verification.');
|
||||||
|
}
|
||||||
|
liuBei.hp = Math.max(1, liuBei.maxHp - 10);
|
||||||
|
campScene.report?.units?.forEach((unit) => {
|
||||||
|
if (unit.id === 'liu-bei') {
|
||||||
|
unit.hp = liuBei.hp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
campScene.render();
|
||||||
|
});
|
||||||
|
await page.mouse.click(918, 38);
|
||||||
|
await page.waitForTimeout(160);
|
||||||
|
await page.screenshot({ path: 'dist/verification-camp-supplies.png', fullPage: true });
|
||||||
|
await page.mouse.click(1120, 245);
|
||||||
|
await page.waitForTimeout(260);
|
||||||
|
|
||||||
|
const campStateAfterSupply = await page.evaluate(() => window.__HEROS_DEBUG__?.camp());
|
||||||
|
const liuBeiAfterSupply = campStateAfterSupply?.campaign?.roster?.find((unit) => unit.id === 'liu-bei');
|
||||||
|
if (!liuBeiAfterSupply || liuBeiAfterSupply.hp !== liuBeiAfterSupply.maxHp) {
|
||||||
|
throw new Error(`Expected bean supply to recover Liu Bei: ${JSON.stringify(campStateAfterSupply)}`);
|
||||||
|
}
|
||||||
|
if ((campStateAfterSupply.campaign?.inventory?.['콩'] ?? 0) !== 0) {
|
||||||
|
throw new Error(`Expected bean supply to be consumed: ${JSON.stringify(campStateAfterSupply.campaign?.inventory)}`);
|
||||||
|
}
|
||||||
|
|
||||||
await page.mouse.click(830, 38);
|
await page.mouse.click(830, 38);
|
||||||
await page.waitForTimeout(120);
|
await page.waitForTimeout(120);
|
||||||
await page.mouse.click(974, 482);
|
await page.mouse.click(974, 482);
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ type CampTabButtonView = {
|
|||||||
text: Phaser.GameObjects.Text;
|
text: Phaser.GameObjects.Text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type CampSupplyDefinition = {
|
||||||
|
label: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
healHp: number;
|
||||||
|
bondExp: number;
|
||||||
|
};
|
||||||
|
|
||||||
const portraitKeys: Record<PortraitKey, string> = {
|
const portraitKeys: Record<PortraitKey, string> = {
|
||||||
liuBei: 'portrait-liu-bei',
|
liuBei: 'portrait-liu-bei',
|
||||||
guanYu: 'portrait-guan-yu',
|
guanYu: 'portrait-guan-yu',
|
||||||
@@ -44,6 +52,25 @@ const portraitByUnitId: Record<string, PortraitKey> = {
|
|||||||
'zhang-fei': 'zhangFei'
|
'zhang-fei': 'zhangFei'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const campSupplies: CampSupplyDefinition[] = [
|
||||||
|
{
|
||||||
|
label: '콩',
|
||||||
|
title: '콩',
|
||||||
|
description: '선택 장수의 병력을 12 회복합니다.',
|
||||||
|
healHp: 12,
|
||||||
|
bondExp: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '탁주',
|
||||||
|
title: '탁주',
|
||||||
|
description: '선택 장수의 병력을 8 회복하고 연결된 공명 경험치를 2 올립니다.',
|
||||||
|
healHp: 8,
|
||||||
|
bondExp: 2
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, supply]));
|
||||||
|
|
||||||
const campDialogues: CampDialogue[] = [
|
const campDialogues: CampDialogue[] = [
|
||||||
{
|
{
|
||||||
id: 'liu-guan-after-first-battle',
|
id: 'liu-guan-after-first-battle',
|
||||||
@@ -433,24 +460,39 @@ export class CampScene extends Phaser.Scene {
|
|||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, palette.blue, 0.56);
|
bg.setStrokeStyle(1, palette.blue, 0.56);
|
||||||
this.track(this.add.text(x + 24, y + 22, '정비와 보급', this.textStyle(24, '#f2e3bf', true)));
|
this.track(this.add.text(x + 24, y + 22, '정비와 보급', this.textStyle(24, '#f2e3bf', true)));
|
||||||
this.track(this.add.text(x + 24, y + 58, '획득한 보상과 장비 성장 상태를 확인합니다. 장비 교체와 상점은 다음 확장에서 붙일 수 있습니다.', this.textStyle(14, '#d4dce6')));
|
this.track(this.add.text(x + 24, y + 58, '소모품은 왼쪽에서 선택한 장수에게 사용됩니다.', this.textStyle(14, '#d4dce6')));
|
||||||
|
|
||||||
const rewards = this.inventoryLabels();
|
const unit = this.selectedUnit();
|
||||||
rewards.forEach((reward, index) => {
|
if (unit) {
|
||||||
this.renderSupplyBox(reward, x + 24 + index * 160, y + 104);
|
this.renderSupplyTargetCard(unit, x + 24, y + 94, 342, 86);
|
||||||
|
}
|
||||||
|
|
||||||
|
campSupplies.forEach((supply, index) => {
|
||||||
|
this.renderSupplyUseRow(supply, x + 390, y + 94 + index * 74, 390);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.track(this.add.text(x + 24, y + 190, '부대 장비 요약', this.textStyle(20, '#f2e3bf', true)));
|
this.track(this.add.text(x + 24, y + 202, '전리품과 명성', this.textStyle(19, '#f2e3bf', true)));
|
||||||
|
const trophies = this.nonConsumableInventoryLabels();
|
||||||
|
if (trophies.length === 0) {
|
||||||
|
this.track(this.add.text(x + 24, y + 236, '소모품 외 전리품 없음', this.textStyle(13, '#9fb0bf')));
|
||||||
|
} else {
|
||||||
|
trophies.forEach((reward, index) => {
|
||||||
|
this.renderSupplyBox(reward, x + 24 + index * 154, y + 232);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.track(this.add.text(x + 24, y + 306, '부대 장비 요약', this.textStyle(19, '#f2e3bf', true)));
|
||||||
this.currentUnits()
|
this.currentUnits()
|
||||||
.filter((unit) => unit.faction === 'ally')
|
.filter((unit) => unit.faction === 'ally')
|
||||||
.forEach((unit, index) => {
|
.forEach((unit, index) => {
|
||||||
const rowY = y + 232 + index * 58;
|
const rowY = y + 342 + index * 30;
|
||||||
this.track(this.add.text(x + 24, rowY, unit.name, this.textStyle(17, '#f2e3bf', true)));
|
this.track(this.add.text(x + 24, rowY, unit.name, this.textStyle(14, '#f2e3bf', true)));
|
||||||
equipmentSlots.forEach((slot, slotIndex) => {
|
equipmentSlots.forEach((slot, slotIndex) => {
|
||||||
const state = unit.equipment[slot];
|
const state = unit.equipment[slot];
|
||||||
const item = getItem(state.itemId);
|
const item = getItem(state.itemId);
|
||||||
this.track(this.add.text(x + 110 + slotIndex * 218, rowY, `${equipmentSlotLabels[slot]} ${item.name}`, this.textStyle(13, '#d4dce6')));
|
const slotX = x + 104 + slotIndex * 220;
|
||||||
this.drawBar(x + 110 + slotIndex * 218, rowY + 24, 176, 6, state.exp / equipmentExpToNext(state.level), item.rank === 'treasure' ? palette.gold : palette.blue);
|
this.track(this.add.text(slotX, rowY - 2, `${equipmentSlotLabels[slot]} ${item.name} Lv${state.level}`, this.textStyle(11, '#d4dce6')));
|
||||||
|
this.drawBar(slotX, rowY + 17, 176, 6, state.exp / equipmentExpToNext(state.level), item.rank === 'treasure' ? palette.gold : palette.blue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -462,6 +504,135 @@ export class CampScene extends Phaser.Scene {
|
|||||||
this.track(this.add.text(x + 14, y + 15, label, this.textStyle(16, '#f2e3bf', true)));
|
this.track(this.add.text(x + 14, y + 15, label, this.textStyle(16, '#f2e3bf', true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderSupplyTargetCard(unit: UnitData, x: number, y: number, width: number, height: number) {
|
||||||
|
const bg = this.track(this.add.rectangle(x, y, width, height, 0x0d141c, 0.92));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, palette.gold, 0.48);
|
||||||
|
this.track(this.add.text(x + 16, y + 12, '사용 대상', this.textStyle(15, '#f2e3bf', true)));
|
||||||
|
this.track(this.add.text(x + 16, y + 38, `${unit.name} ${unit.className} Lv ${unit.level}`, this.textStyle(16, '#d4dce6', true)));
|
||||||
|
this.track(this.add.text(x + 16, y + 62, `병력 ${unit.hp}/${unit.maxHp}`, this.textStyle(13, '#9fb0bf', true)));
|
||||||
|
this.drawBar(x + 118, y + 68, width - 138, 8, unit.hp / unit.maxHp, palette.green);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSupplyUseRow(supply: CampSupplyDefinition, x: number, y: number, width: number) {
|
||||||
|
const unit = this.selectedUnit();
|
||||||
|
const amount = this.inventoryAmount(supply.label);
|
||||||
|
const enabled = Boolean(unit && this.canUseSupply(supply, unit));
|
||||||
|
const bg = this.track(this.add.rectangle(x, y, width, 62, enabled ? 0x151f2a : 0x111820, enabled ? 0.92 : 0.76));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.62 : 0.38);
|
||||||
|
|
||||||
|
this.track(this.add.text(x + 14, y + 10, `${supply.title} x${amount}`, this.textStyle(16, amount > 0 ? '#f2e3bf' : '#7f8994', true)));
|
||||||
|
this.track(
|
||||||
|
this.add.text(x + 14, y + 34, supply.description, {
|
||||||
|
...this.textStyle(12, enabled ? '#c8d2dd' : '#77818c'),
|
||||||
|
wordWrap: { width: width - 128, useAdvancedWrap: true }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = this.track(this.add.rectangle(x + width - 54, y + 31, 82, 30, enabled ? 0x1a2630 : 0x121922, enabled ? 0.96 : 0.72));
|
||||||
|
button.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.76 : 0.42);
|
||||||
|
const buttonText = this.track(this.add.text(x + width - 54, y + 31, enabled ? '사용' : '불가', this.textStyle(13, enabled ? '#f2e3bf' : '#7f8994', true)));
|
||||||
|
buttonText.setOrigin(0.5);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
const action = () => this.useSupply(supply);
|
||||||
|
button.setInteractive({ useHandCursor: true });
|
||||||
|
button.on('pointerover', () => button.setFillStyle(0x283947, 0.98));
|
||||||
|
button.on('pointerout', () => button.setFillStyle(0x1a2630, 0.96));
|
||||||
|
button.on('pointerdown', action);
|
||||||
|
buttonText.setInteractive({ useHandCursor: true });
|
||||||
|
buttonText.on('pointerdown', action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private canUseSupply(supply: CampSupplyDefinition, unit: UnitData) {
|
||||||
|
if (this.inventoryAmount(supply.label) <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (supply.healHp > 0 && unit.hp < unit.maxHp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return supply.bondExp > 0 && this.currentBonds().some((bond) => bond.unitIds.includes(unit.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private useSupply(supply: CampSupplyDefinition) {
|
||||||
|
const campaign = this.campaign ?? getCampaignState();
|
||||||
|
const target = campaign.roster.find((unit) => unit.id === this.selectedUnitId);
|
||||||
|
if (!target) {
|
||||||
|
this.showCampNotice('사용할 장수를 선택하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((campaign.inventory[supply.label] ?? 0) <= 0) {
|
||||||
|
this.showCampNotice(`${supply.title}이 없습니다.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousHp = target.hp;
|
||||||
|
target.hp = Math.min(target.maxHp, target.hp + supply.healHp);
|
||||||
|
const hpGain = target.hp - previousHp;
|
||||||
|
const bondGain = this.applySupplyBondExp(campaign, target.id, supply.bondExp);
|
||||||
|
|
||||||
|
if (hpGain <= 0 && bondGain <= 0) {
|
||||||
|
this.showCampNotice(`${target.name}에게 지금은 사용할 필요가 없습니다.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign.inventory[supply.label] -= 1;
|
||||||
|
if (campaign.inventory[supply.label] <= 0) {
|
||||||
|
delete campaign.inventory[supply.label];
|
||||||
|
}
|
||||||
|
this.syncReportUnitHp(campaign, target.id, target.hp);
|
||||||
|
|
||||||
|
this.campaign = saveCampaignState(campaign);
|
||||||
|
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||||
|
soundDirector.playEffect('exp-gain', { volume: 0.24, stopAfterMs: 620 });
|
||||||
|
|
||||||
|
const hpText = hpGain > 0 ? `병력 +${hpGain}` : '';
|
||||||
|
const bondText = bondGain > 0 ? `공명 +${supply.bondExp}` : '';
|
||||||
|
this.showCampNotice(`${target.name} ${supply.title} 사용 · ${[hpText, bondText].filter(Boolean).join(' / ')}`);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySupplyBondExp(campaign: CampaignState, unitId: string, amount: number) {
|
||||||
|
if (amount <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let affected = 0;
|
||||||
|
campaign.bonds
|
||||||
|
.filter((bond) => bond.unitIds.includes(unitId))
|
||||||
|
.forEach((bond) => {
|
||||||
|
this.advanceCampBond(bond, amount);
|
||||||
|
affected += 1;
|
||||||
|
const reportBond = campaign.firstBattleReport?.bonds.find((candidate) => candidate.id === bond.id);
|
||||||
|
if (reportBond) {
|
||||||
|
reportBond.level = bond.level;
|
||||||
|
reportBond.exp = bond.exp;
|
||||||
|
reportBond.battleExp = bond.battleExp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return affected * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private advanceCampBond(bond: CampaignState['bonds'][number], amount: number) {
|
||||||
|
bond.battleExp += amount;
|
||||||
|
bond.exp += amount;
|
||||||
|
while (bond.exp >= 100) {
|
||||||
|
bond.exp -= 100;
|
||||||
|
bond.level = Math.min(100, bond.level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private syncReportUnitHp(campaign: CampaignState, unitId: string, hp: number) {
|
||||||
|
const reportUnit = campaign.firstBattleReport?.units.find((unit) => unit.id === unitId);
|
||||||
|
if (reportUnit) {
|
||||||
|
reportUnit.hp = hp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private renderDetailGauge(label: string, value: string, ratio: number, x: number, y: number, width: number, color: number) {
|
private renderDetailGauge(label: string, value: string, ratio: number, x: number, y: number, width: number, color: number) {
|
||||||
this.track(this.add.text(x, y - 2, label, this.textStyle(13, '#9fb0bf', true)));
|
this.track(this.add.text(x, y - 2, label, this.textStyle(13, '#9fb0bf', true)));
|
||||||
this.drawBar(x + 54, y + 6, width - 134, 8, ratio, color);
|
this.drawBar(x + 54, y + 6, width - 134, 8, ratio, color);
|
||||||
@@ -620,6 +791,23 @@ export class CampScene extends Phaser.Scene {
|
|||||||
return this.report?.itemRewards.length ? this.report.itemRewards : ['콩 1', '탁주 1'];
|
return this.report?.itemRewards.length ? this.report.itemRewards : ['콩 1', '탁주 1'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private nonConsumableInventoryLabels() {
|
||||||
|
const entries = Object.entries(this.campaign?.inventory ?? {}).filter(([label, amount]) => {
|
||||||
|
return amount > 0 && !campSupplyByLabel.has(label);
|
||||||
|
});
|
||||||
|
if (entries.length > 0) {
|
||||||
|
return entries.map(([label, amount]) => `${label} ${amount}`);
|
||||||
|
}
|
||||||
|
return this.inventoryLabels().filter((label) => {
|
||||||
|
const normalized = label.replace(/\s+\d+$/, '').trim();
|
||||||
|
return !campSupplyByLabel.has(normalized);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private inventoryAmount(label: string) {
|
||||||
|
return this.campaign?.inventory[label] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
private drawBar(x: number, y: number, width: number, height: number, ratio: number, color: number) {
|
private drawBar(x: number, y: number, width: number, height: number, ratio: number, color: number) {
|
||||||
const track = this.track(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
|
const track = this.track(this.add.rectangle(x, y, width, height, 0x070b10, 0.86));
|
||||||
track.setOrigin(0);
|
track.setOrigin(0);
|
||||||
@@ -654,11 +842,17 @@ export class CampScene extends Phaser.Scene {
|
|||||||
selectedDialogueId: this.selectedDialogueId,
|
selectedDialogueId: this.selectedDialogueId,
|
||||||
campaign: this.campaign
|
campaign: this.campaign
|
||||||
? {
|
? {
|
||||||
step: this.campaign.step,
|
step: this.campaign.step,
|
||||||
gold: this.campaign.gold,
|
gold: this.campaign.gold,
|
||||||
inventory: this.campaign.inventory,
|
inventory: this.campaign.inventory,
|
||||||
completedCampDialogues: this.campaign.completedCampDialogues
|
roster: this.campaign.roster.map((unit) => ({
|
||||||
}
|
id: unit.id,
|
||||||
|
name: unit.name,
|
||||||
|
hp: unit.hp,
|
||||||
|
maxHp: unit.maxHp
|
||||||
|
})),
|
||||||
|
completedCampDialogues: this.campaign.completedCampDialogues
|
||||||
|
}
|
||||||
: null,
|
: null,
|
||||||
report: this.report
|
report: this.report
|
||||||
? {
|
? {
|
||||||
|
|||||||
Reference in New Issue
Block a user