Rework first battle story as unit cutscenes

This commit is contained in:
2026-07-04 00:02:16 +09:00
parent 42a596bd8b
commit 8b81d16cf1
4 changed files with 588 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
import type { TerrainType, UnitClassKey } from './battleRules';
import type { EquipmentSet } from './battleItems';
import type { StoryCutscene } from './storyCutscenes';
export type PortraitKey = string;
@@ -11,6 +12,7 @@ export type StoryPage = {
portrait?: PortraitKey;
bgm?: string;
text: string;
cutscene?: StoryCutscene;
};
export type UnitData = {
@@ -185,6 +187,201 @@ export const firstBattleVictoryPages: StoryPage[] = [
}
];
const firstBattleStoryOverrides: Record<string, Partial<StoryPage>> = {
'militia-rises': {
chapter: '탁현 의용군',
speaker: '장비',
text: '창을 들 사람은 앞으로 나오시오. 오늘 모인 이름 없는 무리가 마을의 방패가 될 것이오.',
cutscene: {
kind: 'muster',
title: '탁현 의용군 집결',
subtitle: '세 형제와 마을 사람들이 첫 출전을 준비합니다.',
actors: [
{ unitId: 'liu-bei', x: 0.34, y: 0.53, size: 122, direction: 'east', label: '군주' },
{ unitId: 'guan-yu', x: 0.49, y: 0.52, size: 132, direction: 'east', label: '선봉' },
{ unitId: 'zhang-fei', x: 0.64, y: 0.53, size: 132, direction: 'west', label: '돌파' }
],
briefing: {
title: '출전 준비',
lines: ['유비는 중앙에서 전열을 잡습니다.', '관우는 앞길을 열고, 장비는 측면을 찌릅니다.']
},
rewards: [
{ label: '아군: 유비 · 관우 · 장비', tone: 'bond' },
{ label: '목표: 마을 방어', tone: 'next' }
]
}
},
'first-sortie': {
chapter: '첫 출전',
speaker: '유비',
text: '오늘은 마을을 지키는 첫 싸움이다. 서로의 등을 믿고, 백성들이 물러설 시간을 벌자.',
cutscene: {
kind: 'muster',
title: '세 형제 출진',
subtitle: '전투에서 조작할 유닛이 스토리 화면에 그대로 등장합니다.',
actors: [
{ unitId: 'liu-bei', x: 0.32, y: 0.53, size: 136, direction: 'east', label: '유비' },
{ unitId: 'guan-yu', x: 0.5, y: 0.51, size: 144, direction: 'east', label: '관우' },
{ unitId: 'zhang-fei', x: 0.68, y: 0.53, size: 144, direction: 'west', label: '장비' }
],
briefing: {
title: '전장 역할',
lines: ['유비: 생존과 지휘', '관우: 전면 돌파', '장비: 측면 압박']
},
rewards: [
{ label: '패배 조건: 유비 퇴각 금지', tone: 'next' },
{ label: '공명: 세 형제 인접 운용', tone: 'bond' }
]
}
},
'battle-briefing': {
chapter: '탁현 작전판',
speaker: '전황',
text: '황건 두령을 쓰러뜨리면 적의 전열이 무너진다. 마을 입구를 지키며 북쪽 적진으로 밀고 올라가자.',
cutscene: {
kind: 'operation',
title: '탁현의 전투',
subtitle: '마을 입구에서 시작해 황건 두령을 격파하십시오.',
actors: [
{ unitId: 'liu-bei', x: 0.24, y: 0.54, size: 104, direction: 'east', label: '중앙' },
{ unitId: 'guan-yu', x: 0.34, y: 0.53, size: 108, direction: 'east', label: '전면' },
{ unitId: 'zhang-fei', x: 0.44, y: 0.54, size: 108, direction: 'east', label: '측면' },
{ unitId: 'rebel-leader', x: 0.78, y: 0.53, size: 108, direction: 'west', label: '두령' }
],
markers: [
{ x: 0.2, y: 0.74, label: '아군 시작', side: 'ally' },
{ x: 0.44, y: 0.5, label: '마을 입구', side: 'objective' },
{ x: 0.74, y: 0.28, label: '황건 두령', side: 'enemy' }
],
briefing: {
title: '승리 조건',
lines: ['황건 두령 격파', '유비 생존', '마을 지형 확보']
},
rewards: [
{ label: '보상: 보급품 · 연습검 · 의용군 명성', tone: 'reward' }
]
}
},
'first-victory-village': {
chapter: '첫 승리',
speaker: '전황',
text: '황건의 깃발이 꺾이고, 흩어졌던 백성들이 마을로 돌아오기 시작했다.',
cutscene: {
kind: 'victory',
title: '탁현 방어 성공',
subtitle: '전투에서 싸운 세 유닛이 그대로 승리 장면에 남습니다.',
actors: [
{ unitId: 'liu-bei', x: 0.36, y: 0.53, size: 132, direction: 'east', label: '유비' },
{ unitId: 'guan-yu', x: 0.52, y: 0.52, size: 142, direction: 'west', label: '관우' },
{ unitId: 'zhang-fei', x: 0.67, y: 0.53, size: 142, direction: 'west', label: '장비' }
],
rewards: [
{ label: '승리 조건 달성', tone: 'reward' },
{ label: '마을 방어', tone: 'bond' },
{ label: '다음: 황건 잔당 추격', tone: 'next' }
]
}
},
'first-victory-liu-bei': {
chapter: '첫 승리',
speaker: '유비',
text: '오늘의 승리는 작다. 하지만 백성을 지키는 길은 여기서 시작된다.',
cutscene: {
kind: 'victory',
title: '전투 후 정비',
subtitle: '획득 보상은 다음 출전 준비와 장비 화면으로 이어집니다.',
actors: [
{ unitId: 'liu-bei', x: 0.44, y: 0.52, size: 146, direction: 'south', label: '유비' },
{ unitId: 'guan-yu', x: 0.58, y: 0.53, size: 130, direction: 'west', label: '관우' },
{ unitId: 'zhang-fei', x: 0.7, y: 0.54, size: 130, direction: 'west', label: '장비' }
],
rewards: [
{ label: '보급품: 콩 · 탁주', tone: 'reward' },
{ label: '장비: 연습검', tone: 'reward' },
{ label: '명성: 의용군 +1', tone: 'bond' }
]
}
},
'first-victory-guan-yu': {
chapter: '의용군의 이름',
speaker: '관우',
text: '형님의 뜻을 보고 사람들이 모일 것입니다. 다음 싸움은 오늘보다 무거운 책임을 요구하겠지요.',
cutscene: {
kind: 'victory',
title: '관우의 다짐',
subtitle: '선봉의 장병기가 다음 전투의 전열을 맡습니다.',
actors: [
{ unitId: 'guan-yu', x: 0.45, y: 0.52, size: 152, direction: 'east', label: '선봉' },
{ unitId: 'liu-bei', x: 0.31, y: 0.54, size: 122, direction: 'east', label: '지휘' },
{ unitId: 'zhang-fei', x: 0.64, y: 0.54, size: 128, direction: 'west', label: '돌파' }
],
rewards: [
{ label: '출전 가능: 관우', tone: 'bond' },
{ label: '역할 추천: 전면', tone: 'next' }
]
}
},
'first-victory-zhang-fei': {
chapter: '의용군의 이름',
speaker: '장비',
text: '좋다! 다음에는 더 크게 몰아치자. 창을 드는 자들은 우리 의용군을 따라올 것이다.',
cutscene: {
kind: 'victory',
title: '장비의 기세',
subtitle: '측면 돌파와 압박이 다음 전장의 선택지가 됩니다.',
actors: [
{ unitId: 'zhang-fei', x: 0.48, y: 0.52, size: 154, direction: 'east', label: '돌파' },
{ unitId: 'liu-bei', x: 0.34, y: 0.54, size: 122, direction: 'east', label: '지휘' },
{ unitId: 'guan-yu', x: 0.64, y: 0.53, size: 130, direction: 'west', label: '선봉' }
],
rewards: [
{ label: '출전 가능: 장비', tone: 'bond' },
{ label: '역할 추천: 측면', tone: 'next' }
]
}
},
'first-victory-next': {
chapter: '다음 길',
speaker: '전황',
text: '세 형제의 이름은 탁현 일대에 퍼지기 시작했다. 이제 황건 잔당의 퇴로를 끊어야 한다.',
cutscene: {
kind: 'operation',
title: '다음 전투 해금',
subtitle: '황건 잔당 추격으로 이어집니다.',
actors: [
{ unitId: 'liu-bei', x: 0.25, y: 0.54, size: 108, direction: 'east', label: '유비' },
{ unitId: 'guan-yu', x: 0.34, y: 0.53, size: 112, direction: 'east', label: '관우' },
{ unitId: 'zhang-fei', x: 0.43, y: 0.54, size: 112, direction: 'east', label: '장비' }
],
markers: [
{ x: 0.24, y: 0.68, label: '탁현', side: 'ally' },
{ x: 0.58, y: 0.46, label: '북쪽 길목', side: 'objective' },
{ x: 0.76, y: 0.32, label: '황건 잔당', side: 'enemy' }
],
briefing: {
title: '정비 후 출전',
lines: ['보급품 배정', '장비 교체', '출전 무장 선택']
},
rewards: [
{ label: '군영에서 보상 확인', tone: 'reward' },
{ label: '다음: 황건 잔당 추격', tone: 'next' }
]
}
}
};
function applyStoryPageOverrides(pages: StoryPage[]) {
pages.forEach((page) => {
const override = firstBattleStoryOverrides[page.id];
if (override) {
Object.assign(page, override);
}
});
}
applyStoryPageOverrides(prologuePages);
applyStoryPageOverrides(firstBattleVictoryPages);
export const secondBattleIntroPages: StoryPage[] = [
{
id: 'second-pursuit-rumor',

View File

@@ -0,0 +1,100 @@
import type { UnitDirection } from './unitAssets';
export type StoryCutsceneKind = 'muster' | 'operation' | 'victory';
export type StoryCutsceneTone = 'reward' | 'next' | 'bond';
export type StoryCutsceneMarkerSide = 'ally' | 'enemy' | 'objective';
export type StoryCutsceneActorProfile = {
unitId: string;
name: string;
roleLabel: string;
unitTextureKey: string;
portraitKey: string;
color: number;
};
export type StoryCutsceneActor = {
unitId: string;
x: number;
y: number;
size?: number;
direction?: UnitDirection;
label?: string;
line?: string;
};
export type StoryCutsceneMarker = {
x: number;
y: number;
label: string;
side: StoryCutsceneMarkerSide;
};
export type StoryCutsceneBriefing = {
title: string;
lines: string[];
};
export type StoryCutsceneReward = {
label: string;
tone?: StoryCutsceneTone;
};
export type StoryCutscene = {
kind: StoryCutsceneKind;
title: string;
subtitle?: string;
actors?: StoryCutsceneActor[];
briefing?: StoryCutsceneBriefing;
markers?: StoryCutsceneMarker[];
rewards?: StoryCutsceneReward[];
};
const storyCutsceneActorProfiles: Record<string, StoryCutsceneActorProfile> = {
'liu-bei': {
unitId: 'liu-bei',
name: '유비',
roleLabel: '검을 든 군주',
unitTextureKey: 'unit-liu-bei',
portraitKey: 'liuBei',
color: 0xd8b15f
},
'guan-yu': {
unitId: 'guan-yu',
name: '관우',
roleLabel: '장병기 선봉',
unitTextureKey: 'unit-guan-yu',
portraitKey: 'guanYu',
color: 0x74c476
},
'zhang-fei': {
unitId: 'zhang-fei',
name: '장비',
roleLabel: '돌파형 창병',
unitTextureKey: 'unit-zhang-fei',
portraitKey: 'zhangFei',
color: 0xd46a4c
},
'rebel-leader': {
unitId: 'rebel-leader',
name: '황건 두령',
roleLabel: '격파 목표',
unitTextureKey: 'unit-rebel-leader',
portraitKey: 'rebelLeader',
color: 0xd6b852
}
};
export function storyCutsceneActorProfileFor(unitId: string) {
return storyCutsceneActorProfiles[unitId];
}
export function storyCutsceneActorTextureKeys(cutscene?: StoryCutscene) {
return Array.from(
new Set(
(cutscene?.actors ?? [])
.map((actor) => storyCutsceneActorProfileFor(actor.unitId)?.unitTextureKey)
.filter((textureKey): textureKey is string => Boolean(textureKey))
)
);
}