Add Xu Province recruit sortie flow
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
export type TerrainType = 'plain' | 'road' | 'forest' | 'hill' | 'village' | 'fort' | 'camp' | 'river' | 'cliff';
|
||||
|
||||
export type UnitClassKey = 'lord' | 'infantry' | 'cavalry' | 'spearman' | 'archer' | 'bandit' | 'yellowTurban' | 'rebelLeader';
|
||||
export type UnitClassKey =
|
||||
| 'lord'
|
||||
| 'infantry'
|
||||
| 'cavalry'
|
||||
| 'spearman'
|
||||
| 'archer'
|
||||
| 'strategist'
|
||||
| 'quartermaster'
|
||||
| 'bandit'
|
||||
| 'yellowTurban'
|
||||
| 'rebelLeader';
|
||||
|
||||
export type TerrainRule = {
|
||||
label: string;
|
||||
@@ -140,6 +150,54 @@ export const unitClasses: Record<UnitClassKey, UnitClassDefinition> = {
|
||||
hill: 1
|
||||
}
|
||||
},
|
||||
strategist: {
|
||||
key: 'strategist',
|
||||
name: '책사',
|
||||
family: '책사계',
|
||||
role: '책략 / 보조',
|
||||
description: '직접 전투력은 낮지만 책략과 지휘 보조로 전장의 흐름을 바꾸는 병과.',
|
||||
terrainRatings: {
|
||||
plain: 95,
|
||||
road: 100,
|
||||
forest: 95,
|
||||
hill: 105,
|
||||
village: 110,
|
||||
fort: 115,
|
||||
camp: 115,
|
||||
river: 0,
|
||||
cliff: 0
|
||||
},
|
||||
movementCosts: {
|
||||
hill: 1,
|
||||
village: 1,
|
||||
fort: 1,
|
||||
camp: 1
|
||||
}
|
||||
},
|
||||
quartermaster: {
|
||||
key: 'quartermaster',
|
||||
name: '군량관',
|
||||
family: '보급계',
|
||||
role: '회복 / 보급',
|
||||
description: '군량과 보급로를 관리해 장기전에 강하고 마을과 진영에서 안정적인 병과.',
|
||||
terrainRatings: {
|
||||
plain: 95,
|
||||
road: 105,
|
||||
forest: 90,
|
||||
hill: 90,
|
||||
village: 115,
|
||||
fort: 105,
|
||||
camp: 120,
|
||||
river: 0,
|
||||
cliff: 0
|
||||
},
|
||||
movementCosts: {
|
||||
road: 1,
|
||||
village: 1,
|
||||
fort: 1,
|
||||
camp: 1
|
||||
}
|
||||
},
|
||||
bandit: {
|
||||
key: 'bandit',
|
||||
name: '도적',
|
||||
|
||||
@@ -2566,6 +2566,51 @@ export const seventhBattleUnits: UnitData[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const xuzhouRecruitUnits: UnitData[] = [
|
||||
{
|
||||
id: 'jian-yong',
|
||||
name: '간옹',
|
||||
faction: 'ally',
|
||||
className: '책사',
|
||||
classKey: 'strategist',
|
||||
level: 4,
|
||||
exp: 12,
|
||||
hp: 26,
|
||||
maxHp: 26,
|
||||
attack: 7,
|
||||
move: 4,
|
||||
stats: { might: 42, intelligence: 78, leadership: 66, agility: 64, luck: 72 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'training-sword', level: 1, exp: 8 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 6 },
|
||||
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||
},
|
||||
x: 3,
|
||||
y: 17
|
||||
},
|
||||
{
|
||||
id: 'mi-zhu',
|
||||
name: '미축',
|
||||
faction: 'ally',
|
||||
className: '군량관',
|
||||
classKey: 'quartermaster',
|
||||
level: 4,
|
||||
exp: 8,
|
||||
hp: 28,
|
||||
maxHp: 28,
|
||||
attack: 7,
|
||||
move: 4,
|
||||
stats: { might: 40, intelligence: 74, leadership: 70, agility: 58, luck: 82 },
|
||||
equipment: {
|
||||
weapon: { itemId: 'training-sword', level: 1, exp: 6 },
|
||||
armor: { itemId: 'cloth-armor', level: 1, exp: 6 },
|
||||
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||
},
|
||||
x: 4,
|
||||
y: 17
|
||||
}
|
||||
];
|
||||
|
||||
export const firstBattleBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__guan-yu',
|
||||
@@ -2593,6 +2638,25 @@ export const firstBattleBonds: BattleBond[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const xuzhouRecruitBonds: BattleBond[] = [
|
||||
{
|
||||
id: 'liu-bei__jian-yong',
|
||||
unitIds: ['liu-bei', 'jian-yong'],
|
||||
title: '오랜 벗',
|
||||
level: 48,
|
||||
exp: 0,
|
||||
description: '간옹은 유비가 흔들릴 때 현실적인 말로 길을 정리해 주는 오랜 벗이다.'
|
||||
},
|
||||
{
|
||||
id: 'liu-bei__mi-zhu',
|
||||
unitIds: ['liu-bei', 'mi-zhu'],
|
||||
title: '서주 후원',
|
||||
level: 42,
|
||||
exp: 0,
|
||||
description: '미축은 서주의 민심과 군량을 묶어 유비군이 오래 버틸 수 있게 돕는다.'
|
||||
}
|
||||
];
|
||||
|
||||
export const secondBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const thirdBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
export const fourthBattleBonds: BattleBond[] = firstBattleBonds.map(cloneBattleBondForScenario);
|
||||
|
||||
@@ -68,6 +68,8 @@ const unitTextureByClass: Partial<Record<UnitClassKey, string>> = {
|
||||
infantry: 'unit-guan-yu',
|
||||
spearman: 'unit-zhang-fei',
|
||||
archer: 'unit-rebel-archer',
|
||||
strategist: 'unit-liu-bei',
|
||||
quartermaster: 'unit-liu-bei',
|
||||
cavalry: 'unit-rebel-cavalry',
|
||||
bandit: 'unit-rebel',
|
||||
yellowTurban: 'unit-rebel',
|
||||
@@ -470,17 +472,22 @@ const usableCatalog: Record<string, BattleUsable> = {
|
||||
const unitStrategyIds: Record<string, string[]> = {
|
||||
'liu-bei': ['aid', 'encourage'],
|
||||
'guan-yu': ['fireTactic'],
|
||||
'zhang-fei': ['roar']
|
||||
'zhang-fei': ['roar'],
|
||||
'jian-yong': ['aid', 'encourage', 'fireTactic'],
|
||||
'mi-zhu': ['aid', 'encourage']
|
||||
};
|
||||
|
||||
const initialItemStocks: Record<string, Record<string, number>> = {
|
||||
'liu-bei': { bean: 2, salve: 1 },
|
||||
'guan-yu': { bean: 1 },
|
||||
'zhang-fei': { bean: 1, wine: 1 }
|
||||
'zhang-fei': { bean: 1, wine: 1 },
|
||||
'jian-yong': { bean: 1 },
|
||||
'mi-zhu': { bean: 2, salve: 1 }
|
||||
};
|
||||
|
||||
const attackRangeByClass: Partial<Record<UnitClassKey, number>> = {
|
||||
archer: 2
|
||||
archer: 2,
|
||||
strategist: 2
|
||||
};
|
||||
|
||||
const enemyAiByUnitId: Record<string, EnemyAiBehavior> = {
|
||||
@@ -846,7 +853,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
private drawUnits() {
|
||||
battleUnits.forEach((unit) => {
|
||||
const sizeRatio = unit.faction === 'ally' ? 1.16 : 1.1;
|
||||
const textureBase = unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel';
|
||||
const textureBase = this.unitTextureKey(unit);
|
||||
const sprite = this.add.sprite(this.tileCenterX(unit.x), this.tileCenterY(unit.y), textureBase, this.unitFrameIndex('south'));
|
||||
sprite.setName(`unit-${unit.id}`);
|
||||
sprite.setDisplaySize(this.layout.tileSize * sizeRatio, this.layout.tileSize * sizeRatio);
|
||||
@@ -4257,7 +4264,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
attackerSprite.setDisplaySize(126, 126);
|
||||
|
||||
const defenderSprite = this.trackCombatObject(
|
||||
this.add.sprite(defenderX, groundY, unitTexture[result.defender.id] ?? 'unit-rebel', this.unitFrameIndex(defenderDirection))
|
||||
this.add.sprite(defenderX, groundY, this.unitTextureKey(result.defender), this.unitFrameIndex(defenderDirection))
|
||||
);
|
||||
defenderSprite.setDepth(depth + 6);
|
||||
defenderSprite.setDisplaySize(126, 126);
|
||||
@@ -4361,7 +4368,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
userSprite.setDepth(depth + 3);
|
||||
userSprite.setDisplaySize(112, 112);
|
||||
|
||||
const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 146, unitTexture[result.target.id] ?? 'unit-rebel', this.unitFrameIndex('west')));
|
||||
const targetSprite = this.trackCombatObject(this.add.sprite(left + panelWidth - 160, top + 146, this.unitTextureKey(result.target), this.unitFrameIndex('west')));
|
||||
targetSprite.setDepth(depth + 3);
|
||||
targetSprite.setDisplaySize(112, 112);
|
||||
|
||||
@@ -4423,7 +4430,7 @@ export class BattleScene extends Phaser.Scene {
|
||||
portrait.setDepth(85);
|
||||
portrait.setDisplaySize(58, 80);
|
||||
} else {
|
||||
const unitIcon = this.trackCombatObject(this.add.sprite(x + 38, y + 58, unitTexture[unit.id] ?? 'unit-rebel', this.unitFrameIndex('south')));
|
||||
const unitIcon = this.trackCombatObject(this.add.sprite(x + 38, y + 58, this.unitTextureKey(unit), this.unitFrameIndex('south')));
|
||||
unitIcon.setDepth(85);
|
||||
unitIcon.setDisplaySize(64, 64);
|
||||
}
|
||||
@@ -5586,8 +5593,12 @@ export class BattleScene extends Phaser.Scene {
|
||||
return unitFrameRows[direction] * 4 + frame;
|
||||
}
|
||||
|
||||
private unitTextureKey(unit: UnitData) {
|
||||
return unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel';
|
||||
}
|
||||
|
||||
private unitActionTexture(unit: UnitData) {
|
||||
const base = unitTexture[unit.id] ?? 'unit-rebel';
|
||||
const base = this.unitTextureKey(unit);
|
||||
const actionKey = `${base}-actions`;
|
||||
return this.textures.exists(actionKey) ? actionKey : base;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import { equipmentExpToNext, equipmentSlotLabels, equipmentSlots, getItem, type
|
||||
import { getUnitClass } from '../data/battleRules';
|
||||
import { defaultBattleScenario } from '../data/battles';
|
||||
import { getSortieFlow } from '../data/campaignFlow';
|
||||
import { firstBattleBonds, firstBattleUnits, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import { firstBattleBonds, firstBattleUnits, xuzhouRecruitBonds, xuzhouRecruitUnits, type PortraitKey, type UnitData } from '../data/scenario';
|
||||
import {
|
||||
applyCampBondExp,
|
||||
ensureCampaignRosterUnits,
|
||||
getCampaignState,
|
||||
getFirstBattleReport,
|
||||
markCampaignStep,
|
||||
@@ -702,6 +703,60 @@ const campDialogues: CampDialogue[] = [
|
||||
rewardExp: 7
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-jian-yong-after-xuzhou',
|
||||
title: '오랜 벗의 조언',
|
||||
availableAfterBattleIds: [campBattleIds.seventh],
|
||||
unitIds: ['liu-bei', 'jian-yong'],
|
||||
bondId: 'liu-bei__jian-yong',
|
||||
rewardExp: 18,
|
||||
lines: [
|
||||
'간옹: 현덕, 서주를 맡는 일은 칼을 드는 일보다 더 무겁네. 백성은 말보다 먼저 밥과 길을 보네.',
|
||||
'유비: 헌화, 내가 그 무게를 감당할 수 있을까 두렵소.',
|
||||
'간옹: 두려움을 아는 사람이면 적어도 함부로 빼앗지는 않겠지. 그러니 먼저 사람을 살피게.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'listen-before-rule',
|
||||
label: '먼저 백성의 말을 듣는다',
|
||||
response: '간옹은 유비가 급히 명분을 세우기보다 서주의 목소리를 먼저 듣겠다고 하자 고개를 끄덕였다.',
|
||||
rewardExp: 8
|
||||
},
|
||||
{
|
||||
id: 'appoint-practical-aide',
|
||||
label: '실무를 맡아 달라 한다',
|
||||
response: '간옹은 투덜대면서도 군영 장부와 사람들의 청원을 살피겠다고 나섰다.',
|
||||
rewardExp: 7
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'liu-mi-zhu-after-xuzhou',
|
||||
title: '서주의 군량',
|
||||
availableAfterBattleIds: [campBattleIds.seventh],
|
||||
unitIds: ['liu-bei', 'mi-zhu'],
|
||||
bondId: 'liu-bei__mi-zhu',
|
||||
rewardExp: 18,
|
||||
lines: [
|
||||
'미축: 유 장군, 서주의 창고는 아직 온전하지만 오래 버티려면 상인과 호족의 마음을 함께 묶어야 합니다.',
|
||||
'유비: 백성에게 더 짐을 지우고 싶지는 않소. 다른 길이 있겠소?',
|
||||
'미축: 사사로운 이익을 줄이고 공적인 신뢰를 세우면 길은 열립니다. 제가 그 일을 맡겠습니다.'
|
||||
],
|
||||
choices: [
|
||||
{
|
||||
id: 'protect-grain',
|
||||
label: '군량을 백성과 나눈다',
|
||||
response: '미축은 군량을 무기로 삼지 않겠다는 유비의 뜻을 확인하고 서주의 상단을 설득하기로 했다.',
|
||||
rewardExp: 8
|
||||
},
|
||||
{
|
||||
id: 'secure-supply-route',
|
||||
label: '보급로 정비를 부탁한다',
|
||||
response: '미축은 곧장 장부를 펼쳐 길목과 창고를 다시 묶는 계획을 세웠다.',
|
||||
rewardExp: 7
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -730,6 +785,7 @@ export class CampScene extends Phaser.Scene {
|
||||
this.visitedTabs = new Set(['status']);
|
||||
this.campaign = getCampaignState();
|
||||
this.report = this.campaign.firstBattleReport ?? getFirstBattleReport() ?? this.createFallbackReport();
|
||||
this.ensureCurrentCampRecruitment();
|
||||
this.selectedUnitId = this.currentUnits().find((unit) => unit.faction === 'ally')?.id ?? 'liu-bei';
|
||||
this.selectedSortieUnitIds = this.normalizedSortieUnitIds(this.campaign.selectedSortieUnitIds);
|
||||
this.selectedDialogueId = this.availableCampDialogues()[0]?.id ?? campDialogues[0].id;
|
||||
@@ -777,6 +833,15 @@ export class CampScene extends Phaser.Scene {
|
||||
};
|
||||
}
|
||||
|
||||
private ensureCurrentCampRecruitment() {
|
||||
if (this.currentCampBattleId() !== campBattleIds.seventh || !this.campaign) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.campaign = ensureCampaignRosterUnits(xuzhouRecruitUnits, xuzhouRecruitBonds);
|
||||
this.report = this.campaign.firstBattleReport ?? this.report;
|
||||
}
|
||||
|
||||
private reportSummary() {
|
||||
if (!this.report) {
|
||||
return '';
|
||||
@@ -947,8 +1012,8 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
this.renderSortieBriefing(x + 34, y + 112, 396, 152, depth + 2);
|
||||
this.renderSortieChecklist(x + 462, y + 112, 404, 170, depth + 2);
|
||||
this.renderSortieUnitSummary(x + 34, y + 292, 832, 148, depth + 2);
|
||||
this.renderSortieRewardHint(x + 34, y + 462, 548, 52, depth + 2);
|
||||
this.renderSortieUnitSummary(x + 34, y + 292, 832, 176, depth + 2);
|
||||
this.renderSortieRewardHint(x + 34, y + 480, 548, 52, depth + 2);
|
||||
|
||||
this.addSortieButton('군영으로', x + width - 298, y + height - 44, 124, () => {
|
||||
soundDirector.playSelect();
|
||||
@@ -1003,16 +1068,18 @@ export class CampScene extends Phaser.Scene {
|
||||
bg.setStrokeStyle(1, palette.blue, 0.48);
|
||||
const allies = this.sortieAllies();
|
||||
const selectedCount = this.selectedSortieUnitIds.length;
|
||||
const rowGap = allies.length > 4 ? 24 : 30;
|
||||
const rowHeight = allies.length > 4 ? 22 : 26;
|
||||
this.trackSortie(
|
||||
this.add.text(x + 18, y + 14, `출전 무장 선택 ${selectedCount}/${Math.min(maxSortieUnits, allies.length)}`, this.textStyle(18, '#f2e3bf', true))
|
||||
).setDepth(depth + 1);
|
||||
this.trackSortie(this.add.text(x + width - 18, y + 17, '클릭해서 출전/대기 전환', this.textStyle(12, '#9fb0bf'))).setOrigin(1, 0).setDepth(depth + 1);
|
||||
|
||||
allies.forEach((unit, index) => {
|
||||
const rowY = y + 48 + index * 30;
|
||||
const rowY = y + 48 + index * rowGap;
|
||||
const selected = this.isSortieSelected(unit.id);
|
||||
const required = requiredSortieUnitIds.has(unit.id);
|
||||
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, 26, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
||||
const row = this.trackSortie(this.add.rectangle(x + 18, rowY - 5, width - 36, rowHeight, selected ? 0x172a22 : 0x151b24, selected ? 0.96 : 0.82));
|
||||
row.setOrigin(0);
|
||||
row.setDepth(depth + 1);
|
||||
row.setStrokeStyle(1, selected ? palette.green : 0x53606c, selected ? 0.62 : 0.38);
|
||||
@@ -1166,11 +1233,15 @@ export class CampScene extends Phaser.Scene {
|
||||
|
||||
private renderUnitColumn() {
|
||||
const allies = this.currentUnits().filter((unit) => unit.faction === 'ally');
|
||||
const availableHeight = 462;
|
||||
const rowGap = allies.length > 1 ? Math.min(150, Math.floor(availableHeight / allies.length)) : 150;
|
||||
const cardHeight = Math.min(126, Math.max(70, rowGap - 10));
|
||||
const compact = cardHeight < 104;
|
||||
allies.forEach((unit, index) => {
|
||||
const x = 42;
|
||||
const y = 120 + index * 150;
|
||||
const y = 120 + index * rowGap;
|
||||
const active = this.selectedUnitId === unit.id;
|
||||
const bg = this.track(this.add.rectangle(x, y, 318, 126, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
||||
const bg = this.track(this.add.rectangle(x, y, 318, cardHeight, active ? 0x25384a : 0x111922, active ? 0.96 : 0.86));
|
||||
bg.setOrigin(0);
|
||||
bg.setStrokeStyle(2, active ? palette.gold : palette.blue, active ? 0.9 : 0.46);
|
||||
bg.setInteractive({ useHandCursor: true });
|
||||
@@ -1181,18 +1252,32 @@ export class CampScene extends Phaser.Scene {
|
||||
});
|
||||
|
||||
const portraitKey = portraitByUnitId[unit.id];
|
||||
const portraitCenterY = y + cardHeight / 2;
|
||||
const portraitSize = compact ? 62 : 92;
|
||||
if (portraitKey) {
|
||||
const portrait = this.track(this.add.image(x + 58, y + 62, portraitKeys[portraitKey]));
|
||||
portrait.setDisplaySize(92, 92);
|
||||
const portrait = this.track(this.add.image(x + 58, portraitCenterY, portraitKeys[portraitKey]));
|
||||
portrait.setDisplaySize(portraitSize, portraitSize);
|
||||
} else {
|
||||
this.renderUnitFallbackPortrait(x + 58, portraitCenterY, compact ? 31 : 44, unit);
|
||||
}
|
||||
|
||||
this.track(this.add.text(x + 116, y + 20, `${unit.name} Lv ${unit.level}`, this.textStyle(21, '#f2e3bf', true)));
|
||||
this.track(this.add.text(x + 116, y + 51, `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(14, '#d4dce6')));
|
||||
this.track(this.add.text(x + 116, y + 78, `경험 ${unit.exp}/100`, this.textStyle(14, '#d8b15f', true)));
|
||||
this.drawBar(x + 116, y + 104, 176, 8, unit.exp / 100, palette.gold);
|
||||
const textX = compact ? x + 102 : x + 116;
|
||||
this.track(this.add.text(textX, y + (compact ? 13 : 20), `${unit.name} Lv ${unit.level}`, this.textStyle(compact ? 17 : 21, '#f2e3bf', true)));
|
||||
this.track(this.add.text(textX, y + (compact ? 38 : 51), `${unit.className} · HP ${unit.hp}/${unit.maxHp}`, this.textStyle(compact ? 12 : 14, '#d4dce6')));
|
||||
this.track(this.add.text(textX, y + (compact ? 58 : 78), `경험 ${unit.exp}/100`, this.textStyle(compact ? 12 : 14, '#d8b15f', true)));
|
||||
this.drawBar(textX, y + cardHeight - 18, compact ? 160 : 176, 8, unit.exp / 100, palette.gold);
|
||||
});
|
||||
}
|
||||
|
||||
private renderUnitFallbackPortrait(x: number, y: number, radius: number, unit: UnitData) {
|
||||
const bg = this.track(this.add.circle(x, y, radius, 0x1f3140, 0.96));
|
||||
bg.setStrokeStyle(2, palette.gold, 0.58);
|
||||
const initial = this.track(this.add.text(x, y - radius * 0.34, unit.name.slice(0, 1), this.textStyle(Math.max(17, Math.floor(radius * 0.86)), '#f2e3bf', true)));
|
||||
initial.setOrigin(0.5);
|
||||
const label = this.track(this.add.text(x, y + radius * 0.34, unit.className, this.textStyle(Math.max(9, Math.floor(radius * 0.3)), '#d4dce6', true)));
|
||||
label.setOrigin(0.5);
|
||||
}
|
||||
|
||||
private renderReportPanel() {
|
||||
const report = this.report;
|
||||
if (!report) {
|
||||
@@ -1234,6 +1319,8 @@ export class CampScene extends Phaser.Scene {
|
||||
if (portraitKey) {
|
||||
const portrait = this.track(this.add.image(x + 84, y + 90, portraitKeys[portraitKey]));
|
||||
portrait.setDisplaySize(112, 112);
|
||||
} else {
|
||||
this.renderUnitFallbackPortrait(x + 84, y + 90, 48, unit);
|
||||
}
|
||||
|
||||
this.track(this.add.text(x + 166, y + 22, `${unit.name} Lv ${unit.level}`, this.textStyle(27, '#f2e3bf', true)));
|
||||
@@ -1280,15 +1367,18 @@ export class CampScene extends Phaser.Scene {
|
||||
this.track(this.add.text(x + 24, y + 56, '출진 전 대화에서 선택지를 고르면 해당 장수들의 공명 경험치가 오릅니다.', this.textStyle(14, '#d4dce6')));
|
||||
|
||||
const dialogues = this.availableCampDialogues();
|
||||
const compactDialogueList = dialogues.length > 3;
|
||||
const dialogueRowGap = compactDialogueList ? 44 : 64;
|
||||
const dialogueRowHeight = compactDialogueList ? 36 : 48;
|
||||
if (!dialogues.some((dialogue) => dialogue.id === this.selectedDialogueId)) {
|
||||
this.selectedDialogueId = dialogues[0]?.id ?? campDialogues[0].id;
|
||||
}
|
||||
|
||||
dialogues.forEach((dialogue, index) => {
|
||||
const completed = this.completedCampDialogues().includes(dialogue.id);
|
||||
const rowY = y + 96 + index * 64;
|
||||
const rowY = y + 96 + index * dialogueRowGap;
|
||||
const selected = this.selectedDialogueId === dialogue.id;
|
||||
const row = this.track(this.add.rectangle(x + 24, rowY, 320, 48, selected ? 0x25384a : 0x151f2a, selected ? 0.96 : 0.86));
|
||||
const row = this.track(this.add.rectangle(x + 24, rowY, 320, dialogueRowHeight, selected ? 0x25384a : 0x151f2a, selected ? 0.96 : 0.86));
|
||||
row.setOrigin(0);
|
||||
row.setStrokeStyle(1, completed ? palette.green : selected ? palette.gold : palette.blue, selected ? 0.72 : 0.46);
|
||||
row.setInteractive({ useHandCursor: true });
|
||||
@@ -1297,9 +1387,9 @@ export class CampScene extends Phaser.Scene {
|
||||
this.selectedDialogueId = dialogue.id;
|
||||
this.render();
|
||||
});
|
||||
this.track(this.add.text(x + 38, rowY + 8, dialogue.title, this.textStyle(15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
||||
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 5 : 8), dialogue.title, this.textStyle(compactDialogueList ? 13 : 15, completed ? '#a8ffd0' : '#f2e3bf', true)));
|
||||
const maxReward = dialogue.rewardExp + Math.max(...dialogue.choices.map((choice) => choice.rewardExp));
|
||||
this.track(this.add.text(x + 38, rowY + 29, `${this.unitName(dialogue.unitIds[0])} · ${this.unitName(dialogue.unitIds[1])} 공명 +${dialogue.rewardExp}~${maxReward}`, this.textStyle(12, '#9fb0bf')));
|
||||
this.track(this.add.text(x + 38, rowY + (compactDialogueList ? 22 : 29), `${this.unitName(dialogue.unitIds[0])} · ${this.unitName(dialogue.unitIds[1])} 공명 +${dialogue.rewardExp}~${maxReward}`, this.textStyle(compactDialogueList ? 10 : 12, '#9fb0bf')));
|
||||
});
|
||||
|
||||
this.renderSelectedDialogue(x + 372, y + 96, 416, 300);
|
||||
@@ -1356,12 +1446,15 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private renderBondList(x: number, y: number, width: number) {
|
||||
const bonds = this.currentBonds();
|
||||
const compact = bonds.length > 3;
|
||||
const rowGap = compact ? 25 : 36;
|
||||
this.track(this.add.text(x, y, '공명', this.textStyle(18, '#f2e3bf', true)));
|
||||
this.currentBonds().forEach((bond, index) => {
|
||||
const rowY = y + 32 + index * 36;
|
||||
bonds.forEach((bond, index) => {
|
||||
const rowY = y + (compact ? 28 : 32) + index * rowGap;
|
||||
const first = this.unitName(bond.unitIds[0]);
|
||||
const second = this.unitName(bond.unitIds[1]);
|
||||
this.track(this.add.text(x, rowY, `${first}·${second} Lv ${bond.level}`, this.textStyle(13, '#d4dce6')));
|
||||
this.track(this.add.text(x, rowY, `${first}·${second} Lv ${bond.level}`, this.textStyle(compact ? 11 : 13, '#d4dce6')));
|
||||
this.drawBar(x + 136, rowY + 6, width - 136, 7, bond.exp / 100, bond.battleExp > 0 ? palette.gold : palette.blue);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -271,6 +271,60 @@ export function applyCampBondExp(dialogueId: string, bondId: string, amount: num
|
||||
return cloneReport(state.firstBattleReport);
|
||||
}
|
||||
|
||||
export function ensureCampaignRosterUnits(units: UnitData[], bonds: BattleBond[] = []) {
|
||||
const state = ensureCampaignState();
|
||||
let changed = false;
|
||||
const existingUnitIds = new Set(state.roster.map((unit) => unit.id));
|
||||
|
||||
units.forEach((unit) => {
|
||||
if (existingUnitIds.has(unit.id)) {
|
||||
return;
|
||||
}
|
||||
state.roster.push(cloneUnit(unit));
|
||||
existingUnitIds.add(unit.id);
|
||||
changed = true;
|
||||
});
|
||||
|
||||
const existingBondIds = new Set(state.bonds.map((bond) => bond.id));
|
||||
bonds.forEach((bond) => {
|
||||
if (existingBondIds.has(bond.id)) {
|
||||
return;
|
||||
}
|
||||
state.bonds.push(createCampBondSnapshot(bond));
|
||||
existingBondIds.add(bond.id);
|
||||
changed = true;
|
||||
});
|
||||
|
||||
if (state.firstBattleReport) {
|
||||
const reportUnitIds = new Set(state.firstBattleReport.units.map((unit) => unit.id));
|
||||
units.forEach((unit) => {
|
||||
if (reportUnitIds.has(unit.id)) {
|
||||
return;
|
||||
}
|
||||
state.firstBattleReport?.units.push(cloneUnit(unit));
|
||||
reportUnitIds.add(unit.id);
|
||||
changed = true;
|
||||
});
|
||||
|
||||
const reportBondIds = new Set(state.firstBattleReport.bonds.map((bond) => bond.id));
|
||||
bonds.forEach((bond) => {
|
||||
if (reportBondIds.has(bond.id)) {
|
||||
return;
|
||||
}
|
||||
state.firstBattleReport?.bonds.push(createCampBondSnapshot(bond));
|
||||
reportBondIds.add(bond.id);
|
||||
changed = true;
|
||||
});
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
state.updatedAt = new Date().toISOString();
|
||||
persistCampaignState(state);
|
||||
}
|
||||
|
||||
return cloneCampaignState(state);
|
||||
}
|
||||
|
||||
function ensureCampaignState() {
|
||||
if (!campaignState) {
|
||||
campaignState = readStoredCampaignState() ?? createInitialCampaignState();
|
||||
@@ -433,3 +487,11 @@ function cloneBondSnapshot(bond: CampBondSnapshot): CampBondSnapshot {
|
||||
unitIds: [...bond.unitIds] as [string, string]
|
||||
};
|
||||
}
|
||||
|
||||
function createCampBondSnapshot(bond: BattleBond): CampBondSnapshot {
|
||||
return {
|
||||
...bond,
|
||||
unitIds: [...bond.unitIds] as [string, string],
|
||||
battleExp: 0
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user