Add equipment growth system
This commit is contained in:
@@ -16,15 +16,16 @@ Build a small complete tactical RPG loop:
|
|||||||
- Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio
|
- Cinematic title scene with an original Taoyuan Oath background, subtle motion, menu UI, and first-input audio
|
||||||
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
|
- Cinematic prologue pages with high-resolution story backgrounds, character portraits, non-black scene transitions, and scenario data
|
||||||
- File-based original soft stereo orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
|
- File-based original soft stereo orchestral BGM loops for title, prologue, oath, militia, and battle-prep scenes
|
||||||
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, class and terrain affinity rules, ally/enemy roster tabs, unit detail stat panels, terrain-cost movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, right-click tactical menu, turn ending/reset, resonance bond tracking, and grayscale acted-unit feedback
|
- First battle scene with a high-resolution battlefield background, large 12x12 tactical grid, high-detail pixel-style unit sprites, class and terrain affinity rules, ally/enemy roster tabs, unit detail stat panels, weapon/armor/accessory equipment growth, terrain-cost movement range preview, click-to-move unit movement, cursor-adjacent post-move command popup, right-click move cancel, right-click tactical menu, turn ending/reset, resonance bond tracking, and grayscale acted-unit feedback
|
||||||
- Flow verification script from title to first battle
|
- Flow verification script from title to first battle
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
1. Implement attack range, damage, defeat, victory checks, and real resonance assist attacks
|
1. Implement attack range, damage, defeat, victory checks, and real resonance assist attacks
|
||||||
2. Add strategy and item inventories with usable battle effects
|
2. Apply treasure equipment effects to damage, defense, recovery, and command rules
|
||||||
3. Add a simple enemy AI turn
|
3. Add strategy and item inventories with usable battle effects
|
||||||
4. Add battle result scene and local save/load
|
4. Add a simple enemy AI turn
|
||||||
|
5. Add battle result scene and local save/load
|
||||||
|
|
||||||
## Content Direction
|
## Content Direction
|
||||||
|
|
||||||
|
|||||||
203
src/game/data/battleItems.ts
Normal file
203
src/game/data/battleItems.ts
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
export type EquipmentSlot = 'weapon' | 'armor' | 'accessory';
|
||||||
|
|
||||||
|
export type EquipmentRank = 'common' | 'treasure';
|
||||||
|
|
||||||
|
export type EquipmentState = {
|
||||||
|
itemId: string;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EquipmentSet = Record<EquipmentSlot, EquipmentState>;
|
||||||
|
|
||||||
|
export type ItemDefinition = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slot: EquipmentSlot;
|
||||||
|
rank: EquipmentRank;
|
||||||
|
description: string;
|
||||||
|
attackBonus?: number;
|
||||||
|
defenseBonus?: number;
|
||||||
|
strategyBonus?: number;
|
||||||
|
effects: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const equipmentSlotLabels: Record<EquipmentSlot, string> = {
|
||||||
|
weapon: '무기',
|
||||||
|
armor: '방어구',
|
||||||
|
accessory: '보조구'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const equipmentSlots: EquipmentSlot[] = ['weapon', 'armor', 'accessory'];
|
||||||
|
|
||||||
|
export const itemCatalog: Record<string, ItemDefinition> = {
|
||||||
|
'training-sword': {
|
||||||
|
id: 'training-sword',
|
||||||
|
name: '연습검',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'common',
|
||||||
|
description: '의용군이 가장 쉽게 다룰 수 있는 기본 검.',
|
||||||
|
attackBonus: 1,
|
||||||
|
effects: ['공격 경험치를 안정적으로 쌓는다']
|
||||||
|
},
|
||||||
|
'twin-oath-blades': {
|
||||||
|
id: 'twin-oath-blades',
|
||||||
|
name: '쌍의검',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '도원에서 맹세한 뜻을 담아 벼린 쌍검.',
|
||||||
|
attackBonus: 3,
|
||||||
|
effects: ['공명 관계가 70 이상이면 공격 경험치 +2']
|
||||||
|
},
|
||||||
|
'green-dragon-glaive': {
|
||||||
|
id: 'green-dragon-glaive',
|
||||||
|
name: '청룡월도',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '묵직한 월도로 전열을 갈라내는 관우의 보물 무기.',
|
||||||
|
attackBonus: 5,
|
||||||
|
effects: ['적보다 무력이 높으면 피해 보너스']
|
||||||
|
},
|
||||||
|
'serpent-spear': {
|
||||||
|
id: 'serpent-spear',
|
||||||
|
name: '장팔장창',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '한 번 내지르면 기세까지 꿰뚫는 장비의 긴 창.',
|
||||||
|
attackBonus: 5,
|
||||||
|
effects: ['연속 공격 판정 보정']
|
||||||
|
},
|
||||||
|
'yellow-turban-saber': {
|
||||||
|
id: 'yellow-turban-saber',
|
||||||
|
name: '황건도',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'common',
|
||||||
|
description: '황건 무리가 들고 다니는 거친 도검.',
|
||||||
|
attackBonus: 1,
|
||||||
|
effects: ['평지 전투에 무난하다']
|
||||||
|
},
|
||||||
|
'short-bow': {
|
||||||
|
id: 'short-bow',
|
||||||
|
name: '단궁',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'common',
|
||||||
|
description: '가볍게 쏘기 좋은 짧은 활.',
|
||||||
|
attackBonus: 2,
|
||||||
|
effects: ['궁병 기본 사거리 장비']
|
||||||
|
},
|
||||||
|
'leader-axe': {
|
||||||
|
id: 'leader-axe',
|
||||||
|
name: '두령부',
|
||||||
|
slot: 'weapon',
|
||||||
|
rank: 'common',
|
||||||
|
description: '두령이 위압을 보이기 위해 쓰는 큼직한 도끼.',
|
||||||
|
attackBonus: 3,
|
||||||
|
effects: ['HP가 낮은 적을 압박한다']
|
||||||
|
},
|
||||||
|
'cloth-armor': {
|
||||||
|
id: 'cloth-armor',
|
||||||
|
name: '포의',
|
||||||
|
slot: 'armor',
|
||||||
|
rank: 'common',
|
||||||
|
description: '가벼운 천옷. 민첩함은 해치지 않지만 방어는 낮다.',
|
||||||
|
defenseBonus: 1,
|
||||||
|
effects: ['방어 경험치를 빠르게 시작한다']
|
||||||
|
},
|
||||||
|
'lamellar-armor': {
|
||||||
|
id: 'lamellar-armor',
|
||||||
|
name: '찰갑',
|
||||||
|
slot: 'armor',
|
||||||
|
rank: 'common',
|
||||||
|
description: '작은 철편을 엮어 만든 실전용 갑옷.',
|
||||||
|
defenseBonus: 3,
|
||||||
|
effects: ['근접 견제에 강하다']
|
||||||
|
},
|
||||||
|
'oath-robe': {
|
||||||
|
id: 'oath-robe',
|
||||||
|
name: '도원포',
|
||||||
|
slot: 'armor',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '맹세의 문양을 수놓은 전투복.',
|
||||||
|
defenseBonus: 2,
|
||||||
|
strategyBonus: 1,
|
||||||
|
effects: ['공명 경험치 획득 시 방어 경험치 +1']
|
||||||
|
},
|
||||||
|
'reinforced-lamellar': {
|
||||||
|
id: 'reinforced-lamellar',
|
||||||
|
name: '철린갑',
|
||||||
|
slot: 'armor',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '비늘처럼 맞물린 철편으로 급소를 덮은 보물 갑옷.',
|
||||||
|
defenseBonus: 5,
|
||||||
|
effects: ['방어 시 받는 피해 감소']
|
||||||
|
},
|
||||||
|
'rebel-vest': {
|
||||||
|
id: 'rebel-vest',
|
||||||
|
name: '황건 조끼',
|
||||||
|
slot: 'armor',
|
||||||
|
rank: 'common',
|
||||||
|
description: '두꺼운 천과 가죽을 덧댄 조끼.',
|
||||||
|
defenseBonus: 1,
|
||||||
|
effects: ['숲 지형에서 버티기 좋다']
|
||||||
|
},
|
||||||
|
'peach-charm': {
|
||||||
|
id: 'peach-charm',
|
||||||
|
name: '복숭아 부적',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '도원결의의 상징을 새긴 작은 부적.',
|
||||||
|
strategyBonus: 2,
|
||||||
|
effects: ['턴 시작 시 낮은 병력을 조금 회복할 예정']
|
||||||
|
},
|
||||||
|
'war-manual': {
|
||||||
|
id: 'war-manual',
|
||||||
|
name: '병법서',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '진형과 공격 타이밍을 적은 병법서.',
|
||||||
|
strategyBonus: 2,
|
||||||
|
effects: ['공명 연계 판정 보정']
|
||||||
|
},
|
||||||
|
'bravery-token': {
|
||||||
|
id: 'bravery-token',
|
||||||
|
name: '호담패',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '물러서지 않는 기개를 새긴 패.',
|
||||||
|
attackBonus: 1,
|
||||||
|
effects: ['HP가 낮을수록 공격 의지가 오른다']
|
||||||
|
},
|
||||||
|
'grain-pouch': {
|
||||||
|
id: 'grain-pouch',
|
||||||
|
name: '군량 주머니',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'common',
|
||||||
|
description: '전투 중 병사들의 허기를 달래는 보조 도구.',
|
||||||
|
effects: ['장기전 회복 아이템과 연동 예정']
|
||||||
|
},
|
||||||
|
'yellow-scarf-charm': {
|
||||||
|
id: 'yellow-scarf-charm',
|
||||||
|
name: '황건 부적',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'common',
|
||||||
|
description: '황건 무리가 신념의 증표로 지닌 부적.',
|
||||||
|
effects: ['사기 유지에 도움을 준다']
|
||||||
|
},
|
||||||
|
'wind-quiver': {
|
||||||
|
id: 'wind-quiver',
|
||||||
|
name: '바람 화살통',
|
||||||
|
slot: 'accessory',
|
||||||
|
rank: 'treasure',
|
||||||
|
description: '가벼운 화살깃과 균형추가 달린 보물 화살통.',
|
||||||
|
attackBonus: 1,
|
||||||
|
effects: ['궁병의 명중 보정']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getItem(itemId: string) {
|
||||||
|
return itemCatalog[itemId] ?? itemCatalog['training-sword'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equipmentExpToNext(level: number) {
|
||||||
|
return 50 + Math.min(level, 9) * 20;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { TerrainType, UnitClassKey } from './battleRules';
|
import type { TerrainType, UnitClassKey } from './battleRules';
|
||||||
|
import type { EquipmentSet } from './battleItems';
|
||||||
|
|
||||||
export type PortraitKey = 'liuBei' | 'guanYu' | 'zhangFei';
|
export type PortraitKey = 'liuBei' | 'guanYu' | 'zhangFei';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export type UnitData = {
|
|||||||
attack: number;
|
attack: number;
|
||||||
move: number;
|
move: number;
|
||||||
stats: UnitStats;
|
stats: UnitStats;
|
||||||
|
equipment: EquipmentSet;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
@@ -168,6 +170,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 9,
|
attack: 9,
|
||||||
move: 4,
|
move: 4,
|
||||||
stats: { might: 76, intelligence: 78, leadership: 84, agility: 70, luck: 88 },
|
stats: { might: 76, intelligence: 78, leadership: 84, agility: 70, luck: 88 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'twin-oath-blades', level: 1, exp: 18 },
|
||||||
|
armor: { itemId: 'oath-robe', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'peach-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 9
|
y: 9
|
||||||
},
|
},
|
||||||
@@ -182,6 +189,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 10,
|
attack: 10,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 96, intelligence: 74, leadership: 91, agility: 72, luck: 68 },
|
stats: { might: 96, intelligence: 74, leadership: 91, agility: 72, luck: 68 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'green-dragon-glaive', level: 1, exp: 26 },
|
||||||
|
armor: { itemId: 'reinforced-lamellar', level: 1, exp: 10 },
|
||||||
|
accessory: { itemId: 'war-manual', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 2,
|
x: 2,
|
||||||
y: 10
|
y: 10
|
||||||
},
|
},
|
||||||
@@ -196,6 +208,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 10,
|
attack: 10,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 95, intelligence: 42, leadership: 83, agility: 67, luck: 61 },
|
stats: { might: 95, intelligence: 42, leadership: 83, agility: 67, luck: 61 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'serpent-spear', level: 1, exp: 20 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 1, exp: 8 },
|
||||||
|
accessory: { itemId: 'bravery-token', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 10
|
y: 10
|
||||||
},
|
},
|
||||||
@@ -210,6 +227,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 6,
|
attack: 6,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 49, intelligence: 34, leadership: 38, agility: 45, luck: 40 },
|
stats: { might: 49, intelligence: 34, leadership: 38, agility: 45, luck: 40 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 6 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 4 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 8,
|
x: 8,
|
||||||
y: 2
|
y: 2
|
||||||
},
|
},
|
||||||
@@ -224,6 +246,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 6,
|
attack: 6,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 47, intelligence: 32, leadership: 36, agility: 48, luck: 38 },
|
stats: { might: 47, intelligence: 32, leadership: 36, agility: 48, luck: 38 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'yellow-turban-saber', level: 1, exp: 4 },
|
||||||
|
armor: { itemId: 'rebel-vest', level: 1, exp: 5 },
|
||||||
|
accessory: { itemId: 'yellow-scarf-charm', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 9,
|
x: 9,
|
||||||
y: 3
|
y: 3
|
||||||
},
|
},
|
||||||
@@ -238,6 +265,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 7,
|
attack: 7,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 52, intelligence: 40, leadership: 43, agility: 56, luck: 42 },
|
stats: { might: 52, intelligence: 40, leadership: 43, agility: 56, luck: 42 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'short-bow', level: 1, exp: 10 },
|
||||||
|
armor: { itemId: 'cloth-armor', level: 1, exp: 6 },
|
||||||
|
accessory: { itemId: 'wind-quiver', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 2
|
y: 2
|
||||||
},
|
},
|
||||||
@@ -252,6 +284,11 @@ export const firstBattleUnits: UnitData[] = [
|
|||||||
attack: 9,
|
attack: 9,
|
||||||
move: 3,
|
move: 3,
|
||||||
stats: { might: 64, intelligence: 46, leadership: 61, agility: 52, luck: 50 },
|
stats: { might: 64, intelligence: 46, leadership: 61, agility: 52, luck: 50 },
|
||||||
|
equipment: {
|
||||||
|
weapon: { itemId: 'leader-axe', level: 1, exp: 18 },
|
||||||
|
armor: { itemId: 'lamellar-armor', level: 1, exp: 12 },
|
||||||
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
||||||
|
},
|
||||||
x: 10,
|
x: 10,
|
||||||
y: 4
|
y: 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import {
|
|||||||
type UnitStats
|
type UnitStats
|
||||||
} from '../data/scenario';
|
} from '../data/scenario';
|
||||||
import { getTerrainRule, getUnitClass, type TerrainType } from '../data/battleRules';
|
import { getTerrainRule, getUnitClass, type TerrainType } from '../data/battleRules';
|
||||||
|
import {
|
||||||
|
equipmentExpToNext,
|
||||||
|
equipmentSlotLabels,
|
||||||
|
equipmentSlots,
|
||||||
|
getItem,
|
||||||
|
type EquipmentSlot
|
||||||
|
} from '../data/battleItems';
|
||||||
import { palette } from '../ui/palette';
|
import { palette } from '../ui/palette';
|
||||||
|
|
||||||
const unitTexture: Record<string, string> = {
|
const unitTexture: Record<string, string> = {
|
||||||
@@ -70,6 +77,18 @@ type BondState = BattleBond & {
|
|||||||
battleExp: number;
|
battleExp: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EquipmentGrowthResult = {
|
||||||
|
slot: EquipmentSlot;
|
||||||
|
itemName: string;
|
||||||
|
amount: number;
|
||||||
|
level: number;
|
||||||
|
exp: number;
|
||||||
|
next: number;
|
||||||
|
leveled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxEquipmentLevel = 9;
|
||||||
|
|
||||||
const commandLabels: Record<BattleCommand, string> = {
|
const commandLabels: Record<BattleCommand, string> = {
|
||||||
attack: '공격',
|
attack: '공격',
|
||||||
strategy: '책략',
|
strategy: '책략',
|
||||||
@@ -724,7 +743,8 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
this.activeFaction = 'enemy';
|
this.activeFaction = 'enemy';
|
||||||
this.updateTurnText();
|
this.updateTurnText();
|
||||||
this.renderSituationPanel('아군 턴을 종료했습니다.\n황건적은 아직 간단 AI 구현 전이라 정비만 하고 넘어갑니다.');
|
const pressureMessage = this.resolveEnemyPressure();
|
||||||
|
this.renderSituationPanel(`아군 턴을 종료했습니다.\n${pressureMessage}`);
|
||||||
|
|
||||||
this.time.delayedCall(700, () => this.beginNextAllyTurn());
|
this.time.delayedCall(700, () => this.beginNextAllyTurn());
|
||||||
}
|
}
|
||||||
@@ -757,8 +777,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
|
|
||||||
private recordAttackIntent(unit: UnitData) {
|
private recordAttackIntent(unit: UnitData) {
|
||||||
const target = this.findNearestEnemy(unit);
|
const target = this.findNearestEnemy(unit);
|
||||||
|
const weaponGrowth = this.awardEquipmentExp(unit, 'weapon', this.weaponExpGain(unit));
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return `${unit.name} 공격 명령 완료\n공격 가능한 적이 없습니다.`;
|
return `${unit.name} 공격 명령 완료\n공격 가능한 적이 없습니다.\n${this.formatEquipmentGrowth(weaponGrowth)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const partners = this.attackIntents
|
const partners = this.attackIntents
|
||||||
@@ -786,7 +807,70 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
const bonus = this.strongestBondBonus(unit.id);
|
const bonus = this.strongestBondBonus(unit.id);
|
||||||
const chain = bonus.chainRate > 0 ? `\n공명 보너스: 피해 +${bonus.damageBonus}% / 연계 ${bonus.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}${chain}${exp}`;
|
return `${unit.name} 공격 명령 완료\n예상 목표: ${target.name}\n${this.formatEquipmentGrowth(weaponGrowth)}${chain}${exp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveEnemyPressure() {
|
||||||
|
const defender = this.findEnemyPressureTarget();
|
||||||
|
if (!defender) {
|
||||||
|
return '황건적은 움직일 틈을 찾지 못했습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const armorGrowth = this.awardEquipmentExp(defender, 'armor', 6);
|
||||||
|
return `황건적의 견제 공격을 ${defender.name}이 받아냈습니다.\n${this.formatEquipmentGrowth(armorGrowth)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findEnemyPressureTarget() {
|
||||||
|
const allies = firstBattleUnits.filter((unit) => unit.faction === 'ally' && unit.hp > 0);
|
||||||
|
if (allies.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...allies].sort((a, b) => this.nearestEnemyDistance(a) - this.nearestEnemyDistance(b))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private nearestEnemyDistance(unit: UnitData) {
|
||||||
|
const distances = firstBattleUnits
|
||||||
|
.filter((candidate) => candidate.faction === 'enemy' && candidate.hp > 0)
|
||||||
|
.map((enemy) => this.tileDistance(unit, enemy));
|
||||||
|
return distances.length > 0 ? Math.min(...distances) : Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private weaponExpGain(unit: UnitData) {
|
||||||
|
const weapon = getItem(unit.equipment.weapon.itemId);
|
||||||
|
const bonus = this.strongestBondBonus(unit.id);
|
||||||
|
return 8 + (weapon.rank === 'treasure' ? 2 : 0) + (bonus.chainRate > 0 ? 2 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private awardEquipmentExp(unit: UnitData, slot: EquipmentSlot, amount: number): EquipmentGrowthResult {
|
||||||
|
const state = unit.equipment[slot];
|
||||||
|
const item = getItem(state.itemId);
|
||||||
|
let exp = state.exp + amount;
|
||||||
|
let leveled = false;
|
||||||
|
|
||||||
|
while (state.level < maxEquipmentLevel && exp >= equipmentExpToNext(state.level)) {
|
||||||
|
exp -= equipmentExpToNext(state.level);
|
||||||
|
state.level += 1;
|
||||||
|
leveled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = equipmentExpToNext(state.level);
|
||||||
|
state.exp = state.level >= maxEquipmentLevel ? Math.min(exp, next) : exp;
|
||||||
|
|
||||||
|
return {
|
||||||
|
slot,
|
||||||
|
itemName: item.name,
|
||||||
|
amount,
|
||||||
|
level: state.level,
|
||||||
|
exp: state.exp,
|
||||||
|
next,
|
||||||
|
leveled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatEquipmentGrowth(result: EquipmentGrowthResult) {
|
||||||
|
const levelUp = result.leveled ? ` / Lv ${result.level} 달성` : '';
|
||||||
|
return `${equipmentSlotLabels[result.slot]} ${result.itemName} 경험치 +${result.amount} (${result.exp}/${result.next})${levelUp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private findNearestEnemy(unit: UnitData) {
|
private findNearestEnemy(unit: UnitData) {
|
||||||
@@ -1042,8 +1126,9 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
const messageTop = Math.min(panelY + panelHeight - 254, listTop + units.length * 50 + 14);
|
const messageHeight = 104;
|
||||||
this.renderPanelMessage(message, left, messageTop, width);
|
const messageTop = Math.min(panelY + panelHeight - messageHeight - 124, listTop + units.length * 50 + 14);
|
||||||
|
this.renderPanelMessage(message, left, messageTop, width, messageHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,26 +1238,106 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
hpValue.setOrigin(1, 0);
|
hpValue.setOrigin(1, 0);
|
||||||
this.drawGauge(left + 76, top + 84, width - 160, 10, unit.hp / unit.maxHp, 0x59d18c);
|
this.drawGauge(left + 76, top + 84, width - 160, 10, unit.hp / unit.maxHp, 0x59d18c);
|
||||||
|
|
||||||
this.renderSmallValueBox(left, top + 112, '공격', `${unit.attack}`);
|
const attackBonus = this.equipmentAttackBonus(unit);
|
||||||
|
this.renderSmallValueBox(left, top + 112, '공격', `${unit.attack + attackBonus}`);
|
||||||
this.renderSmallValueBox(left + width / 2 + 6, top + 112, '이동', `${unit.move}`);
|
this.renderSmallValueBox(left + width / 2 + 6, top + 112, '이동', `${unit.move}`);
|
||||||
|
|
||||||
const statTop = top + 164;
|
const statTop = top + 158;
|
||||||
statLabels.forEach((stat, index) => {
|
statLabels.forEach((stat, index) => {
|
||||||
this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 32, width);
|
this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 28, width);
|
||||||
});
|
});
|
||||||
|
|
||||||
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}%${acted ? ' / 행동완료' : ''}`;
|
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}%${acted ? ' / 행동완료' : ''}`;
|
||||||
this.trackSideObject(this.add.text(left, top + 328, positionText, {
|
this.trackSideObject(this.add.text(left, top + 306, positionText, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
color: acted ? '#bdbdbd' : '#9fb0bf'
|
color: acted ? '#bdbdbd' : '#9fb0bf'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.renderEquipmentSummary(unit, left, top + 330, width);
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
this.renderPanelMessage(message, left, top + 352, width);
|
this.renderPanelMessage(message, left, top + 440, width, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderEquipmentSummary(unit: UnitData, x: number, y: number, width: number) {
|
||||||
|
this.trackSideObject(this.add.text(x, y, '장비', {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '16px',
|
||||||
|
color: '#f2e3bf',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
|
||||||
|
equipmentSlots.forEach((slot, index) => {
|
||||||
|
this.renderEquipmentRow(unit, slot, x, y + 24 + index * 28, width);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEquipmentRow(unit: UnitData, slot: EquipmentSlot, x: number, y: number, width: number) {
|
||||||
|
const state = unit.equipment[slot];
|
||||||
|
const item = getItem(state.itemId);
|
||||||
|
const next = equipmentExpToNext(state.level);
|
||||||
|
const isTreasure = item.rank === 'treasure';
|
||||||
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 24, isTreasure ? 0x1f2430 : 0x101820, isTreasure ? 0.94 : 0.86));
|
||||||
|
bg.setOrigin(0);
|
||||||
|
bg.setStrokeStyle(1, isTreasure ? palette.gold : 0x53606c, isTreasure ? 0.58 : 0.42);
|
||||||
|
|
||||||
|
this.trackSideObject(this.add.text(x + 8, y + 5, equipmentSlotLabels[slot], {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#9fb0bf',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
this.trackSideObject(this.add.text(x + 54, y + 4, item.name, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: isTreasure ? '#f4dfad' : '#d4dce6',
|
||||||
|
fontStyle: isTreasure ? '700' : '400'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const bonusText = this.itemBonusText(item);
|
||||||
|
this.trackSideObject(this.add.text(x + width - 124, y + 6, bonusText, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#9fb0bf'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const expText = this.trackSideObject(this.add.text(x + width - 54, y + 6, `${state.exp}/${next}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color: '#9fb0bf'
|
||||||
|
}));
|
||||||
|
expText.setOrigin(1, 0);
|
||||||
|
|
||||||
|
const levelText = this.trackSideObject(this.add.text(x + width - 8, y + 3, `Lv ${state.level}`, {
|
||||||
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
|
fontSize: '14px',
|
||||||
|
color: isTreasure ? '#f2e3bf' : '#d4dce6',
|
||||||
|
fontStyle: '700'
|
||||||
|
}));
|
||||||
|
levelText.setOrigin(1, 0);
|
||||||
|
|
||||||
|
this.drawGauge(x + 54, y + 19, width - 154, 4, state.exp / next, isTreasure ? 0xd8b15f : 0x58aee0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private itemBonusText(item: ReturnType<typeof getItem>) {
|
||||||
|
const parts = [
|
||||||
|
item.attackBonus ? `공+${item.attackBonus}` : '',
|
||||||
|
item.defenseBonus ? `방+${item.defenseBonus}` : '',
|
||||||
|
item.strategyBonus ? `책+${item.strategyBonus}` : ''
|
||||||
|
].filter(Boolean);
|
||||||
|
return parts.length > 0 ? parts.join(' ') : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private equipmentAttackBonus(unit: UnitData) {
|
||||||
|
return equipmentSlots.reduce((total, slot) => {
|
||||||
|
const item = getItem(unit.equipment[slot].itemId);
|
||||||
|
const levelBonus = slot === 'weapon' ? unit.equipment[slot].level - 1 : 0;
|
||||||
|
return total + (item.attackBonus ?? 0) + levelBonus;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private renderSmallValueBox(x: number, y: number, label: string, value: string) {
|
private renderSmallValueBox(x: number, y: number, label: string, value: string) {
|
||||||
const boxWidth = (this.layout.panelWidth - 60) / 2;
|
const boxWidth = (this.layout.panelWidth - 60) / 2;
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, boxWidth, 38, 0x16212d, 0.92));
|
const bg = this.trackSideObject(this.add.rectangle(x, y, boxWidth, 38, 0x16212d, 0.92));
|
||||||
@@ -1221,16 +1386,16 @@ export class BattleScene extends Phaser.Scene {
|
|||||||
fill.setOrigin(0);
|
fill.setOrigin(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderPanelMessage(text: string, x: number, y: number, width: number) {
|
private renderPanelMessage(text: string, x: number, y: number, width: number, height = 74) {
|
||||||
const bg = this.trackSideObject(this.add.rectangle(x, y, width, 74, 0x101820, 0.72));
|
const bg = this.trackSideObject(this.add.rectangle(x, y, width, height, 0x101820, 0.72));
|
||||||
bg.setOrigin(0);
|
bg.setOrigin(0);
|
||||||
bg.setStrokeStyle(1, 0x53606c, 0.42);
|
bg.setStrokeStyle(1, 0x53606c, 0.42);
|
||||||
this.trackSideObject(this.add.text(x + 12, y + 10, text, {
|
this.trackSideObject(this.add.text(x + 12, y + 10, text, {
|
||||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||||
fontSize: '15px',
|
fontSize: height < 60 ? '13px' : '15px',
|
||||||
color: '#d4dce6',
|
color: '#d4dce6',
|
||||||
wordWrap: { width: width - 24, useAdvancedWrap: true },
|
wordWrap: { width: width - 24, useAdvancedWrap: true },
|
||||||
lineSpacing: 4
|
lineSpacing: height < 60 ? 2 : 4
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user