feat: deepen audiovisual story immersion
This commit is contained in:
@@ -981,15 +981,24 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
nextHints: ['형주 방위', '익주 진입', '성도 항복']
|
||||
},
|
||||
{
|
||||
id: 'shu-han',
|
||||
id: 'hanzhong-shu-han',
|
||||
title: '촉한 건국',
|
||||
period: '한실을 잇는 깃발',
|
||||
description: '여러 전장을 지나 촉한을 세우고 북벌을 바라보는 장기 목표입니다.',
|
||||
period: '한중에서 나라로',
|
||||
description: '가맹관에서 한중 결전까지 전선을 밀어 올리고, 유랑군의 깃발을 촉한의 국호로 세우는 장입니다.',
|
||||
battleIds: [
|
||||
'thirty-fourth-battle-jiameng-pass',
|
||||
'thirty-fifth-battle-yangping-scout',
|
||||
'thirty-sixth-battle-dingjun-vanguard',
|
||||
'thirty-seventh-battle-hanzhong-decisive',
|
||||
'thirty-seventh-battle-hanzhong-decisive'
|
||||
],
|
||||
nextHints: ['가맹관 돌파', '정군산 결전', '한중왕 선언', '촉한 건국']
|
||||
},
|
||||
{
|
||||
id: 'jingzhou-yiling',
|
||||
title: '형주 붕괴와 이릉',
|
||||
period: '잃은 땅과 남은 뜻',
|
||||
description: '번성의 공세에서 형주 후방의 붕괴, 맥성의 고립과 이릉의 화공까지 이어지는 비극의 장입니다.',
|
||||
battleIds: [
|
||||
'thirty-eighth-battle-jing-defense',
|
||||
'thirty-ninth-battle-fan-castle-vanguard',
|
||||
'fortieth-battle-han-river-flood',
|
||||
@@ -998,7 +1007,16 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
'forty-third-battle-gongan-collapse',
|
||||
'forty-fourth-battle-maicheng-isolation',
|
||||
'forty-fifth-battle-yiling-vanguard',
|
||||
'forty-sixth-battle-yiling-fire',
|
||||
'forty-sixth-battle-yiling-fire'
|
||||
],
|
||||
nextHints: ['번성 포위', '형주 후방 위기', '맥성 고립', '이릉 화공', '백제성의 유언']
|
||||
},
|
||||
{
|
||||
id: 'nanzhong',
|
||||
title: '남중 평정',
|
||||
period: '칠종칠금',
|
||||
description: '힘으로 꺾는 데서 멈추지 않고, 맹획의 마음과 남중의 신뢰를 얻어 북벌의 후방을 다지는 장입니다.',
|
||||
battleIds: [
|
||||
'forty-seventh-battle-nanzhong-stabilization',
|
||||
'forty-eighth-battle-meng-huo-main-force',
|
||||
'forty-ninth-battle-meng-huo-second-capture',
|
||||
@@ -1008,7 +1026,7 @@ const campaignTimelineChapters: CampaignTimelineChapter[] = [
|
||||
'fifty-third-battle-meng-huo-sixth-capture',
|
||||
'fifty-fourth-battle-meng-huo-final-capture'
|
||||
],
|
||||
nextHints: ['왕업 선언', '번성 포위', '형주 위기', '이릉 화공', '남중 안정', '맹획 회유', '칠종칠금']
|
||||
nextHints: ['남중 안정', '맹획 회유', '칠종칠금', '북벌 후방 확보']
|
||||
},
|
||||
{
|
||||
id: 'northern-campaign',
|
||||
@@ -21974,22 +21992,26 @@ export class CampScene extends Phaser.Scene {
|
||||
const completedIds = this.completedBattleIds();
|
||||
const listX = x + 24;
|
||||
const listY = y + 142;
|
||||
const compactChapterList = campaignTimelineChapters.length > 9;
|
||||
const chapterRowGap = compactChapterList ? 22 : 31;
|
||||
const chapterRowHeight = compactChapterList ? 20 : 27;
|
||||
this.track(this.add.text(listX, listY, '큰 흐름', this.textStyle(17, '#f2e3bf', true)));
|
||||
campaignTimelineChapters.forEach((chapter, index) => {
|
||||
const status = this.timelineChapterStatus(chapter, index, progress.activeChapterIndex, completedIds);
|
||||
const rowY = listY + 30 + index * 31;
|
||||
const rowY = listY + 30 + index * chapterRowGap;
|
||||
const active = index === progress.activeChapterIndex;
|
||||
const complete = status === '완료';
|
||||
const row = this.track(this.add.rectangle(listX, rowY, 360, 27, active ? 0x25384a : complete ? 0x17231d : 0x151f2a, active ? 0.96 : 0.84));
|
||||
const row = this.track(this.add.rectangle(listX, rowY, 360, chapterRowHeight, active ? 0x25384a : complete ? 0x17231d : 0x151f2a, active ? 0.96 : 0.84));
|
||||
row.setOrigin(0);
|
||||
row.setStrokeStyle(1, active ? palette.gold : complete ? palette.green : palette.blue, active ? 0.7 : 0.34);
|
||||
const markerColor = active ? '#fff2b8' : complete ? '#a8ffd0' : '#9fb0bf';
|
||||
this.track(this.add.text(listX + 12, rowY + 5, status, this.textStyle(11, markerColor, true)));
|
||||
this.track(this.add.text(listX + 64, rowY + 4, chapter.title, this.textStyle(13, active ? '#f2e3bf' : '#d4dce6', active)));
|
||||
const textY = rowY + (compactChapterList ? 2 : 5);
|
||||
this.track(this.add.text(listX + 12, textY, status, this.textStyle(compactChapterList ? 10 : 11, markerColor, true)));
|
||||
this.track(this.add.text(listX + 64, rowY + (compactChapterList ? 1 : 4), chapter.title, this.textStyle(compactChapterList ? 11 : 13, active ? '#f2e3bf' : '#d4dce6', active)));
|
||||
const countText = chapter.battleIds.length > 0
|
||||
? `${chapter.battleIds.filter((id) => completedIds.has(id)).length}/${chapter.battleIds.length}`
|
||||
: '예정';
|
||||
const count = this.track(this.add.text(listX + 342, rowY + 5, countText, this.textStyle(11, markerColor, true)));
|
||||
const count = this.track(this.add.text(listX + 342, textY, countText, this.textStyle(compactChapterList ? 10 : 11, markerColor, true)));
|
||||
count.setOrigin(1, 0);
|
||||
});
|
||||
|
||||
@@ -22037,7 +22059,29 @@ export class CampScene extends Phaser.Scene {
|
||||
this.drawBar(x + 18, y + 162, width - 36, 8, ratio, status === '완료' ? palette.green : palette.gold);
|
||||
|
||||
if (chapter.battleIds.length > 0) {
|
||||
chapter.battleIds.forEach((battleId, index) => {
|
||||
const visibleBattleLimit = 4;
|
||||
const firstPendingBattleIndex = chapter.battleIds.findIndex((battleId) => !completedIds.has(battleId));
|
||||
const focusBattleIndex = firstPendingBattleIndex >= 0 ? firstPendingBattleIndex : chapter.battleIds.length - 1;
|
||||
const visibleBattleStart = Phaser.Math.Clamp(
|
||||
focusBattleIndex - 1,
|
||||
0,
|
||||
Math.max(0, chapter.battleIds.length - visibleBattleLimit)
|
||||
);
|
||||
const visibleBattleIds = chapter.battleIds.slice(visibleBattleStart, visibleBattleStart + visibleBattleLimit);
|
||||
if (chapter.battleIds.length > visibleBattleIds.length) {
|
||||
const range = this.track(this.add.text(
|
||||
x + width - 18,
|
||||
y + 174,
|
||||
`전장 ${visibleBattleStart + 1}–${visibleBattleStart + visibleBattleIds.length} 표시`,
|
||||
this.textStyle(10, '#9fb0bf', true)
|
||||
));
|
||||
range.setOrigin(1, 0);
|
||||
}
|
||||
const battleTitleX = x + 78;
|
||||
const battleTitleWidth = 116;
|
||||
const objectiveX = battleTitleX + battleTitleWidth + 10;
|
||||
const objectiveWidth = x + width - 30 - objectiveX;
|
||||
visibleBattleIds.forEach((battleId, index) => {
|
||||
const battle = getBattleScenario(battleId);
|
||||
const complete = completedIds.has(battleId);
|
||||
const rowY = y + 190 + index * 28;
|
||||
@@ -22045,9 +22089,27 @@ export class CampScene extends Phaser.Scene {
|
||||
row.setOrigin(0);
|
||||
row.setStrokeStyle(1, complete ? palette.green : palette.blue, complete ? 0.48 : 0.32);
|
||||
this.track(this.add.text(x + 30, rowY + 4, complete ? '완료' : '대기', this.textStyle(10, complete ? '#a8ffd0' : '#9fb0bf', true)));
|
||||
this.track(this.add.text(x + 78, rowY + 3, battle.title, this.textStyle(12, complete ? '#d4dce6' : '#f2e3bf', !complete)));
|
||||
const objective = this.track(this.add.text(x + width - 30, rowY + 4, battle.victoryConditionLabel, this.textStyle(10, '#9fb0bf', true)));
|
||||
objective.setOrigin(1, 0);
|
||||
const battleTitle = this.track(this.add.text(
|
||||
battleTitleX,
|
||||
rowY + 3,
|
||||
this.compactText(battle.title, 10),
|
||||
{
|
||||
...this.textStyle(12, complete ? '#d4dce6' : '#f2e3bf', !complete),
|
||||
fixedWidth: battleTitleWidth
|
||||
}
|
||||
));
|
||||
battleTitle.setData('timelineField', 'battle-title');
|
||||
const objective = this.track(this.add.text(
|
||||
objectiveX,
|
||||
rowY + 4,
|
||||
this.compactText(battle.victoryConditionLabel, 15),
|
||||
{
|
||||
...this.textStyle(10, '#9fb0bf', true),
|
||||
align: 'right',
|
||||
fixedWidth: objectiveWidth
|
||||
}
|
||||
));
|
||||
objective.setData('timelineField', 'victory-condition');
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -22310,10 +22372,23 @@ export class CampScene extends Phaser.Scene {
|
||||
);
|
||||
|
||||
if (completed) {
|
||||
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, 0x17231d, 0.96));
|
||||
done.setStrokeStyle(1, palette.green, 0.76);
|
||||
const text = this.track(this.add.text(x + width / 2, y + height - 34, '대화 완료', this.textStyle(15, '#a8ffd0', true)));
|
||||
text.setOrigin(0.5);
|
||||
const chosen = this.recordedDialogueChoice(dialogue);
|
||||
if (chosen) {
|
||||
const memory = this.track(this.add.rectangle(x + 18, y + height - 94, width - 36, 76, 0x17231d, 0.96));
|
||||
memory.setOrigin(0);
|
||||
memory.setStrokeStyle(1, palette.green, 0.76);
|
||||
this.track(this.add.text(x + 32, y + height - 84, `나의 결정 · ${chosen.label}`, this.textStyle(13, '#a8ffd0', true)));
|
||||
this.track(this.add.text(x + 32, y + height - 58, `남은 말 · ${this.compactText(chosen.response, 54)}`, {
|
||||
...this.textStyle(12, '#d4dce6'),
|
||||
wordWrap: { width: width - 64, useAdvancedWrap: true },
|
||||
lineSpacing: 2
|
||||
}));
|
||||
} else {
|
||||
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 250, 34, 0x17231d, 0.96));
|
||||
done.setStrokeStyle(1, palette.green, 0.76);
|
||||
const text = this.track(this.add.text(x + width / 2, y + height - 34, '대화 완료 · 과거 선택 기록 없음', this.textStyle(13, '#a8ffd0', true)));
|
||||
text.setOrigin(0.5);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22418,10 +22493,23 @@ export class CampScene extends Phaser.Scene {
|
||||
);
|
||||
|
||||
if (completed) {
|
||||
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 220, 34, 0x17231d, 0.96));
|
||||
done.setStrokeStyle(1, palette.green, 0.76);
|
||||
const text = this.track(this.add.text(x + width / 2, y + height - 34, '방문 완료', this.textStyle(15, '#a8ffd0', true)));
|
||||
text.setOrigin(0.5);
|
||||
const chosen = this.recordedVisitChoice(visit);
|
||||
if (chosen) {
|
||||
const memory = this.track(this.add.rectangle(x + 18, y + height - 94, width - 36, 76, 0x17231d, 0.96));
|
||||
memory.setOrigin(0);
|
||||
memory.setStrokeStyle(1, palette.green, 0.76);
|
||||
this.track(this.add.text(x + 32, y + height - 84, `나의 결정 · ${chosen.label}`, this.textStyle(13, '#a8ffd0', true)));
|
||||
this.track(this.add.text(x + 32, y + height - 58, `남은 말 · ${this.compactText(chosen.response, 54)}`, {
|
||||
...this.textStyle(12, '#d4dce6'),
|
||||
wordWrap: { width: width - 64, useAdvancedWrap: true },
|
||||
lineSpacing: 2
|
||||
}));
|
||||
} else {
|
||||
const done = this.track(this.add.rectangle(x + width / 2, y + height - 34, 250, 34, 0x17231d, 0.96));
|
||||
done.setStrokeStyle(1, palette.green, 0.76);
|
||||
const text = this.track(this.add.text(x + width / 2, y + height - 34, '방문 완료 · 과거 선택 기록 없음', this.textStyle(13, '#a8ffd0', true)));
|
||||
text.setOrigin(0.5);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23063,7 +23151,7 @@ export class CampScene extends Phaser.Scene {
|
||||
bondExp: choice.bondExp,
|
||||
gold: choice.gold,
|
||||
itemRewards: choice.itemRewards
|
||||
});
|
||||
}, choice.id);
|
||||
|
||||
if (updated) {
|
||||
this.campaign = updated;
|
||||
@@ -23095,7 +23183,7 @@ export class CampScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
const rewardExp = dialogue.rewardExp + choice.rewardExp;
|
||||
const updated = applyCampBondExp(dialogue.id, dialogue.bondId, rewardExp);
|
||||
const updated = applyCampBondExp(dialogue.id, dialogue.bondId, rewardExp, choice.id);
|
||||
if (updated) {
|
||||
this.report = updated;
|
||||
this.campaign = getCampaignState();
|
||||
@@ -23501,6 +23589,16 @@ export class CampScene extends Phaser.Scene {
|
||||
return this.report?.completedCampVisits ?? [];
|
||||
}
|
||||
|
||||
private recordedDialogueChoice(dialogue: CampDialogue) {
|
||||
const choiceId = this.campaign?.campDialogueChoiceIds[dialogue.id];
|
||||
return choiceId ? dialogue.choices.find((choice) => choice.id === choiceId) : undefined;
|
||||
}
|
||||
|
||||
private recordedVisitChoice(visit: CampVisitDefinition) {
|
||||
const choiceId = this.campaign?.campVisitChoiceIds[visit.id];
|
||||
return choiceId ? visit.choices.find((choice) => choice.id === choiceId) : undefined;
|
||||
}
|
||||
|
||||
private inventoryLabels() {
|
||||
const inventory = this.campaign?.inventory ?? {};
|
||||
const entries = Object.entries(inventory).filter(([, amount]) => amount > 0);
|
||||
|
||||
Reference in New Issue
Block a user