Improve combat UX and story assets
@@ -189,6 +189,93 @@ try {
|
|||||||
throw new Error(`Expected enemy threat markers from map menu: ${JSON.stringify(threatState)}`);
|
throw new Error(`Expected enemy threat markers from map menu: ${JSON.stringify(threatState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visibleEnemyScreen = await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
const enemy = state?.units?.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||||
|
|
||||||
|
if (!scene || !enemy) {
|
||||||
|
throw new Error('Expected enemy and battle scene for enemy threat click test.');
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.centerCameraOnTile(enemy.x, enemy.y);
|
||||||
|
return {
|
||||||
|
x: scene.tileCenterX(enemy.x),
|
||||||
|
y: scene.tileCenterY(enemy.y)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(120);
|
||||||
|
await page.mouse.click(visibleEnemyScreen.x, visibleEnemyScreen.y);
|
||||||
|
await page.waitForTimeout(180);
|
||||||
|
|
||||||
|
const clickedEnemyThreatState = await page.evaluate(() => window.__HEROS_DEBUG__?.battle());
|
||||||
|
if (
|
||||||
|
!clickedEnemyThreatState?.enemyMoveMarkerCount ||
|
||||||
|
clickedEnemyThreatState.enemyMoveMarkerCount <= 0 ||
|
||||||
|
!clickedEnemyThreatState.threatMarkerCount ||
|
||||||
|
clickedEnemyThreatState.threatMarkerCount <= 0
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected clicked enemy movement and attack markers: ${JSON.stringify(clickedEnemyThreatState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combatMechanicsProbe = clickedEnemyThreatState.combatMechanicsProbe;
|
||||||
|
if (
|
||||||
|
!combatMechanicsProbe ||
|
||||||
|
combatMechanicsProbe.adjacentBondDamageBonus <= 0 ||
|
||||||
|
combatMechanicsProbe.sameTargetBondDamageBonus <= 0 ||
|
||||||
|
combatMechanicsProbe.farBondDamageBonus !== 0 ||
|
||||||
|
combatMechanicsProbe.counterDamage >= combatMechanicsProbe.normalDamage
|
||||||
|
) {
|
||||||
|
throw new Error(`Expected positional bond bonuses and reduced counter damage: ${JSON.stringify(combatMechanicsProbe)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveWaitTurnPromptState = await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
const state = window.__HEROS_DEBUG__?.battle();
|
||||||
|
const lastAlly = state?.units?.find((unit) => unit.id === 'zhang-fei');
|
||||||
|
|
||||||
|
if (!scene || !state || !lastAlly) {
|
||||||
|
throw new Error('Expected Zhang Fei and battle scene for move-wait turn prompt test.');
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.hideTurnEndPrompt();
|
||||||
|
scene.clearMarkers();
|
||||||
|
scene.hideCommandMenu();
|
||||||
|
scene.actedUnitIds = new Set(state.units.filter((unit) => unit.faction === 'ally' && unit.hp > 0 && unit.id !== lastAlly.id).map((unit) => unit.id));
|
||||||
|
scene.phase = 'idle';
|
||||||
|
scene.selectedUnit = undefined;
|
||||||
|
scene.pendingMove = undefined;
|
||||||
|
|
||||||
|
const liveLastAlly = scene.debugUnitById(lastAlly.id);
|
||||||
|
if (!liveLastAlly) {
|
||||||
|
throw new Error('Expected live Zhang Fei unit for move-wait turn prompt test.');
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.selectUnit(liveLastAlly);
|
||||||
|
scene.moveSelectedUnit(liveLastAlly.x, liveLastAlly.y);
|
||||||
|
scene.completeUnitAction(liveLastAlly, 'wait');
|
||||||
|
return window.__HEROS_DEBUG__?.battle();
|
||||||
|
});
|
||||||
|
if (!moveWaitTurnPromptState?.turnPromptVisible || moveWaitTurnPromptState.phase !== 'idle') {
|
||||||
|
throw new Error(`Expected move-wait completion to show turn-end prompt: ${JSON.stringify(moveWaitTurnPromptState)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const scene = window.__HEROS_GAME__?.scene.getScene('BattleScene');
|
||||||
|
if (!scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scene.actedUnitIds.clear();
|
||||||
|
scene.selectedUnit = undefined;
|
||||||
|
scene.pendingMove = undefined;
|
||||||
|
scene.targetingAction = undefined;
|
||||||
|
scene.selectedUsable = undefined;
|
||||||
|
scene.phase = 'idle';
|
||||||
|
scene.clearMarkers();
|
||||||
|
scene.hideCommandMenu();
|
||||||
|
scene.hideTurnEndPrompt();
|
||||||
|
});
|
||||||
|
|
||||||
await page.mouse.click(260, 300, { button: 'right' });
|
await page.mouse.click(260, 300, { button: 'right' });
|
||||||
await page.waitForTimeout(120);
|
await page.waitForTimeout(120);
|
||||||
await page.mouse.click(182, 230);
|
await page.mouse.click(182, 230);
|
||||||
@@ -407,12 +494,15 @@ try {
|
|||||||
if (
|
if (
|
||||||
secondBattleState.camera?.mapWidth !== 24 ||
|
secondBattleState.camera?.mapWidth !== 24 ||
|
||||||
secondBattleState.camera?.mapHeight !== 20 ||
|
secondBattleState.camera?.mapHeight !== 20 ||
|
||||||
|
secondBattleState.mapTextureKey !== 'battle-map-second' ||
|
||||||
|
!secondBattleState.mapTextureReady ||
|
||||||
|
!secondBattleState.mapBackgroundReady ||
|
||||||
secondEnemies.length < 10 ||
|
secondEnemies.length < 10 ||
|
||||||
!secondEnemyBehaviors.has('aggressive') ||
|
!secondEnemyBehaviors.has('aggressive') ||
|
||||||
!secondEnemyBehaviors.has('guard') ||
|
!secondEnemyBehaviors.has('guard') ||
|
||||||
!secondEnemyBehaviors.has('hold')
|
!secondEnemyBehaviors.has('hold')
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected expanded second battle map and mixed enemy AI: ${JSON.stringify(secondBattleState)}`);
|
throw new Error(`Expected loaded expanded second battle map and mixed enemy AI: ${JSON.stringify(secondBattleState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||||
@@ -1830,12 +1920,11 @@ try {
|
|||||||
!eighteenthEnemyBehaviors.has('hold') ||
|
!eighteenthEnemyBehaviors.has('hold') ||
|
||||||
!eighteenthEnemies.some((unit) => unit.id === 'bowang-leader-xiahou-dun') ||
|
!eighteenthEnemies.some((unit) => unit.id === 'bowang-leader-xiahou-dun') ||
|
||||||
!eighteenthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
!eighteenthAllies.some((unit) => unit.id === 'zhuge-liang') ||
|
||||||
!treasureLabels.has('자웅일대검 공명') ||
|
|
||||||
(!treasureLabels.has('청룡언월도 무력 우위') && !treasureLabels.has('청룡언월도 대도')) ||
|
(!treasureLabels.has('청룡언월도 무력 우위') && !treasureLabels.has('청룡언월도 대도')) ||
|
||||||
!treasureLabels.has('장팔사모 치명') ||
|
!treasureLabels.has('장팔사모 치명') ||
|
||||||
!treasureLabels.has('백우선 책략')
|
!treasureLabels.has('백우선 책략')
|
||||||
) {
|
) {
|
||||||
throw new Error(`Expected eighteenth battle to use Bowang map, Xiahou Dun objective, mixed AI, deployed Zhuge Liang, and treasure equipment effects: ${JSON.stringify(eighteenthBattleState)}`);
|
throw new Error(`Expected eighteenth battle to use Bowang map, Xiahou Dun objective, mixed AI, deployed Zhuge Liang, and non-positional treasure equipment effects: ${JSON.stringify(eighteenthBattleState)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
await page.evaluate(() => window.__HEROS_DEBUG__?.forceBattleOutcome('victory'));
|
||||||
|
|||||||
BIN
src/assets/images/story/06-yellow-turban-pursuit.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/images/story/07-anti-dong-pass.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
src/assets/images/story/08-xuzhou-handover.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/images/story/09-wandering-road.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
src/assets/images/story/10-wolong-cottage.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/assets/images/story/11-red-cliffs-fire.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/assets/images/story/12-yizhou-mountain-pass.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/assets/images/story/13-northern-expedition.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/images/story/14-nanzhong-pacification.png
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/assets/images/story/15-yiling-baidi-aftermath.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
@@ -1026,6 +1026,13 @@ type BondState = BattleBond & {
|
|||||||
battleExp: number;
|
battleExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BondCombatBonus = {
|
||||||
|
damageBonus: number;
|
||||||
|
chainRate: number;
|
||||||
|
label?: string;
|
||||||
|
partnerIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type UnitBattleStats = {
|
type UnitBattleStats = {
|
||||||
damageDealt: number;
|
damageDealt: number;
|
||||||
damageTaken: number;
|
damageTaken: number;
|
||||||
@@ -1146,6 +1153,7 @@ type CombatPreview = {
|
|||||||
damage: number;
|
damage: number;
|
||||||
bondDamageBonus: number;
|
bondDamageBonus: number;
|
||||||
bondLabel?: string;
|
bondLabel?: string;
|
||||||
|
bondPartnerIds: string[];
|
||||||
equipmentDamageBonus: number;
|
equipmentDamageBonus: number;
|
||||||
equipmentDamageReduction: number;
|
equipmentDamageReduction: number;
|
||||||
equipmentHitBonus: number;
|
equipmentHitBonus: number;
|
||||||
@@ -3289,8 +3297,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.selectedUnit = undefined;
|
this.selectedUnit = undefined;
|
||||||
this.pendingMove = undefined;
|
this.pendingMove = undefined;
|
||||||
this.phase = 'idle';
|
this.phase = 'idle';
|
||||||
this.clearMarkers();
|
|
||||||
this.hideCommandMenu();
|
this.hideCommandMenu();
|
||||||
|
this.showEnemyUnitThreatRange(unit);
|
||||||
this.renderUnitDetail(unit, this.enemyDetailMessage(unit));
|
this.renderUnitDetail(unit, this.enemyDetailMessage(unit));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3377,7 +3385,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.moveUnitView(unit, x, y, view);
|
this.moveUnitView(unit, x, y, view);
|
||||||
}
|
}
|
||||||
this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY);
|
this.showCommandMenu(unit, pointer?.x ?? targetX, pointer?.y ?? targetY);
|
||||||
this.renderUnitDetail(unit, '공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.');
|
const finalAllyHint = this.remainingAllyCount() === 1 ? '\n마지막 미행동 장수입니다. 대기를 선택하면 턴 종료 확인이 열립니다.' : '';
|
||||||
|
this.renderUnitDetail(unit, `공격, 책략, 도구, 대기 중 하나를 선택하세요.\n우클릭하면 이동을 취소합니다.${finalAllyHint}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private canMoveTo(unit: UnitData, x: number, y: number) {
|
private canMoveTo(unit: UnitData, x: number, y: number) {
|
||||||
@@ -3503,6 +3512,84 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showEnemyUnitThreatRange(enemy: UnitData) {
|
||||||
|
this.clearMarkers();
|
||||||
|
const behavior = this.enemyBehavior(enemy);
|
||||||
|
const moveTiles = this.reachableTiles(enemy);
|
||||||
|
const threatTiles = this.enemyThreatTilesFor(enemy, moveTiles);
|
||||||
|
|
||||||
|
moveTiles.forEach((tile) => this.renderEnemyMoveMarker(enemy, tile.x, tile.y));
|
||||||
|
threatTiles.forEach((tile) => this.renderEnemyUnitThreatMarker(enemy, tile.x, tile.y));
|
||||||
|
this.renderSituationPanel(
|
||||||
|
[
|
||||||
|
`${enemy.name} 대응 범위`,
|
||||||
|
`이동 ${moveTiles.length}칸 · 공격 ${threatTiles.length}칸`,
|
||||||
|
`${this.enemyBehaviorLabel(behavior)} 태세 · 이동 ${enemy.move} · 사거리 ${this.attackRange(enemy)}`
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enemyThreatTilesFor(enemy: UnitData, moveTiles = this.reachableTiles(enemy)) {
|
||||||
|
const threats = new Map<string, { x: number; y: number }>();
|
||||||
|
const origins = [{ x: enemy.x, y: enemy.y, cost: 0 }, ...moveTiles];
|
||||||
|
const range = this.attackRange(enemy);
|
||||||
|
origins.forEach((origin) => {
|
||||||
|
this.tilesWithinDistance(origin.x, origin.y, range).forEach(({ x, y }) => {
|
||||||
|
if (!this.isThreatTargetTile(x, y, enemy.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
threats.set(this.tileKey(x, y), { x, y });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(threats.values()).sort((a, b) => a.y - b.y || a.x - b.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEnemyMoveMarker(enemy: UnitData, x: number, y: number) {
|
||||||
|
const marker = this.add.rectangle(this.tileTopLeftX(x), this.tileTopLeftY(y), this.layout.tileSize, this.layout.tileSize, palette.blue, 0.16);
|
||||||
|
marker.setData('tileX', x);
|
||||||
|
marker.setData('tileY', y);
|
||||||
|
marker.setData('markerType', 'enemy-move');
|
||||||
|
marker.setOrigin(0);
|
||||||
|
marker.setStrokeStyle(1, palette.blue, 0.42);
|
||||||
|
marker.setDepth(4.15);
|
||||||
|
if (this.mapMask) {
|
||||||
|
marker.setMask(this.mapMask);
|
||||||
|
}
|
||||||
|
marker.setVisible(this.isTileVisible(x, y));
|
||||||
|
marker.setInteractive({ useHandCursor: true });
|
||||||
|
marker.on('pointerover', () => {
|
||||||
|
marker.setFillStyle(palette.blue, 0.26);
|
||||||
|
this.renderUnitDetail(enemy, `${this.enemyDetailMessage(enemy)}\n\n이동 가능 칸`);
|
||||||
|
});
|
||||||
|
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.16));
|
||||||
|
marker.on('pointerdown', () => this.renderUnitDetail(enemy, `${this.enemyDetailMessage(enemy)}\n\n이동 가능 칸`));
|
||||||
|
this.markers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEnemyUnitThreatMarker(enemy: UnitData, x: number, y: number) {
|
||||||
|
const behavior = this.enemyBehavior(enemy);
|
||||||
|
const color = this.threatColor(behavior);
|
||||||
|
const marker = this.add.rectangle(this.tileTopLeftX(x), this.tileTopLeftY(y), this.layout.tileSize, this.layout.tileSize, color, 0.28);
|
||||||
|
marker.setData('tileX', x);
|
||||||
|
marker.setData('tileY', y);
|
||||||
|
marker.setData('markerType', 'threat');
|
||||||
|
marker.setOrigin(0);
|
||||||
|
marker.setStrokeStyle(1, 0xffd1a1, 0.64);
|
||||||
|
marker.setDepth(4.65);
|
||||||
|
if (this.mapMask) {
|
||||||
|
marker.setMask(this.mapMask);
|
||||||
|
}
|
||||||
|
marker.setVisible(this.isTileVisible(x, y));
|
||||||
|
marker.setInteractive({ useHandCursor: true });
|
||||||
|
marker.on('pointerover', () => {
|
||||||
|
marker.setFillStyle(color, 0.46);
|
||||||
|
this.renderUnitDetail(enemy, `${this.enemyDetailMessage(enemy)}\n\n이동 후 공격 가능 칸`);
|
||||||
|
});
|
||||||
|
marker.on('pointerout', () => marker.setFillStyle(color, 0.28));
|
||||||
|
marker.on('pointerdown', () => this.renderUnitDetail(enemy, `${this.enemyDetailMessage(enemy)}\n\n이동 후 공격 가능 칸`));
|
||||||
|
this.markers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
private enemyThreatTiles() {
|
private enemyThreatTiles() {
|
||||||
const threats = new Map<string, ThreatTile>();
|
const threats = new Map<string, ThreatTile>();
|
||||||
battleUnits
|
battleUnits
|
||||||
@@ -4037,6 +4124,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.phase = 'animating';
|
this.phase = 'animating';
|
||||||
const result = this.resolveCombatAction(attacker, target, action, false, usable);
|
const result = this.resolveCombatAction(attacker, target, action, false, usable);
|
||||||
this.clearMarkers();
|
this.clearMarkers();
|
||||||
|
await this.showBondMapEffect(result);
|
||||||
await this.playCombatCutIn(result);
|
await this.playCombatCutIn(result);
|
||||||
result.counter = this.resolveCounterAttack(result);
|
result.counter = this.resolveCounterAttack(result);
|
||||||
if (result.counter) {
|
if (result.counter) {
|
||||||
@@ -4127,9 +4215,11 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
remaining > 0
|
remaining > 0
|
||||||
? `${remaining}명의 아군이 아직 행동할 수 있습니다.`
|
? `${remaining}명의 아군이 아직 행동할 수 있습니다.`
|
||||||
: '모든 아군이 행동했습니다. 턴 종료 여부를 선택하세요.';
|
: '모든 아군이 행동했습니다. 턴 종료 여부를 선택하세요.';
|
||||||
this.renderRosterPanel('ally', `${message}\n${turnHint}`);
|
const completionMessage = `${message}\n${turnHint}`;
|
||||||
|
this.renderRosterPanel('ally', completionMessage);
|
||||||
|
|
||||||
if (remaining === 0) {
|
if (remaining === 0) {
|
||||||
|
this.renderSituationPanel(completionMessage);
|
||||||
this.showTurnEndPrompt(message);
|
this.showTurnEndPrompt(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5034,15 +5124,16 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return this.tileDistance(user, target) <= usable.range;
|
return this.tileDistance(user, target) <= usable.range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatPreview(attacker: UnitData, defender: UnitData, action: DamageCommand, usable?: BattleUsable): CombatPreview {
|
private combatPreview(attacker: UnitData, defender: UnitData, action: DamageCommand, usable?: BattleUsable, isCounter = false): CombatPreview {
|
||||||
const terrain = battleMap.terrain[defender.y][defender.x];
|
const terrain = battleMap.terrain[defender.y][defender.x];
|
||||||
const terrainRule = getTerrainRule(terrain);
|
const terrainRule = getTerrainRule(terrain);
|
||||||
const attackPower = this.actionPower(attacker, action, usable);
|
const attackPower = this.actionPower(attacker, action, usable);
|
||||||
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
const defensePower = this.actionDefense(defender, action, terrainRule.defenseBonus);
|
||||||
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id) : { damageBonus: 0, chainRate: 0 };
|
const bondBonus = this.activeBondCombatBonus(attacker, defender, action, isCounter);
|
||||||
const equipmentEffect = this.combatEquipmentEffect(attacker, defender, action, usable);
|
const equipmentEffect = this.combatEquipmentEffect(attacker, defender, action, usable, bondBonus);
|
||||||
const totalDamageBonus = bondBonus.damageBonus + equipmentEffect.damageBonus - equipmentEffect.damageReduction;
|
const totalDamageBonus = bondBonus.damageBonus + equipmentEffect.damageBonus - equipmentEffect.damageReduction;
|
||||||
const damage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + totalDamageBonus / 100)), 3, defender.maxHp);
|
const baseDamage = Phaser.Math.Clamp(Math.round((attackPower - defensePower) * (1 + totalDamageBonus / 100)), 3, defender.maxHp);
|
||||||
|
const damage = isCounter ? Phaser.Math.Clamp(Math.round(baseDamage * 0.45), 1, defender.maxHp) : baseDamage;
|
||||||
const defenderTerrainRating = getUnitClass(defender.classKey).terrainRatings[terrain] ?? 100;
|
const defenderTerrainRating = getUnitClass(defender.classKey).terrainRatings[terrain] ?? 100;
|
||||||
const terrainHitPenalty = Math.floor(terrainRule.defenseBonus / 2) + Math.max(0, Math.floor((defenderTerrainRating - 100) / 4));
|
const terrainHitPenalty = Math.floor(terrainRule.defenseBonus / 2) + Math.max(0, Math.floor((defenderTerrainRating - 100) / 4));
|
||||||
const hitRate = Phaser.Math.Clamp(
|
const hitRate = Phaser.Math.Clamp(
|
||||||
@@ -5076,6 +5167,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
damage,
|
damage,
|
||||||
bondDamageBonus: bondBonus.damageBonus,
|
bondDamageBonus: bondBonus.damageBonus,
|
||||||
bondLabel: bondBonus.label,
|
bondLabel: bondBonus.label,
|
||||||
|
bondPartnerIds: bondBonus.partnerIds,
|
||||||
equipmentDamageBonus: equipmentEffect.damageBonus,
|
equipmentDamageBonus: equipmentEffect.damageBonus,
|
||||||
equipmentDamageReduction: equipmentEffect.damageReduction,
|
equipmentDamageReduction: equipmentEffect.damageReduction,
|
||||||
equipmentHitBonus: equipmentEffect.hitBonus,
|
equipmentHitBonus: equipmentEffect.hitBonus,
|
||||||
@@ -5122,7 +5214,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false, usable?: BattleUsable): CombatResult {
|
private resolveCombatAction(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false, usable?: BattleUsable): CombatResult {
|
||||||
const preview = this.combatPreview(attacker, defender, action, usable);
|
const preview = this.combatPreview(attacker, defender, action, usable, isCounter);
|
||||||
const previousDefenderHp = defender.hp;
|
const previousDefenderHp = defender.hp;
|
||||||
const hit = this.rollPercent(preview.hitRate);
|
const hit = this.rollPercent(preview.hitRate);
|
||||||
const critical = hit && this.rollPercent(preview.criticalRate);
|
const critical = hit && this.rollPercent(preview.criticalRate);
|
||||||
@@ -5245,7 +5337,13 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return Math.min(target.maxHp - target.hp, Math.max(1, usable.power + bonus + equipmentBonus));
|
return Math.min(target.maxHp - target.hp, Math.max(1, usable.power + bonus + equipmentBonus));
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatEquipmentEffect(attacker: UnitData, defender: UnitData, action: DamageCommand, usable?: BattleUsable) {
|
private combatEquipmentEffect(
|
||||||
|
attacker: UnitData,
|
||||||
|
defender: UnitData,
|
||||||
|
action: DamageCommand,
|
||||||
|
usable?: BattleUsable,
|
||||||
|
bondBonus: BondCombatBonus = this.activeBondCombatBonus(attacker, defender, action)
|
||||||
|
) {
|
||||||
const labels: string[] = [];
|
const labels: string[] = [];
|
||||||
let damageBonus = 0;
|
let damageBonus = 0;
|
||||||
let damageReduction = 0;
|
let damageReduction = 0;
|
||||||
@@ -5256,7 +5354,6 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const armor = getItem(defender.equipment.armor.itemId);
|
const armor = getItem(defender.equipment.armor.itemId);
|
||||||
const accessory = getItem(attacker.equipment.accessory.itemId);
|
const accessory = getItem(attacker.equipment.accessory.itemId);
|
||||||
const defenderAccessory = getItem(defender.equipment.accessory.itemId);
|
const defenderAccessory = getItem(defender.equipment.accessory.itemId);
|
||||||
const bondBonus = attacker.faction === 'ally' ? this.strongestBondBonus(attacker.id) : { damageBonus: 0, chainRate: 0 };
|
|
||||||
|
|
||||||
if (action === 'attack' && weapon.id === 'twin-oath-blades' && bondBonus.chainRate > 0) {
|
if (action === 'attack' && weapon.id === 'twin-oath-blades' && bondBonus.chainRate > 0) {
|
||||||
damageBonus += 6;
|
damageBonus += 6;
|
||||||
@@ -5387,7 +5484,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
private formatCounterLine(result: CombatResult) {
|
private formatCounterLine(result: CombatResult) {
|
||||||
const outcome = result.hit ? `${result.critical ? '치명타로 ' : ''}${result.damage} 피해` : '회피됨';
|
const outcome = result.hit ? `${result.critical ? '치명타로 ' : ''}${result.damage} 피해` : '회피됨';
|
||||||
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
const defeated = result.defeated ? ` / ${result.defender.name} 퇴각` : ` / ${result.defender.name} HP ${result.defender.hp}/${result.defender.maxHp}`;
|
||||||
return `반격: ${result.attacker.name} -> ${result.defender.name} ${outcome}${defeated}`;
|
return `반격(감소): ${result.attacker.name} -> ${result.defender.name} ${outcome}${defeated}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private combatGrowthLines(result: CombatResult) {
|
private combatGrowthLines(result: CombatResult) {
|
||||||
@@ -7114,6 +7211,69 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showBondMapEffect(result: CombatResult) {
|
||||||
|
if (result.bondDamageBonus <= 0 || result.bondPartnerIds.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const attackerView = this.unitViews.get(result.attacker.id);
|
||||||
|
if (!attackerView || !this.isTileVisible(result.attacker.x, result.attacker.y)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const partnerUnits = result.bondPartnerIds
|
||||||
|
.map((partnerId) => battleUnits.find((unit) => unit.id === partnerId && unit.hp > 0))
|
||||||
|
.filter((unit): unit is UnitData => Boolean(unit));
|
||||||
|
const visiblePartners = partnerUnits.filter((unit) => this.isTileVisible(unit.x, unit.y) && this.unitViews.has(unit.id));
|
||||||
|
if (visiblePartners.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
soundDirector.playEffect('strategy-cast', { volume: 0.18, rate: 1.14, stopAfterMs: 520 });
|
||||||
|
|
||||||
|
const objects: Phaser.GameObjects.GameObject[] = [];
|
||||||
|
const line = this.add.graphics();
|
||||||
|
line.setDepth(34);
|
||||||
|
line.lineStyle(4, 0xffdf7b, 0.78);
|
||||||
|
visiblePartners.forEach((partner) => {
|
||||||
|
const partnerView = this.unitViews.get(partner.id);
|
||||||
|
if (!partnerView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line.beginPath();
|
||||||
|
line.moveTo(attackerView.sprite.x, attackerView.sprite.y - this.layout.tileSize * 0.28);
|
||||||
|
line.lineTo(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.28);
|
||||||
|
line.strokePath();
|
||||||
|
objects.push(this.createBondBadge(partnerView.sprite.x, partnerView.sprite.y - this.layout.tileSize * 0.86, `${partner.name} ♥ 공명`, 35));
|
||||||
|
});
|
||||||
|
objects.push(line);
|
||||||
|
objects.push(this.createBondBadge(attackerView.sprite.x, attackerView.sprite.y - this.layout.tileSize * 0.96, '연계 발동', 36));
|
||||||
|
|
||||||
|
this.tweens.add({ targets: objects, alpha: 0, y: '-=12', duration: 620, ease: 'Sine.easeOut' });
|
||||||
|
return this.delay(660).then(() => objects.forEach((object) => object.destroy()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createBondBadge(x: number, y: number, label: string, depth: number) {
|
||||||
|
const container = this.add.container(x, y);
|
||||||
|
container.setDepth(depth);
|
||||||
|
const width = Math.max(84, label.length * 13 + 22);
|
||||||
|
const bg = this.add.rectangle(0, 0, width, 30, 0x2a1820, 0.92);
|
||||||
|
bg.setStrokeStyle(2, 0xffdf7b, 0.92);
|
||||||
|
const glow = this.add.circle(-width / 2 + 17, 0, 10, 0xff6f91, 0.82);
|
||||||
|
const text = this.add.text(5, 0, label, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#fff2b8',
|
||||||
|
fontStyle: '700',
|
||||||
|
stroke: '#30100d',
|
||||||
|
strokeThickness: 3
|
||||||
|
});
|
||||||
|
text.setOrigin(0.5);
|
||||||
|
container.add([bg, glow, text]);
|
||||||
|
this.tweens.add({ targets: glow, scale: 1.35, alpha: 0.28, yoyo: true, repeat: 1, duration: 180 });
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
private showBondCombatEffect(result: CombatResult, textX: number, textY: number, auraX: number, auraY: number, depth: number) {
|
private showBondCombatEffect(result: CombatResult, textX: number, textY: number, auraX: number, auraY: number, depth: number) {
|
||||||
if (!result.bondLabel || result.bondDamageBonus <= 0) {
|
if (!result.bondLabel || result.bondDamageBonus <= 0) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@@ -7503,9 +7663,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private recordBondAttackIntent(unit: UnitData, target: UnitData) {
|
private recordBondAttackIntent(unit: UnitData, target: UnitData) {
|
||||||
const partners = this.attackIntents
|
const partners = this.activeBondCombatBonus(unit, target, 'attack').partnerIds;
|
||||||
.filter((intent) => intent.targetId === target.id && intent.attackerId !== unit.id)
|
|
||||||
.map((intent) => intent.attackerId);
|
|
||||||
const growthResults: BondGrowthResult[] = [];
|
const growthResults: BondGrowthResult[] = [];
|
||||||
|
|
||||||
partners.forEach((partnerId) => {
|
partners.forEach((partnerId) => {
|
||||||
@@ -7549,9 +7707,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
return `${unit.name} 공격 명령 완료\n공격 가능한 적이 없습니다.\n${this.formatEquipmentGrowth(weaponGrowth)}`;
|
return `${unit.name} 공격 명령 완료\n공격 가능한 적이 없습니다.\n${this.formatEquipmentGrowth(weaponGrowth)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partners = this.attackIntents
|
const bondBonus = this.activeBondCombatBonus(unit, target, 'attack');
|
||||||
.filter((intent) => intent.targetId === target.id && intent.attackerId !== unit.id)
|
const partners = bondBonus.partnerIds;
|
||||||
.map((intent) => intent.attackerId);
|
|
||||||
let gained = 0;
|
let gained = 0;
|
||||||
|
|
||||||
partners.forEach((partnerId) => {
|
partners.forEach((partnerId) => {
|
||||||
@@ -7571,8 +7728,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
this.attackIntents = this.attackIntents.filter((intent) => intent.attackerId !== unit.id);
|
this.attackIntents = this.attackIntents.filter((intent) => intent.attackerId !== unit.id);
|
||||||
this.attackIntents.push({ attackerId: unit.id, targetId: target.id });
|
this.attackIntents.push({ attackerId: unit.id, targetId: target.id });
|
||||||
|
|
||||||
const bonus = this.strongestBondBonus(unit.id);
|
const chain = bondBonus.chainRate > 0 ? `\n공명 보너스: 피해 +${bondBonus.damageBonus}% / 연계 ${bondBonus.chainRate}%` : '';
|
||||||
const chain = bonus.chainRate > 0 ? `\n공명 보너스: 피해 +${bonus.damageBonus}% / 연계 ${bonus.chainRate}%` : '';
|
|
||||||
const exp = gained > 0 ? `\n같은 목표를 노린 공명 경험치 +${gained}` : '\n같은 목표를 노린 장수가 생기면 공명 경험치가 쌓입니다.';
|
const exp = gained > 0 ? `\n같은 목표를 노린 공명 경험치 +${gained}` : '\n같은 목표를 노린 장수가 생기면 공명 경험치가 쌓입니다.';
|
||||||
return `${unit.name} 공격 명령 완료\n예상 목표: ${target.name}\n${this.formatEquipmentGrowth(weaponGrowth)}${chain}${exp}`;
|
return `${unit.name} 공격 명령 완료\n예상 목표: ${target.name}\n${this.formatEquipmentGrowth(weaponGrowth)}${chain}${exp}`;
|
||||||
}
|
}
|
||||||
@@ -7702,6 +7858,47 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private activeBondCombatBonus(attacker: UnitData, defender: UnitData, action: DamageCommand, isCounter = false): BondCombatBonus {
|
||||||
|
if (isCounter || action !== 'attack' || attacker.faction !== 'ally') {
|
||||||
|
return { damageBonus: 0, chainRate: 0, partnerIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = Array.from(this.bondStates.values())
|
||||||
|
.map((bond) => {
|
||||||
|
if (!bond.unitIds.includes(attacker.id)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const partnerId = bond.unitIds.find((unitId) => unitId !== attacker.id);
|
||||||
|
const partner = partnerId ? battleUnits.find((unit) => unit.id === partnerId && unit.hp > 0) : undefined;
|
||||||
|
if (!partner || partner.faction !== attacker.faction || !this.canBondPartnerJoinAttack(attacker, partner, defender)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return { bond, partner };
|
||||||
|
})
|
||||||
|
.filter((candidate): candidate is { bond: BondState; partner: UnitData } => Boolean(candidate))
|
||||||
|
.sort((a, b) => b.bond.level - a.bond.level);
|
||||||
|
|
||||||
|
const strongest = candidates[0];
|
||||||
|
if (!strongest) {
|
||||||
|
return { damageBonus: 0, chainRate: 0, partnerIds: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [first, second] = strongest.bond.unitIds.map((id) => this.unitName(id));
|
||||||
|
return {
|
||||||
|
damageBonus: Math.floor(strongest.bond.level / 20) * 3,
|
||||||
|
chainRate: strongest.bond.level >= 70 ? 18 : strongest.bond.level >= 50 ? 8 : 0,
|
||||||
|
label: `${strongest.bond.title}: ${first}·${second}`,
|
||||||
|
partnerIds: [strongest.partner.id]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private canBondPartnerJoinAttack(attacker: UnitData, partner: UnitData, defender: UnitData) {
|
||||||
|
if (this.tileDistance(attacker, partner) <= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return this.canUseDamageCommand(partner, defender, 'attack');
|
||||||
|
}
|
||||||
|
|
||||||
private strongestBondBonus(unitId: string) {
|
private strongestBondBonus(unitId: string) {
|
||||||
const related = Array.from(this.bondStates.values()).filter((bond) => bond.unitIds.includes(unitId));
|
const related = Array.from(this.bondStates.values()).filter((bond) => bond.unitIds.includes(unitId));
|
||||||
const strongest = related.sort((a, b) => b.level - a.level)[0];
|
const strongest = related.sort((a, b) => b.level - a.level)[0];
|
||||||
@@ -8858,9 +9055,14 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
activeFaction: this.activeFaction,
|
activeFaction: this.activeFaction,
|
||||||
phase: this.phase,
|
phase: this.phase,
|
||||||
battleOutcome: this.battleOutcome ?? null,
|
battleOutcome: this.battleOutcome ?? null,
|
||||||
|
mapTextureKey: battleScenario.mapTextureKey,
|
||||||
|
mapTextureReady: this.textures.exists(battleScenario.mapTextureKey),
|
||||||
|
mapBackgroundReady: this.mapBackground?.texture.key === battleScenario.mapTextureKey,
|
||||||
resultVisible: this.resultObjects.length > 0,
|
resultVisible: this.resultObjects.length > 0,
|
||||||
|
turnPromptVisible: this.turnPromptObjects.length > 0,
|
||||||
markerCount: this.markers.length,
|
markerCount: this.markers.length,
|
||||||
threatMarkerCount: this.markers.filter((marker) => marker.getData('markerType') === 'threat').length,
|
threatMarkerCount: this.markers.filter((marker) => marker.getData('markerType') === 'threat').length,
|
||||||
|
enemyMoveMarkerCount: this.markers.filter((marker) => marker.getData('markerType') === 'enemy-move').length,
|
||||||
camera: {
|
camera: {
|
||||||
x: this.cameraTileX,
|
x: this.cameraTileX,
|
||||||
y: this.cameraTileY,
|
y: this.cameraTileY,
|
||||||
@@ -8887,6 +9089,7 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
itemStocks: this.serializeItemStocks(),
|
itemStocks: this.serializeItemStocks(),
|
||||||
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
|
battleBuffs: Array.from(this.battleBuffs.values()).map((buff) => ({ ...buff })),
|
||||||
treasureEffectPreviews: this.debugTreasureEffectPreviews(),
|
treasureEffectPreviews: this.debugTreasureEffectPreviews(),
|
||||||
|
combatMechanicsProbe: this.debugCombatMechanicsProbe(),
|
||||||
units: battleUnits.map((unit) => ({
|
units: battleUnits.map((unit) => ({
|
||||||
id: unit.id,
|
id: unit.id,
|
||||||
name: unit.name,
|
name: unit.name,
|
||||||
@@ -8914,6 +9117,61 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugUnitById(unitId: string) {
|
||||||
|
if (!import.meta.env.DEV) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return battleUnits.find((unit) => unit.id === unitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private debugCombatMechanicsProbe() {
|
||||||
|
const bond = Array.from(this.bondStates.values()).find((candidate) => {
|
||||||
|
const units = candidate.unitIds.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0));
|
||||||
|
return units.length === 2 && units.every((unit) => unit?.faction === 'ally') && Math.floor(candidate.level / 20) * 3 > 0;
|
||||||
|
});
|
||||||
|
const attacker = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[0] && unit.hp > 0) : undefined;
|
||||||
|
const partner = bond ? battleUnits.find((unit) => unit.id === bond.unitIds[1] && unit.hp > 0) : undefined;
|
||||||
|
const enemy = battleUnits.find((unit) => unit.faction === 'enemy' && unit.hp > 0);
|
||||||
|
if (!bond || !attacker || !partner || !enemy) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = partner.x <= battleMap.width - 3 ? 1 : -1;
|
||||||
|
const nearX = partner.x + direction;
|
||||||
|
const edgeX = partner.x + direction * 2;
|
||||||
|
if (!this.isInBounds(nearX, partner.y) || !this.isInBounds(edgeX, partner.y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const farAttackerX = Math.abs(battleMap.width - 2 - partner.x) > 2 ? battleMap.width - 2 : 1;
|
||||||
|
const farDefenderX = farAttackerX > 0 ? farAttackerX - 1 : farAttackerX + 1;
|
||||||
|
if (!this.isInBounds(farAttackerX, partner.y) || !this.isInBounds(farDefenderX, partner.y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjacentPreview = this.combatPreview({ ...attacker, x: nearX, y: partner.y }, { ...enemy, x: edgeX, y: partner.y }, 'attack');
|
||||||
|
const sameTargetPreview = this.combatPreview({ ...attacker, x: edgeX, y: partner.y }, { ...enemy, x: nearX, y: partner.y }, 'attack');
|
||||||
|
const farPreview = this.combatPreview({ ...attacker, x: farAttackerX, y: partner.y }, { ...enemy, x: farDefenderX, y: partner.y }, 'attack');
|
||||||
|
const normalIncomingPreview = this.combatPreview(enemy, attacker, 'attack');
|
||||||
|
const counterIncomingPreview = this.combatPreview(enemy, attacker, 'attack', undefined, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
bondId: bond.id,
|
||||||
|
attackerId: attacker.id,
|
||||||
|
partnerId: partner.id,
|
||||||
|
enemyId: enemy.id,
|
||||||
|
adjacentBondDamageBonus: adjacentPreview.bondDamageBonus,
|
||||||
|
adjacentPartnerIds: adjacentPreview.bondPartnerIds,
|
||||||
|
sameTargetBondDamageBonus: sameTargetPreview.bondDamageBonus,
|
||||||
|
sameTargetPartnerIds: sameTargetPreview.bondPartnerIds,
|
||||||
|
farBondDamageBonus: farPreview.bondDamageBonus,
|
||||||
|
farPartnerIds: farPreview.bondPartnerIds,
|
||||||
|
normalDamage: normalIncomingPreview.damage,
|
||||||
|
counterDamage: counterIncomingPreview.damage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private debugTreasureEffectPreviews() {
|
private debugTreasureEffectPreviews() {
|
||||||
return ['liu-bei', 'guan-yu', 'zhang-fei', 'zhuge-liang']
|
return ['liu-bei', 'guan-yu', 'zhang-fei', 'zhuge-liang']
|
||||||
.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0))
|
.map((unitId) => battleUnits.find((unit) => unit.id === unitId && unit.hp > 0))
|
||||||
|
|||||||
@@ -2,11 +2,22 @@ import Phaser from 'phaser';
|
|||||||
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
import guanYuPortraitUrl from '../../assets/images/portraits/guan-yu.png';
|
||||||
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
|
import liuBeiPortraitUrl from '../../assets/images/portraits/liu-bei.png';
|
||||||
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
|
import zhangFeiPortraitUrl from '../../assets/images/portraits/zhang-fei.png';
|
||||||
|
import { battleMapAssets } from '../data/battleMapAssets';
|
||||||
import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png';
|
import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png';
|
||||||
import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png';
|
import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png';
|
||||||
import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png';
|
import storyThreeHeroesUrl from '../../assets/images/story/03-three-heroes-meet.png';
|
||||||
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
|
import storyMilitiaUrl from '../../assets/images/story/04-volunteer-militia.png';
|
||||||
import storySortieUrl from '../../assets/images/story/05-first-sortie.png';
|
import storySortieUrl from '../../assets/images/story/05-first-sortie.png';
|
||||||
|
import storyYellowPursuitUrl from '../../assets/images/story/06-yellow-turban-pursuit.png';
|
||||||
|
import storyAntiDongPassUrl from '../../assets/images/story/07-anti-dong-pass.png';
|
||||||
|
import storyXuzhouHandoverUrl from '../../assets/images/story/08-xuzhou-handover.png';
|
||||||
|
import storyWanderingRoadUrl from '../../assets/images/story/09-wandering-road.png';
|
||||||
|
import storyWolongCottageUrl from '../../assets/images/story/10-wolong-cottage.png';
|
||||||
|
import storyRedCliffsFireUrl from '../../assets/images/story/11-red-cliffs-fire.png';
|
||||||
|
import storyYizhouMountainPassUrl from '../../assets/images/story/12-yizhou-mountain-pass.png';
|
||||||
|
import storyNorthernExpeditionUrl from '../../assets/images/story/13-northern-expedition.png';
|
||||||
|
import storyNanzhongPacificationUrl from '../../assets/images/story/14-nanzhong-pacification.png';
|
||||||
|
import storyYilingBaidiAftermathUrl from '../../assets/images/story/15-yiling-baidi-aftermath.png';
|
||||||
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
|
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
|
||||||
import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png';
|
import guanYuActionSheetUrl from '../../assets/images/units/unit-guan-yu-actions.png';
|
||||||
import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png';
|
import guanYuUnitSheetUrl from '../../assets/images/units/unit-guan-yu.png';
|
||||||
@@ -58,10 +69,25 @@ export class BootScene extends Phaser.Scene {
|
|||||||
this.load.image('story-three-heroes', storyThreeHeroesUrl);
|
this.load.image('story-three-heroes', storyThreeHeroesUrl);
|
||||||
this.load.image('story-militia', storyMilitiaUrl);
|
this.load.image('story-militia', storyMilitiaUrl);
|
||||||
this.load.image('story-sortie', storySortieUrl);
|
this.load.image('story-sortie', storySortieUrl);
|
||||||
|
this.load.image('story-yellow-pursuit', storyYellowPursuitUrl);
|
||||||
|
this.load.image('story-anti-dong', storyAntiDongPassUrl);
|
||||||
|
this.load.image('story-xuzhou', storyXuzhouHandoverUrl);
|
||||||
|
this.load.image('story-wandering', storyWanderingRoadUrl);
|
||||||
|
this.load.image('story-wolong', storyWolongCottageUrl);
|
||||||
|
this.load.image('story-red-cliffs', storyRedCliffsFireUrl);
|
||||||
|
this.load.image('story-yizhou', storyYizhouMountainPassUrl);
|
||||||
|
this.load.image('story-northern', storyNorthernExpeditionUrl);
|
||||||
|
this.load.image('story-nanzhong', storyNanzhongPacificationUrl);
|
||||||
|
this.load.image('story-yiling-baidi', storyYilingBaidiAftermathUrl);
|
||||||
|
this.load.image('story-resolve', storyLiuBeiUrl);
|
||||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||||
|
|
||||||
|
Object.entries(battleMapAssets).forEach(([key, url]) => {
|
||||||
|
this.load.image(key, url);
|
||||||
|
});
|
||||||
|
|
||||||
unitSheets.forEach(({ key, url, actionKey, actionUrl }) => {
|
unitSheets.forEach(({ key, url, actionKey, actionUrl }) => {
|
||||||
this.load.spritesheet(key, url, {
|
this.load.spritesheet(key, url, {
|
||||||
frameWidth: unitSheetFrameSize,
|
frameWidth: unitSheetFrameSize,
|
||||||
|
|||||||