From ddc862d217892de6c1d9122ff72d460c11284e6f Mon Sep 17 00:00:00 2001 From: Wickedness Date: Wed, 24 Jun 2026 20:24:04 +0900 Subject: [PATCH] Add signature unit visuals --- scripts/verify-flow.mjs | 14 ++-- src/game/scenes/BattleScene.ts | 23 +++++- src/game/scenes/BootScene.ts | 142 ++++++++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 13 deletions(-) diff --git a/scripts/verify-flow.mjs b/scripts/verify-flow.mjs index 27024bd..eff2abc 100644 --- a/scripts/verify-flow.mjs +++ b/scripts/verify-flow.mjs @@ -8948,12 +8948,12 @@ try { !sixtyThirdEnemyBehaviors.has('aggressive') || !sixtyThirdEnemyBehaviors.has('guard') || !sixtyThirdEnemyBehaviors.has('hold') || - !sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-leader-sima-yi') || + !sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-leader-sima-yi' && unit.textureBase === 'unit-sima-yi') || !sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-officer-zhang-he') || !sixtyThirdEnemies.some((unit) => unit.id === 'northern-ninth-officer-guo-huai') || - !sixtyThirdAllies.some((unit) => unit.id === 'zhuge-liang') || - !sixtyThirdAllies.some((unit) => unit.id === 'jiang-wei') || - !sixtyThirdAllies.some((unit) => unit.id === 'zhao-yun') || + !sixtyThirdAllies.some((unit) => unit.id === 'zhuge-liang' && unit.textureBase === 'unit-zhuge-liang') || + !sixtyThirdAllies.some((unit) => unit.id === 'jiang-wei' && unit.textureBase === 'unit-jiang-wei') || + !sixtyThirdAllies.some((unit) => unit.id === 'zhao-yun' && unit.textureBase === 'unit-zhao-yun') || !sixtyThirdAllies.some((unit) => unit.id === 'wei-yan') || !sixtyThirdAllies.some((unit) => unit.id === 'wang-ping') || !sixtyThirdAllies.some((unit) => unit.id === 'huang-quan') || @@ -9129,11 +9129,11 @@ try { !sixtyFourthEnemyBehaviors.has('aggressive') || !sixtyFourthEnemyBehaviors.has('guard') || !sixtyFourthEnemyBehaviors.has('hold') || - !sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-leader-sima-yi' && unit.combatPortraitKey === 'portrait-sima-yi') || + !sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-leader-sima-yi' && unit.combatPortraitKey === 'portrait-sima-yi' && unit.textureBase === 'unit-sima-yi') || !sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-officer-zhang-he') || !sixtyFourthEnemies.some((unit) => unit.id === 'northern-tenth-officer-guo-huai') || - !sixtyFourthAllies.some((unit) => unit.id === 'zhuge-liang' && unit.combatPortraitKey === 'portrait-zhuge-liang') || - !sixtyFourthAllies.some((unit) => unit.id === 'jiang-wei' && unit.combatPortraitKey === 'portrait-jiang-wei') || + !sixtyFourthAllies.some((unit) => unit.id === 'zhuge-liang' && unit.combatPortraitKey === 'portrait-zhuge-liang' && unit.textureBase === 'unit-zhuge-liang') || + !sixtyFourthAllies.some((unit) => unit.id === 'jiang-wei' && unit.combatPortraitKey === 'portrait-jiang-wei' && unit.textureBase === 'unit-jiang-wei') || !sixtyFourthAllies.some((unit) => unit.id === 'wang-ping') || !sixtyFourthAllies.some((unit) => unit.id === 'huang-quan') || !sixtyFourthAllies.some((unit) => unit.id === 'li-yan') || diff --git a/src/game/scenes/BattleScene.ts b/src/game/scenes/BattleScene.ts index dd09043..8984388 100644 --- a/src/game/scenes/BattleScene.ts +++ b/src/game/scenes/BattleScene.ts @@ -28,8 +28,11 @@ const unitTexture: Record = { 'liu-bei': 'unit-liu-bei', 'guan-yu': 'unit-guan-yu', 'zhang-fei': 'unit-zhang-fei', - 'zhao-yun': 'unit-rebel-cavalry', - 'zhuge-liang': 'unit-liu-bei', + 'zhao-yun': 'unit-zhao-yun', + 'zhuge-liang': 'unit-zhuge-liang', + 'jiang-wei': 'unit-jiang-wei', + 'northern-second-officer-jiang-wei': 'unit-jiang-wei', + 'northern-third-officer-jiang-wei': 'unit-jiang-wei', 'ma-liang': 'unit-liu-bei', 'yi-ji': 'unit-liu-bei', 'gong-zhi': 'unit-guan-yu', @@ -930,6 +933,7 @@ const unitTextureByClass: Partial> = { }; const coreAllyTextureIds = new Set(['liu-bei', 'guan-yu', 'zhang-fei']); +const signatureUnitTextureIds = new Set(['zhao-yun', 'zhuge-liang', 'jiang-wei', 'northern-second-officer-jiang-wei', 'northern-third-officer-jiang-wei']); const earlyRebelTexturePrefixes = ['rebel-', 'pursuit-']; @@ -8185,6 +8189,11 @@ export class BattleScene extends Phaser.Scene { } private unitTextureKey(unit: UnitData) { + const signatureTexture = this.signatureUnitTextureKey(unit); + if (signatureTexture) { + return signatureTexture; + } + const factionTexture = this.unitFactionTextureKey(unit); if (factionTexture) { return factionTexture; @@ -8220,6 +8229,16 @@ export class BattleScene extends Phaser.Scene { return unitTexture[unit.id] ?? unitTextureByClass[unit.classKey] ?? 'unit-rebel'; } + private signatureUnitTextureKey(unit: UnitData) { + if (unit.id.includes('sima-yi') || unit.name === '사마의') { + return 'unit-sima-yi'; + } + if (signatureUnitTextureIds.has(unit.id)) { + return unitTexture[unit.id]; + } + return undefined; + } + private unitFactionTextureKey(unit: UnitData) { if (unit.faction === 'ally') { if (coreAllyTextureIds.has(unit.id)) { diff --git a/src/game/scenes/BootScene.ts b/src/game/scenes/BootScene.ts index b2e3fe0..a2c8743 100644 --- a/src/game/scenes/BootScene.ts +++ b/src/game/scenes/BootScene.ts @@ -24,6 +24,17 @@ import zhangFeiUnitSheetUrl from '../../assets/images/units/unit-zhang-fei.png'; import { storyBackgroundAssets } from '../data/storyAssets'; type UnitDirection = 'south' | 'east' | 'north' | 'west'; +type UnitSheetDecoration = 'zhaoYun' | 'zhugeLiang' | 'jiangWei' | 'simaYi'; + +type GeneratedUnitSheet = { + key: string; + sourceKey: string; + actionKey: string; + sourceActionKey: string; + tint: number; + alpha: number; + decoration?: UnitSheetDecoration; +}; const unitWalkFrameCount = 4; const unitSheetFrameSize = 313; @@ -46,18 +57,22 @@ const sourceUnitSheets = [ { key: 'unit-rebel-leader', url: rebelLeaderUnitSheetUrl, actionKey: 'unit-rebel-leader-actions', actionUrl: rebelLeaderActionSheetUrl } ]; -const generatedUnitSheets = [ +const generatedUnitSheets: GeneratedUnitSheet[] = [ { key: 'unit-shu-infantry', sourceKey: 'unit-guan-yu', actionKey: 'unit-shu-infantry-actions', sourceActionKey: 'unit-guan-yu-actions', tint: 0x2f7fba, alpha: 0.32 }, { key: 'unit-shu-spearman', sourceKey: 'unit-zhang-fei', actionKey: 'unit-shu-spearman-actions', sourceActionKey: 'unit-zhang-fei-actions', tint: 0x4aa878, alpha: 0.3 }, { key: 'unit-shu-archer', sourceKey: 'unit-rebel-archer', actionKey: 'unit-shu-archer-actions', sourceActionKey: 'unit-rebel-archer-actions', tint: 0x4b9bd5, alpha: 0.36 }, { key: 'unit-shu-cavalry', sourceKey: 'unit-rebel-cavalry', actionKey: 'unit-shu-cavalry-actions', sourceActionKey: 'unit-rebel-cavalry-actions', tint: 0x488fc4, alpha: 0.36 }, { key: 'unit-shu-strategist', sourceKey: 'unit-liu-bei', actionKey: 'unit-shu-strategist-actions', sourceActionKey: 'unit-liu-bei-actions', tint: 0x75b7d8, alpha: 0.28 }, { key: 'unit-shu-officer', sourceKey: 'unit-guan-yu', actionKey: 'unit-shu-officer-actions', sourceActionKey: 'unit-guan-yu-actions', tint: 0x3f91c8, alpha: 0.36 }, + { key: 'unit-zhao-yun', sourceKey: 'unit-rebel-cavalry', actionKey: 'unit-zhao-yun-actions', sourceActionKey: 'unit-rebel-cavalry-actions', tint: 0xd7edf7, alpha: 0.42, decoration: 'zhaoYun' }, + { key: 'unit-zhuge-liang', sourceKey: 'unit-liu-bei', actionKey: 'unit-zhuge-liang-actions', sourceActionKey: 'unit-liu-bei-actions', tint: 0xd8efe3, alpha: 0.34, decoration: 'zhugeLiang' }, + { key: 'unit-jiang-wei', sourceKey: 'unit-rebel-cavalry', actionKey: 'unit-jiang-wei-actions', sourceActionKey: 'unit-rebel-cavalry-actions', tint: 0x64bfa8, alpha: 0.38, decoration: 'jiangWei' }, { key: 'unit-wei-infantry', sourceKey: 'unit-rebel', actionKey: 'unit-wei-infantry-actions', sourceActionKey: 'unit-rebel-actions', tint: 0x8b3030, alpha: 0.42 }, { key: 'unit-wei-archer', sourceKey: 'unit-rebel-archer', actionKey: 'unit-wei-archer-actions', sourceActionKey: 'unit-rebel-archer-actions', tint: 0x9d3c3c, alpha: 0.4 }, { key: 'unit-wei-cavalry', sourceKey: 'unit-rebel-cavalry', actionKey: 'unit-wei-cavalry-actions', sourceActionKey: 'unit-rebel-cavalry-actions', tint: 0x7a3540, alpha: 0.44 }, { key: 'unit-wei-strategist', sourceKey: 'unit-rebel-archer', actionKey: 'unit-wei-strategist-actions', sourceActionKey: 'unit-rebel-archer-actions', tint: 0x7f5a8e, alpha: 0.38 }, { key: 'unit-wei-officer', sourceKey: 'unit-rebel-leader', actionKey: 'unit-wei-officer-actions', sourceActionKey: 'unit-rebel-leader-actions', tint: 0x8f3940, alpha: 0.44 }, + { key: 'unit-sima-yi', sourceKey: 'unit-rebel-leader', actionKey: 'unit-sima-yi-actions', sourceActionKey: 'unit-rebel-leader-actions', tint: 0x1f2638, alpha: 0.58, decoration: 'simaYi' }, { key: 'unit-wu-infantry', sourceKey: 'unit-rebel', actionKey: 'unit-wu-infantry-actions', sourceActionKey: 'unit-rebel-actions', tint: 0x0b8b86, alpha: 0.42 }, { key: 'unit-wu-archer', sourceKey: 'unit-rebel-archer', actionKey: 'unit-wu-archer-actions', sourceActionKey: 'unit-rebel-archer-actions', tint: 0x169b91, alpha: 0.4 }, { key: 'unit-wu-cavalry', sourceKey: 'unit-rebel-cavalry', actionKey: 'unit-wu-cavalry-actions', sourceActionKey: 'unit-rebel-cavalry-actions', tint: 0x087b86, alpha: 0.44 }, @@ -368,12 +383,12 @@ export class BootScene extends Phaser.Scene { private createGeneratedUnitSheets() { generatedUnitSheets.forEach((sheet) => { - this.createTintedUnitSheet(sheet.key, sheet.sourceKey, sheet.tint, sheet.alpha); - this.createTintedUnitSheet(sheet.actionKey, sheet.sourceActionKey, sheet.tint, sheet.alpha); + this.createTintedUnitSheet(sheet.key, sheet.sourceKey, sheet.tint, sheet.alpha, sheet.decoration); + this.createTintedUnitSheet(sheet.actionKey, sheet.sourceActionKey, sheet.tint, sheet.alpha, sheet.decoration); }); } - private createTintedUnitSheet(targetKey: string, sourceKey: string, tint: number, alpha: number) { + private createTintedUnitSheet(targetKey: string, sourceKey: string, tint: number, alpha: number, decoration?: UnitSheetDecoration) { if (this.textures.exists(targetKey) || !this.textures.exists(sourceKey)) { return; } @@ -394,12 +409,131 @@ export class BootScene extends Phaser.Scene { context.fillRect(0, 0, canvas.width, canvas.height); context.globalCompositeOperation = 'source-over'; context.globalAlpha = 1; + if (decoration) { + this.decorateUnitSheet(context, decoration, canvas.width, canvas.height); + } this.textures.addSpriteSheet(targetKey, canvas as unknown as HTMLImageElement, { frameWidth: unitSheetFrameSize, frameHeight: unitSheetFrameSize }); } + private decorateUnitSheet(context: CanvasRenderingContext2D, decoration: UnitSheetDecoration, width: number, height: number) { + const columns = Math.floor(width / unitSheetFrameSize); + const rows = Math.floor(height / unitSheetFrameSize); + + for (let row = 0; row < rows; row += 1) { + for (let column = 0; column < columns; column += 1) { + this.drawUnitDecoration(context, decoration, column * unitSheetFrameSize, row * unitSheetFrameSize); + } + } + } + + private drawUnitDecoration(context: CanvasRenderingContext2D, decoration: UnitSheetDecoration, x: number, y: number) { + context.save(); + context.lineCap = 'round'; + context.lineJoin = 'round'; + + if (decoration === 'zhaoYun') { + context.globalAlpha = 0.82; + context.fillStyle = '#f4fbff'; + context.beginPath(); + context.moveTo(x + 128, y + 106); + context.lineTo(x + 86, y + 262); + context.lineTo(x + 142, y + 246); + context.lineTo(x + 174, y + 112); + context.closePath(); + context.fill(); + context.strokeStyle = '#d7f2ff'; + context.lineWidth = 14; + context.beginPath(); + context.moveTo(x + 92, y + 62); + context.lineTo(x + 236, y + 266); + context.stroke(); + context.strokeStyle = '#4b87c8'; + context.lineWidth = 8; + context.beginPath(); + context.moveTo(x + 100, y + 128); + context.lineTo(x + 202, y + 242); + context.stroke(); + context.fillStyle = '#f8fbff'; + context.fillRect(x + 150, y + 38, 12, 42); + } else if (decoration === 'zhugeLiang') { + context.globalAlpha = 0.82; + context.fillStyle = '#eef2df'; + context.beginPath(); + context.moveTo(x + 118, y + 112); + context.lineTo(x + 96, y + 252); + context.lineTo(x + 198, y + 252); + context.lineTo(x + 176, y + 112); + context.closePath(); + context.fill(); + context.strokeStyle = '#d8b15f'; + context.lineWidth = 8; + context.beginPath(); + context.moveTo(x + 142, y + 88); + context.lineTo(x + 190, y + 72); + context.stroke(); + context.strokeStyle = '#f7f0d6'; + context.lineWidth = 11; + for (let index = 0; index < 5; index += 1) { + context.beginPath(); + context.moveTo(x + 206, y + 208); + context.lineTo(x + 178 + index * 18, y + 132 + Math.abs(index - 2) * 8); + context.stroke(); + } + context.fillStyle = '#f7f0d6'; + context.beginPath(); + context.ellipse(x + 206, y + 208, 18, 26, -0.5, 0, Math.PI * 2); + context.fill(); + } else if (decoration === 'jiangWei') { + context.globalAlpha = 0.84; + context.strokeStyle = '#74f0c8'; + context.lineWidth = 14; + context.beginPath(); + context.moveTo(x + 92, y + 122); + context.lineTo(x + 214, y + 244); + context.stroke(); + context.strokeStyle = '#f1f5e8'; + context.lineWidth = 10; + context.beginPath(); + context.moveTo(x + 226, y + 60); + context.lineTo(x + 118, y + 274); + context.stroke(); + context.fillStyle = '#edf4dc'; + context.fillRect(x + 194, y + 176, 34, 54); + context.strokeStyle = '#2f6e64'; + context.lineWidth = 5; + context.strokeRect(x + 194, y + 176, 34, 54); + } else { + context.globalAlpha = 0.86; + context.fillStyle = '#111522'; + context.beginPath(); + context.moveTo(x + 102, y + 102); + context.lineTo(x + 70, y + 266); + context.lineTo(x + 218, y + 266); + context.lineTo(x + 194, y + 102); + context.closePath(); + context.fill(); + context.strokeStyle = '#d8b15f'; + context.lineWidth = 8; + context.beginPath(); + context.moveTo(x + 86, y + 118); + context.lineTo(x + 214, y + 118); + context.stroke(); + context.strokeStyle = '#9d3c3c'; + context.lineWidth = 12; + context.beginPath(); + context.moveTo(x + 212, y + 66); + context.lineTo(x + 232, y + 238); + context.stroke(); + context.fillStyle = '#71272f'; + context.fillRect(x + 218, y + 72, 48, 42); + } + + context.restore(); + } + private createUnitAnimations() { animatedUnitSheets.forEach(({ key }) => { (['south', 'east', 'north', 'west'] as UnitDirection[]).forEach((direction) => {