feat: add playable first-battle militia camp
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import Phaser from 'phaser';
|
||||
import { soundDirector } from '../audio/SoundDirector';
|
||||
import {
|
||||
prologueDeparturePages,
|
||||
prologueBrotherhoodPages,
|
||||
prologueVillageNpcDefinitions,
|
||||
prologueVillageObjectiveDefinitions,
|
||||
prologueVillageRequiredObjectiveIds,
|
||||
@@ -75,7 +75,7 @@ type ObjectiveRowView = {
|
||||
const objectiveTutorialIds: Record<PrologueVillageRequiredObjectiveId, CampaignTutorialId> = {
|
||||
'meet-zhang-fei': prologueVillageCampaignTutorialIds.meetZhangFei,
|
||||
'meet-guan-yu': prologueVillageCampaignTutorialIds.meetGuanYu,
|
||||
'check-supplies': prologueVillageCampaignTutorialIds.checkSupplies
|
||||
'register-volunteers': prologueVillageCampaignTutorialIds.registerVolunteers
|
||||
};
|
||||
|
||||
export class PrologueVillageScene extends Phaser.Scene {
|
||||
@@ -150,6 +150,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
}
|
||||
ensureUnitAnimations(this, characterTextureKeys);
|
||||
this.createActors();
|
||||
this.restoreNarrativeActorPositions();
|
||||
this.createDialoguePanel();
|
||||
this.loadingOverlay?.destroy();
|
||||
this.loadingOverlay = undefined;
|
||||
@@ -164,7 +165,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
{
|
||||
speaker: '유비',
|
||||
textureKey: 'unit-liu-bei',
|
||||
text: '장터에서 뜻을 함께할 사람을 찾고 의병소의 준비를 확인하자. 모든 준비가 끝나면 북쪽 복숭아 동산으로 가야겠다.'
|
||||
text: '방금 등 뒤에서 들린 목소리의 주인을 찾아보자. 서쪽 격문 앞의 호걸에게 다가가 그 뜻을 들어 보자.'
|
||||
}
|
||||
]);
|
||||
}
|
||||
@@ -246,7 +247,9 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
completed: objective.id === 'make-oath'
|
||||
? hasCompletedCampaignTutorial(prologueVillageCampaignTutorialIds.complete)
|
||||
: this.isObjectiveComplete(objective.id),
|
||||
unlocked: objective.id !== 'make-oath' || this.allRequiredObjectivesComplete()
|
||||
unlocked: objective.id === 'make-oath'
|
||||
? this.allRequiredObjectivesComplete()
|
||||
: this.objectiveUnlocked(objective.id)
|
||||
})),
|
||||
completedObjectiveIds: this.completedRequiredObjectiveIds(),
|
||||
exitUnlocked: this.allRequiredObjectivesComplete(),
|
||||
@@ -350,8 +353,9 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
|
||||
private prepareCampaignProgress() {
|
||||
const campaign = getCampaignState();
|
||||
this.firstVisit = campaign.step !== 'prologue-town' &&
|
||||
!campaign.completedTutorialIds.includes(prologueVillageCampaignTutorialIds.entered);
|
||||
this.firstVisit = !campaign.completedTutorialIds.includes(
|
||||
prologueVillageCampaignTutorialIds.entered
|
||||
);
|
||||
if (campaign.step === 'new' || campaign.step === 'prologue') {
|
||||
markCampaignStep('prologue-town');
|
||||
}
|
||||
@@ -427,7 +431,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
height: 236,
|
||||
wallColor: 0xb99a68,
|
||||
roofColor: 0x6d2f26,
|
||||
sign: '장터 주점'
|
||||
sign: '탁현 관아'
|
||||
});
|
||||
this.addBlocker(105, 160, 410, 236);
|
||||
|
||||
@@ -438,7 +442,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
height: 226,
|
||||
wallColor: 0xc2a974,
|
||||
roofColor: 0x31475a,
|
||||
sign: '동쪽 장터'
|
||||
sign: '장터 주점'
|
||||
});
|
||||
this.addBlocker(990, 148, 390, 226);
|
||||
|
||||
@@ -449,7 +453,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
height: 215,
|
||||
wallColor: 0xa98d61,
|
||||
roofColor: 0x493a32,
|
||||
sign: '탁현 의병소'
|
||||
sign: '탁현 모병소'
|
||||
});
|
||||
this.addBlocker(1120, 665, 310, 215);
|
||||
|
||||
@@ -542,6 +546,22 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
weapons.lineBetween(1070, 710, 1110, 805);
|
||||
weapons.lineBetween(1110, 710, 1070, 805);
|
||||
|
||||
const notice = this.add.graphics().setDepth(34);
|
||||
notice.fillStyle(0x4d3826, 1);
|
||||
notice.fillRect(264, 412, 12, 92);
|
||||
notice.fillRect(440, 412, 12, 92);
|
||||
notice.fillRect(250, 402, 216, 18);
|
||||
notice.fillStyle(0xe3d4a8, 1);
|
||||
notice.fillRect(286, 427, 144, 62);
|
||||
notice.lineStyle(2, 0x765e3d, 0.8);
|
||||
notice.strokeRect(286, 427, 144, 62);
|
||||
this.add.text(358, 457, '의병 모집', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '20px',
|
||||
color: '#3c2a1c',
|
||||
fontStyle: 'bold'
|
||||
}).setOrigin(0.5).setDepth(35);
|
||||
|
||||
this.add.text(780, 244, '복숭아 동산', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '28px',
|
||||
@@ -588,13 +608,13 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
}
|
||||
|
||||
private drawHud() {
|
||||
this.add.text(48, 27, '탁현 · 황건 봉기 직전', {
|
||||
this.add.text(48, 27, '탁현 · 의병 모집 격문이 붙은 날', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '34px',
|
||||
color: '#f4e3b5',
|
||||
fontStyle: 'bold'
|
||||
}).setDepth(1500);
|
||||
this.add.text(460, 36, '유비의 첫걸음', {
|
||||
this.add.text(650, 36, '유비의 첫걸음', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '20px',
|
||||
color: '#b4c0cb'
|
||||
@@ -670,11 +690,11 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
color: '#e3e7eb'
|
||||
}).setDepth(1502);
|
||||
|
||||
this.promptBackground = this.add.rectangle(870, 1018, 540, 58, 0x2a2119, 0.98)
|
||||
this.promptBackground = this.add.rectangle(1110, 1018, 560, 58, 0x2a2119, 0.98)
|
||||
.setStrokeStyle(2, palette.gold, 0.9)
|
||||
.setDepth(1510)
|
||||
.setVisible(false);
|
||||
this.promptText = this.add.text(870, 1018, '', {
|
||||
this.promptText = this.add.text(1110, 1018, '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '21px',
|
||||
color: '#f6e3af',
|
||||
@@ -1019,6 +1039,43 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
this.stopPlayerMovement();
|
||||
}
|
||||
|
||||
private gatherOathParty() {
|
||||
this.playerDirection = 'north';
|
||||
this.setPlayerPosition(oathPosition.x, oathPosition.y + 125);
|
||||
this.setNpcPosition('guan-yu', oathPosition.x - 104, oathPosition.y + 32, 'east');
|
||||
this.setNpcPosition('zhang-fei', oathPosition.x + 104, oathPosition.y + 32, 'west');
|
||||
}
|
||||
|
||||
private restoreNarrativeActorPositions() {
|
||||
if (this.isObjectiveComplete('meet-guan-yu')) {
|
||||
this.setNpcPosition('guan-yu', 850, 655, 'east');
|
||||
this.setNpcPosition('zhang-fei', 810, 805, 'east');
|
||||
return;
|
||||
}
|
||||
if (this.isObjectiveComplete('meet-zhang-fei')) {
|
||||
this.setNpcPosition('zhang-fei', 1000, 540, 'east');
|
||||
}
|
||||
}
|
||||
|
||||
private setNpcPosition(
|
||||
npcId: string,
|
||||
x: number,
|
||||
y: number,
|
||||
direction: UnitDirection
|
||||
) {
|
||||
const view = this.npcViews.get(npcId);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
view.sprite.setPosition(x, y).setDepth(110 + y);
|
||||
view.shadow.setPosition(x, y + 20).setDepth(100 + y);
|
||||
view.nameplate.setPosition(x, y + 63);
|
||||
view.marker?.setPosition(x, y - 72).setVisible(false);
|
||||
if (this.textures.exists(view.definition.textureKey)) {
|
||||
view.sprite.play(`${view.definition.textureKey}-idle-${direction}`);
|
||||
}
|
||||
}
|
||||
|
||||
private safeApproachPosition(targetX: number, targetY: number) {
|
||||
const candidates = [
|
||||
{ x: targetX, y: targetY + 88 },
|
||||
@@ -1055,11 +1112,14 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
const completed = view.definition.objectiveId
|
||||
? this.isObjectiveComplete(view.definition.objectiveId)
|
||||
: false;
|
||||
const dialogue = completed && view.definition.repeatDialogue?.length
|
||||
? view.definition.repeatDialogue
|
||||
: view.definition.dialogue;
|
||||
const unlocked = !view.definition.objectiveId || this.objectiveUnlocked(view.definition.objectiveId);
|
||||
const dialogue = !unlocked && view.definition.lockedDialogue?.length
|
||||
? view.definition.lockedDialogue
|
||||
: completed && view.definition.repeatDialogue?.length
|
||||
? view.definition.repeatDialogue
|
||||
: view.definition.dialogue;
|
||||
this.startDialogue(dialogue, () => {
|
||||
if (view.definition.objectiveId && !completed) {
|
||||
if (view.definition.objectiveId && unlocked && !completed) {
|
||||
this.completeObjective(view.definition.objectiveId);
|
||||
}
|
||||
}, view.definition.id);
|
||||
@@ -1081,35 +1141,27 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gatherOathParty();
|
||||
this.startDialogue([
|
||||
{
|
||||
speaker: '장비',
|
||||
textureKey: 'unit-zhang-fei',
|
||||
text: '내 장원 뒤 복숭아꽃이 한창이오. 싸움에 앞서 서로의 등을 맡길 뜻부터 하늘과 땅에 고합시다.'
|
||||
},
|
||||
{
|
||||
speaker: '유비',
|
||||
textureKey: 'unit-liu-bei',
|
||||
text: '우리는 성은 다르나 이제 형제가 되어, 위로는 나라에 보답하고 아래로는 백성을 편안하게 하겠습니다.'
|
||||
text: '좋소. 성은 서로 다르지만 백성을 구하려는 뜻은 하나이니, 오늘부터 형제가 되어 힘을 합칩시다.'
|
||||
},
|
||||
{
|
||||
speaker: '관우',
|
||||
textureKey: 'unit-guan-yu',
|
||||
text: '한마음으로 힘을 합쳐 어려움 속에서도 서로를 저버리지 않겠습니다.'
|
||||
text: '의를 저버리지 않고 형제를 저버리지 않겠습니다. 어려움이 닥쳐도 함께 맞서겠습니다.'
|
||||
},
|
||||
{
|
||||
speaker: '장비',
|
||||
textureKey: 'unit-zhang-fei',
|
||||
text: '좋소! 오늘부터 함께 싸워 이 마을과 백성을 지킵시다!'
|
||||
},
|
||||
{
|
||||
speaker: '전황',
|
||||
text: '세 사람의 뜻이 하나로 공명했다. 탁현 의용군이 북문 앞에 모이기 시작했다.'
|
||||
},
|
||||
{
|
||||
speaker: '관우',
|
||||
textureKey: 'unit-guan-yu',
|
||||
text: '황건 잔당은 북동쪽 길목에 있습니다. 장비가 측면을 흔들면 제가 정면을 열겠습니다.'
|
||||
},
|
||||
{
|
||||
speaker: '유비',
|
||||
textureKey: 'unit-liu-bei',
|
||||
text: '좋소. 부상자를 지키며 전열을 잇겠습니다. 이제 첫 출전입니다.'
|
||||
text: '형님들의 뜻을 내 목숨처럼 지키겠소. 결의를 마치면 내 장원 밖 막사에서 의병들을 함께 살핍시다!'
|
||||
}
|
||||
], () => this.finishVillage(), 'make-oath');
|
||||
}
|
||||
@@ -1178,6 +1230,12 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
completeCampaignTutorial(objectiveTutorialIds[objectiveId]);
|
||||
if (objectiveId === 'meet-zhang-fei') {
|
||||
this.setNpcPosition('zhang-fei', 1000, 540, 'east');
|
||||
} else if (objectiveId === 'meet-guan-yu') {
|
||||
this.setNpcPosition('guan-yu', 850, 655, 'east');
|
||||
this.setNpcPosition('zhang-fei', 810, 805, 'east');
|
||||
}
|
||||
soundDirector.playObjectiveAchieved();
|
||||
this.refreshObjectiveHud();
|
||||
this.refreshNpcMarkers();
|
||||
@@ -1202,9 +1260,8 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
this.showTransitionOverlay();
|
||||
|
||||
void startGameScene(this, 'StoryScene', {
|
||||
pages: prologueDeparturePages(),
|
||||
nextScene: 'BattleScene',
|
||||
nextSceneData: { battleId: 'first-battle-zhuo-commandery' },
|
||||
pages: prologueBrotherhoodPages(),
|
||||
nextScene: 'PrologueMilitiaCampScene',
|
||||
presentationBattleId: 'first-battle-zhuo-commandery',
|
||||
presentationStage: 'story'
|
||||
}).catch((error) => {
|
||||
@@ -1218,13 +1275,13 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
|
||||
private showTransitionOverlay() {
|
||||
const shade = this.add.rectangle(0, 0, sceneWidth, sceneHeight, 0x080b0f, 0.78).setOrigin(0);
|
||||
const title = this.add.text(sceneWidth / 2, sceneHeight / 2 - 32, '탁현 의용군 · 첫 출전', {
|
||||
const title = this.add.text(sceneWidth / 2, sceneHeight / 2 - 32, '도원결의 · 세 사람의 맹세', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '44px',
|
||||
color: '#f3dda2',
|
||||
fontStyle: 'bold'
|
||||
}).setOrigin(0.5);
|
||||
const subtitle = this.add.text(sceneWidth / 2, sceneHeight / 2 + 44, '북문 밖의 황건 잔당을 향해 이동합니다.', {
|
||||
const subtitle = this.add.text(sceneWidth / 2, sceneHeight / 2 + 44, '맹세를 마친 뒤 의용군 막사에서 첫 출전을 준비합니다.', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '24px',
|
||||
color: '#d7dde4'
|
||||
@@ -1284,7 +1341,9 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
} else {
|
||||
completed = this.isObjectiveComplete(objective.id);
|
||||
}
|
||||
const unlocked = !isOath || this.allRequiredObjectivesComplete();
|
||||
const unlocked = isOath
|
||||
? this.allRequiredObjectivesComplete()
|
||||
: this.objectiveUnlocked(objective.id);
|
||||
row.status.setText(completed ? '✓ 완료' : unlocked ? '◆ 진행 가능' : '◇ 준비 후 개방');
|
||||
row.status.setColor(completed ? '#a8d58e' : unlocked ? '#e1bc69' : '#7f8994');
|
||||
row.label.setColor(completed ? '#b8c4b3' : unlocked ? '#f0e6d2' : '#7f8994');
|
||||
@@ -1299,7 +1358,7 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
const remaining = prologueVillageRequiredObjectiveIds.length - requiredComplete.length;
|
||||
this.objectiveSummaryText?.setText(
|
||||
remaining > 0
|
||||
? `필수 준비 ${requiredComplete.length} / ${prologueVillageRequiredObjectiveIds.length}\n세 항목은 원하는 순서로 확인할 수 있습니다.`
|
||||
? `만남의 흐름 ${requiredComplete.length} / ${prologueVillageRequiredObjectiveIds.length}\n◆ 표시된 다음 인물을 찾아 이야기하세요.`
|
||||
: '모든 준비 완료\n복숭아 동산의 빛나는 표식으로 이동하세요.'
|
||||
);
|
||||
if (this.oathMarker) {
|
||||
@@ -1318,9 +1377,10 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
const completed = this.isObjectiveComplete(definition.objectiveId);
|
||||
marker.setText(completed ? '✓' : '!');
|
||||
marker.setColor(completed ? '#e5f2d5' : '#2a2013');
|
||||
marker.setBackgroundColor(completed ? '#4e7549' : '#e5bd68');
|
||||
const unlocked = this.objectiveUnlocked(definition.objectiveId);
|
||||
marker.setText(completed ? '✓' : unlocked ? '!' : '◇');
|
||||
marker.setColor(completed ? '#e5f2d5' : unlocked ? '#2a2013' : '#9b8f7d');
|
||||
marker.setBackgroundColor(completed ? '#4e7549' : unlocked ? '#e5bd68' : '#343b45');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1409,6 +1469,14 @@ export class PrologueVillageScene extends Phaser.Scene {
|
||||
return hasCompletedCampaignTutorial(objectiveTutorialIds[objectiveId]);
|
||||
}
|
||||
|
||||
private objectiveUnlocked(objectiveId: PrologueVillageRequiredObjectiveId | 'make-oath') {
|
||||
if (objectiveId === 'make-oath') {
|
||||
return this.allRequiredObjectivesComplete();
|
||||
}
|
||||
const objective = prologueVillageObjectiveDefinitions.find((entry) => entry.id === objectiveId);
|
||||
return !objective?.prerequisiteId || this.isObjectiveComplete(objective.prerequisiteId);
|
||||
}
|
||||
|
||||
private completedRequiredObjectiveIds() {
|
||||
return prologueVillageRequiredObjectiveIds.filter((objectiveId) => this.isObjectiveComplete(objectiveId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user