Add equipment growth system

This commit is contained in:
2026-06-22 02:27:03 +09:00
parent f99884ea29
commit b0c6c0ebea
4 changed files with 424 additions and 18 deletions

View File

@@ -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

View 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;
}

View File

@@ -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
} }

View File

@@ -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
})); }));
} }