feat: upgrade combat and item icons

This commit is contained in:
2026-07-17 21:25:57 +09:00
parent e92535165d
commit eb41c05f60
19 changed files with 437 additions and 42 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 KiB

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

View File

@@ -1,9 +1,13 @@
import Phaser from 'phaser';
import battleUiIconsUrl from '../../assets/images/ui/battle-ui-icons.png';
import battleUiIconsMicroUrl from '../../assets/images/ui/battle-ui-icons-micro.png';
export const battleUiIconTextureKey = 'battle-ui-icons';
export const battleUiIconMicroTextureKey = 'battle-ui-icons-micro';
export const battleUiIconFrameSize = 128;
export const battleUiIconMicroFrameSize = 32;
export const battleUiIconMicroMaxDisplaySize = 30;
export const battleUiIconFrames = {
hp: 0,
@@ -34,31 +38,65 @@ export const battleUiIconFrames = {
fire: 24,
shout: 25,
confusion: 25,
failure: 26
failure: 26,
bean: 27,
salve: 28,
wine: 29
} as const;
export type BattleUiIconKey = keyof typeof battleUiIconFrames;
export const battleUiIconV2Sources = {
attack: { frame: battleUiIconFrames.attack, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/attack-v2.png' },
hit: { frame: battleUiIconFrames.hit, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/hit-v2.png' },
critical: { frame: battleUiIconFrames.critical, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/critical-v2.png' },
counter: { frame: battleUiIconFrames.counter, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/counter-v2.png' },
fire: { frame: battleUiIconFrames.fire, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/fire-v2.png' },
bean: { frame: battleUiIconFrames.bean, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/bean-v2.png' },
salve: { frame: battleUiIconFrames.salve, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/salve-v2.png' },
wine: { frame: battleUiIconFrames.wine, source: 'src/assets/images/ui/icon-sources/battle-ui-v2/wine-v2.png' }
} as const satisfies Partial<Record<BattleUiIconKey, { frame: number; source: string }>>;
export const battleUiIconManifest = {
source: 'src/assets/images/ui/battle-ui-icons.png',
textureKey: battleUiIconTextureKey,
frameWidth: battleUiIconFrameSize,
frameHeight: battleUiIconFrameSize,
microSource: 'src/assets/images/ui/battle-ui-icons-micro.png',
microTextureKey: battleUiIconMicroTextureKey,
microFrameWidth: battleUiIconMicroFrameSize,
microFrameHeight: battleUiIconMicroFrameSize,
microMaxDisplaySize: battleUiIconMicroMaxDisplaySize,
columns: 5,
rows: 6,
frames: battleUiIconFrames
frames: battleUiIconFrames,
v2Sources: battleUiIconV2Sources
} as const;
export function battleUiIconTextureForSize(size: number) {
return size <= battleUiIconMicroMaxDisplaySize ? battleUiIconMicroTextureKey : battleUiIconTextureKey;
}
export function loadBattleUiIcons(scene: Phaser.Scene, onReady: () => void) {
if (scene.textures.exists(battleUiIconTextureKey)) {
const needsFullAtlas = !scene.textures.exists(battleUiIconTextureKey);
const needsMicroAtlas = !scene.textures.exists(battleUiIconMicroTextureKey);
if (!needsFullAtlas && !needsMicroAtlas) {
onReady();
return;
}
scene.load.once('complete', onReady);
scene.load.spritesheet(battleUiIconTextureKey, battleUiIconsUrl, {
frameWidth: battleUiIconFrameSize,
frameHeight: battleUiIconFrameSize
});
if (needsFullAtlas) {
scene.load.spritesheet(battleUiIconTextureKey, battleUiIconsUrl, {
frameWidth: battleUiIconFrameSize,
frameHeight: battleUiIconFrameSize
});
}
if (needsMicroAtlas) {
scene.load.spritesheet(battleUiIconMicroTextureKey, battleUiIconsMicroUrl, {
frameWidth: battleUiIconMicroFrameSize,
frameHeight: battleUiIconMicroFrameSize
});
}
scene.load.start();
}

View File

@@ -1,3 +1,5 @@
import type { BattleUiIconKey } from './battleUiIcons';
export type UsableCommand = 'strategy' | 'item';
export type UsableEffect = 'damage' | 'heal' | 'focus';
@@ -10,6 +12,7 @@ export type StrategyCoverageId = 'healing' | 'buff' | 'offense' | 'control';
export type BattleUsable = {
id: string;
iconKey: BattleUiIconKey;
command: UsableCommand;
name: string;
target: UsableTarget;
@@ -32,6 +35,7 @@ export type BattleUsable = {
export const usableCatalog: Record<string, BattleUsable> = {
aid: {
id: 'aid',
iconKey: 'heal',
command: 'strategy',
name: '응급',
target: 'ally',
@@ -43,6 +47,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
encourage: {
id: 'encourage',
iconKey: 'focus',
command: 'strategy',
name: '격려',
target: 'ally',
@@ -58,6 +63,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
fireTactic: {
id: 'fireTactic',
iconKey: 'fire',
command: 'strategy',
name: '화계',
target: 'enemy',
@@ -74,6 +80,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
roar: {
id: 'roar',
iconKey: 'confusion',
command: 'strategy',
name: '고함',
target: 'enemy',
@@ -90,6 +97,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
benevolentCommand: {
id: 'benevolentCommand',
iconKey: 'focus',
command: 'strategy',
name: '인덕의 호령',
target: 'ally',
@@ -106,6 +114,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
azureDragonStrike: {
id: 'azureDragonStrike',
iconKey: 'strategy',
command: 'strategy',
name: '청룡일섬',
target: 'enemy',
@@ -120,6 +129,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
changbanRoar: {
id: 'changbanRoar',
iconKey: 'confusion',
command: 'strategy',
name: '장판교 대갈',
target: 'enemy',
@@ -137,6 +147,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
singleRiderRescue: {
id: 'singleRiderRescue',
iconKey: 'heal',
command: 'strategy',
name: '단기구원',
target: 'ally',
@@ -149,6 +160,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
eastWindFire: {
id: 'eastWindFire',
iconKey: 'fire',
command: 'strategy',
name: '동남풍 화계',
target: 'enemy',
@@ -166,6 +178,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
hundredPacePierce: {
id: 'hundredPacePierce',
iconKey: 'strategy',
command: 'strategy',
name: '백보천양',
target: 'enemy',
@@ -180,6 +193,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
westernCavalryCharge: {
id: 'westernCavalryCharge',
iconKey: 'confusion',
command: 'strategy',
name: '서량철기',
target: 'enemy',
@@ -197,6 +211,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
qilinStratagem: {
id: 'qilinStratagem',
iconKey: 'confusion',
command: 'strategy',
name: '기린지계',
target: 'enemy',
@@ -214,6 +229,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
bean: {
id: 'bean',
iconKey: 'bean',
command: 'item',
name: '콩',
target: 'ally',
@@ -224,6 +240,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
salve: {
id: 'salve',
iconKey: 'salve',
command: 'item',
name: '상처약',
target: 'ally',
@@ -234,6 +251,7 @@ export const usableCatalog: Record<string, BattleUsable> = {
},
wine: {
id: 'wine',
iconKey: 'wine',
command: 'item',
name: '술',
target: 'self',

View File

@@ -23,7 +23,7 @@ import {
} from '../data/unitAssets';
import {
battleUiIconFrames,
battleUiIconTextureKey,
battleUiIconTextureForSize,
loadBattleUiIcons,
type BattleUiIconKey
} from '../data/battleUiIcons';
@@ -8876,7 +8876,7 @@ export class BattleScene extends Phaser.Scene {
if (usable.command === 'item') {
if (usable.effect === 'heal') {
return {
icon: 'heal' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'heal' as BattleUiIconKey,
accent: palette.gold,
effectLabel: '회복',
@@ -8884,7 +8884,7 @@ export class BattleScene extends Phaser.Scene {
};
}
return {
icon: 'accessory' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'focus' as BattleUiIconKey,
accent: palette.gold,
effectLabel: '강화',
@@ -8893,7 +8893,7 @@ export class BattleScene extends Phaser.Scene {
}
if (usable.effect === 'heal') {
return {
icon: 'heal' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'heal' as BattleUiIconKey,
accent: palette.green,
effectLabel: '회복',
@@ -8902,7 +8902,7 @@ export class BattleScene extends Phaser.Scene {
}
if (usable.effect === 'focus') {
return {
icon: 'focus' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'leadership' as BattleUiIconKey,
accent: palette.blue,
effectLabel: '격려',
@@ -8911,7 +8911,7 @@ export class BattleScene extends Phaser.Scene {
}
if (usable.statusEffect === 'burn' || id.includes('fire')) {
return {
icon: 'fire' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'fire' as BattleUiIconKey,
accent: 0xd8732c,
effectLabel: '화염',
@@ -8920,7 +8920,7 @@ export class BattleScene extends Phaser.Scene {
}
if (usable.statusEffect === 'confusion' || id.includes('roar') || id.includes('shout')) {
return {
icon: 'confusion' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'confusion' as BattleUiIconKey,
accent: 0xc96b4a,
effectLabel: '위압',
@@ -8928,7 +8928,7 @@ export class BattleScene extends Phaser.Scene {
};
}
return {
icon: 'strategy' as BattleUiIconKey,
icon: usable.iconKey,
powerIcon: 'strategy' as BattleUiIconKey,
accent: 0xb64a45,
effectLabel: '피해',
@@ -23952,7 +23952,7 @@ export class BattleScene extends Phaser.Scene {
}
private createBattleUiIcon(x: number, y: number, icon: BattleUiIconKey, size = 22) {
const image = this.add.image(x, y, battleUiIconTextureKey, battleUiIconFrames[icon]);
const image = this.add.image(x, y, battleUiIconTextureForSize(size), battleUiIconFrames[icon]);
image.setDisplaySize(size, size);
return image;
}
@@ -24013,7 +24013,7 @@ export class BattleScene extends Phaser.Scene {
private commandIcon(command: BattleCommand, unit: UnitData): BattleUiIconKey {
switch (command) {
case 'attack':
return this.itemIcon(getItem(unit.equipment.weapon.itemId), 'weapon');
return 'attack';
case 'strategy':
return 'strategy';
case 'item':
@@ -24031,12 +24031,12 @@ export class BattleScene extends Phaser.Scene {
private actionIcon(preview: CombatPreview): BattleUiIconKey {
if (preview.action === 'attack') {
return this.itemIcon(getItem(preview.attacker.equipment.weapon.itemId), 'weapon');
return 'attack';
}
if (preview.action === 'strategy') {
return preview.usable ? this.usableIcon(preview.usable) : 'strategy';
}
return preview.usable ? this.itemIcon(getItem(preview.attacker.equipment.accessory.itemId), 'accessory') : 'accessory';
return preview.usable ? this.usableIcon(preview.usable) : 'accessory';
}
private itemBonusText(item: ReturnType<typeof getItem>) {

View File

@@ -1,7 +1,7 @@
import Phaser from 'phaser';
import storyFirstSortieUrl from '../../assets/images/story/05-first-sortie.png';
import { soundDirector } from '../audio/SoundDirector';
import { battleUiIconFrames, battleUiIconTextureKey, loadBattleUiIcons, type BattleUiIconKey } from '../data/battleUiIcons';
import { battleUiIconFrames, battleUiIconTextureForSize, loadBattleUiIcons, type BattleUiIconKey } from '../data/battleUiIcons';
import { selectCampSkin, type CampSkinSelection } from '../data/campSkins';
import {
equipmentExpToNext,
@@ -280,9 +280,11 @@ type SortiePortraitRosterLayout = {
nextEnabled: boolean;
};
type CampSupplyId = 'bean' | 'wine' | 'salve';
type CampSupplyDefinition = {
label: string;
usableId: string;
usableId: CampSupplyId;
title: string;
description: string;
healHp: number;
@@ -290,6 +292,7 @@ type CampSupplyDefinition = {
};
type MerchantItemDefinition = {
usableId: CampSupplyId;
label: string;
title: string;
description: string;
@@ -780,18 +783,21 @@ const campSupplyByLabel = new Map(campSupplies.map((supply) => [supply.label, su
const merchantItems: MerchantItemDefinition[] = [
{
usableId: 'bean',
label: '콩',
title: '콩',
description: '값싸고 가벼운 회복 도구입니다.',
price: 40
},
{
usableId: 'wine',
label: '탁주',
title: '탁주',
description: '병력 회복과 작은 공명 상승에 유용합니다.',
price: 70
},
{
usableId: 'salve',
label: '상처약',
title: '상처약',
description: '큰 부상을 빠르게 회복합니다.',
@@ -15880,18 +15886,33 @@ export class CampScene extends Phaser.Scene {
}
private renderSortieBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, depth: number, alpha = 1) {
if (!this.textures.exists(battleUiIconTextureKey)) {
const displaySize = this.campUiLength(size);
const textureKey = battleUiIconTextureForSize(displaySize);
if (!this.textures.exists(textureKey)) {
const fallback = this.trackSortie(this.add.circle(x, y, size / 2, palette.gold, 0.18));
fallback.setDepth(depth);
return;
}
const icon = this.trackSortie(this.add.image(x, y, battleUiIconTextureKey, battleUiIconFrames[iconKey]));
icon.setDisplaySize(this.campUiLength(size), this.campUiLength(size));
const icon = this.trackSortie(this.add.image(x, y, textureKey, battleUiIconFrames[iconKey]));
icon.setDisplaySize(displaySize, displaySize);
icon.setAlpha(alpha);
icon.setDepth(depth);
}
private renderCampBattleUiIcon(iconKey: BattleUiIconKey, x: number, y: number, size: number, alpha = 1) {
const displaySize = this.campUiLength(size);
const textureKey = battleUiIconTextureForSize(displaySize);
if (!this.textures.exists(textureKey)) {
return this.track(this.add.circle(x, y, size / 2, palette.gold, 0.18));
}
const icon = this.track(this.add.image(x, y, textureKey, battleUiIconFrames[iconKey]));
icon.setDisplaySize(displaySize, displaySize);
icon.setAlpha(alpha);
return icon;
}
private unitClassIconKey(unit: UnitData): BattleUiIconKey {
switch (unit.classKey) {
case 'lord':
@@ -16024,9 +16045,10 @@ export class CampScene extends Phaser.Scene {
bg.setOrigin(0);
bg.setDepth(depth);
bg.setStrokeStyle(1, assigned > 0 ? palette.green : enabled ? palette.gold : 0x53606c, assigned > 0 ? 0.78 : enabled ? 0.5 : 0.28);
this.renderSortieBattleUiIcon(supply.usableId, buttonX + 10, y + 10, 14, depth + 1, enabled ? 0.96 : 0.48);
this.trackSortie(
this.add.text(
buttonX + (buttonWidth - 5) / 2,
buttonX + (buttonWidth - 5) / 2 + 6,
y + 5,
`${supply.title} ${assigned}`,
this.textStyle(9, assigned > 0 ? '#a8ffd0' : enabled ? '#c8d2dd' : '#77818c', true)
@@ -21583,8 +21605,9 @@ export class CampScene extends Phaser.Scene {
const bg = this.track(this.add.rectangle(x, y, width, 40, canBuy ? 0x151f2a : 0x111820, canBuy ? 0.94 : 0.76));
bg.setOrigin(0);
bg.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.58 : 0.36);
this.track(this.add.text(x + 10, y + 6, item.title, this.textStyle(13, canBuy ? '#f2e3bf' : '#7f8994', true)));
this.track(this.add.text(x + 10, y + 23, `${item.price}`, this.textStyle(11, canBuy ? '#d8b15f' : '#7f8994', true)));
this.renderCampBattleUiIcon(item.usableId, x + 20, y + 20, 26, canBuy ? 1 : 0.5);
this.track(this.add.text(x + 40, y + 6, item.title, this.textStyle(13, canBuy ? '#f2e3bf' : '#7f8994', true)));
this.track(this.add.text(x + 40, y + 23, `${item.price}`, this.textStyle(11, canBuy ? '#d8b15f' : '#7f8994', true)));
const button = this.track(this.add.rectangle(x + width - 42, y + 20, 60, 26, canBuy ? 0x1a2630 : 0x121922, canBuy ? 0.96 : 0.72));
button.setStrokeStyle(1, canBuy ? palette.gold : 0x53606c, canBuy ? 0.72 : 0.4);
@@ -21669,11 +21692,12 @@ export class CampScene extends Phaser.Scene {
bg.setOrigin(0);
bg.setStrokeStyle(1, enabled ? palette.gold : 0x53606c, enabled ? 0.62 : 0.38);
this.track(this.add.text(x + 14, y + 10, `${supply.title} x${amount}`, this.textStyle(16, amount > 0 ? '#f2e3bf' : '#7f8994', true)));
this.renderCampBattleUiIcon(supply.usableId, x + 28, y + 31, 36, amount > 0 ? 1 : 0.46);
this.track(this.add.text(x + 56, y + 10, `${supply.title} x${amount}`, this.textStyle(16, amount > 0 ? '#f2e3bf' : '#7f8994', true)));
this.track(
this.add.text(x + 14, y + 34, supply.description, {
this.add.text(x + 56, y + 34, supply.description, {
...this.textStyle(12, enabled ? '#c8d2dd' : '#77818c'),
wordWrap: { width: width - 128, useAdvancedWrap: true }
wordWrap: { width: width - 170, useAdvancedWrap: true }
})
);
@@ -22075,8 +22099,8 @@ export class CampScene extends Phaser.Scene {
private normalizedSortieItemAssignments(assignments?: CampaignSortieItemAssignments) {
const selected = new Set(this.selectedSortieUnitIds);
const validSupplyIds = new Set(campSupplies.map((supply) => supply.usableId));
const remainingBySupply = new Map(campSupplies.map((supply) => [supply.usableId, this.inventoryAmount(supply.label)]));
const validSupplyIds = new Set<string>(campSupplies.map((supply) => supply.usableId));
const remainingBySupply = new Map<string, number>(campSupplies.map((supply) => [supply.usableId, this.inventoryAmount(supply.label)]));
return Object.entries(assignments ?? {}).reduce<CampaignSortieItemAssignments>((next, [unitId, stocks]) => {
if (!selected.has(unitId)) {