Improve battle layout and visuals
This commit is contained in:
BIN
src/assets/images/battle/first-battle-map.png
Normal file
BIN
src/assets/images/battle/first-battle-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
@@ -3,9 +3,6 @@ import { soundDirector } from '../audio/SoundDirector';
|
||||
import { firstBattleMap, firstBattleUnits, type TerrainType, type UnitData } from '../data/scenario';
|
||||
import { palette } from '../ui/palette';
|
||||
|
||||
const tileSize = 48;
|
||||
const mapOffset = { x: 64, y: 72 };
|
||||
|
||||
const terrainColor: Record<TerrainType, number> = {
|
||||
plain: 0x6d8056,
|
||||
forest: 0x355f42,
|
||||
@@ -13,7 +10,33 @@ const terrainColor: Record<TerrainType, number> = {
|
||||
fort: 0x8b6f48
|
||||
};
|
||||
|
||||
const unitTexture: Record<string, string> = {
|
||||
'liu-bei': 'unit-liu-bei',
|
||||
'guan-yu': 'unit-guan-yu',
|
||||
'zhang-fei': 'unit-zhang-fei',
|
||||
'rebel-a': 'unit-rebel',
|
||||
'rebel-b': 'unit-rebel',
|
||||
'rebel-c': 'unit-rebel-cavalry',
|
||||
'rebel-leader': 'unit-rebel-leader'
|
||||
};
|
||||
|
||||
type BattleLayout = {
|
||||
mapX: number;
|
||||
mapY: number;
|
||||
mapWidth: number;
|
||||
mapHeight: number;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
gridSize: number;
|
||||
tileSize: number;
|
||||
panelX: number;
|
||||
panelY: number;
|
||||
panelWidth: number;
|
||||
panelHeight: number;
|
||||
};
|
||||
|
||||
export class BattleScene extends Phaser.Scene {
|
||||
private layout!: BattleLayout;
|
||||
private selectedUnit?: UnitData;
|
||||
private infoText?: Phaser.GameObjects.Text;
|
||||
private turnText?: Phaser.GameObjects.Text;
|
||||
@@ -25,105 +48,186 @@ export class BattleScene extends Phaser.Scene {
|
||||
|
||||
create() {
|
||||
const { width, height } = this.scale;
|
||||
this.layout = this.createLayout(width, height);
|
||||
soundDirector.playMusic('battle-prep');
|
||||
this.add.rectangle(0, 0, width, height, 0x0f1418).setOrigin(0);
|
||||
|
||||
this.add.rectangle(0, 0, width, height, 0x080b0d).setOrigin(0);
|
||||
this.drawMap();
|
||||
this.drawUnits();
|
||||
this.drawSidePanel(width, height);
|
||||
this.setInfo('아군 차례입니다. 유비를 선택해 이동 범위를 확인하세요.');
|
||||
this.drawSidePanel();
|
||||
this.setInfo('아군 차례입니다.\n유비를 선택해 이동 범위를 확인하세요.');
|
||||
}
|
||||
|
||||
private createLayout(width: number, height: number): BattleLayout {
|
||||
const margin = 24;
|
||||
const gap = 20;
|
||||
const panelWidth = Phaser.Math.Clamp(Math.floor(width * 0.27), 320, 360);
|
||||
const mapWidth = width - panelWidth - gap - margin * 2;
|
||||
const mapHeight = height - margin * 2;
|
||||
const gridSize = Math.floor(Math.min(mapHeight - 36, mapWidth - 72) / firstBattleMap.width) * firstBattleMap.width;
|
||||
const tileSize = gridSize / firstBattleMap.width;
|
||||
const mapX = margin;
|
||||
const mapY = margin;
|
||||
|
||||
return {
|
||||
mapX,
|
||||
mapY,
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
gridX: mapX + Math.floor((mapWidth - gridSize) / 2),
|
||||
gridY: mapY + Math.floor((mapHeight - gridSize) / 2),
|
||||
gridSize,
|
||||
tileSize,
|
||||
panelX: mapX + mapWidth + gap,
|
||||
panelY: margin,
|
||||
panelWidth,
|
||||
panelHeight: mapHeight
|
||||
};
|
||||
}
|
||||
|
||||
private drawMap() {
|
||||
const layout = this.layout;
|
||||
const background = this.add.image(
|
||||
layout.mapX + layout.mapWidth / 2,
|
||||
layout.mapY + layout.mapHeight / 2,
|
||||
'battle-map-first'
|
||||
);
|
||||
const source = this.textures.get('battle-map-first').getSourceImage();
|
||||
const coverScale = Math.max(layout.mapWidth / source.width, layout.mapHeight / source.height);
|
||||
background.setScale(coverScale);
|
||||
background.setDepth(0);
|
||||
|
||||
const maskShape = this.add.graphics();
|
||||
maskShape.fillStyle(0xffffff, 1);
|
||||
maskShape.fillRect(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight);
|
||||
maskShape.setAlpha(0);
|
||||
background.setMask(maskShape.createGeometryMask());
|
||||
|
||||
this.add.rectangle(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight, 0x06090b, 0.16).setOrigin(0).setDepth(1);
|
||||
this.add
|
||||
.rectangle(layout.mapX, layout.mapY, layout.mapWidth, layout.mapHeight)
|
||||
.setOrigin(0)
|
||||
.setStrokeStyle(2, palette.gold, 0.62)
|
||||
.setDepth(8);
|
||||
|
||||
firstBattleMap.terrain.forEach((row, y) => {
|
||||
row.forEach((terrain, x) => {
|
||||
const tileX = mapOffset.x + x * tileSize;
|
||||
const tileY = mapOffset.y + y * tileSize;
|
||||
const tile = this.add.rectangle(tileX, tileY, tileSize, tileSize, terrainColor[terrain]).setOrigin(0);
|
||||
tile.setStrokeStyle(1, 0x182028, 0.8);
|
||||
const tileX = layout.gridX + x * layout.tileSize;
|
||||
const tileY = layout.gridY + y * layout.tileSize;
|
||||
const terrainAlpha = terrain === 'plain' ? 0.03 : terrain === 'forest' ? 0.16 : terrain === 'hill' ? 0.13 : 0.18;
|
||||
const tile = this.add.rectangle(tileX, tileY, layout.tileSize, layout.tileSize, terrainColor[terrain], terrainAlpha);
|
||||
tile.setOrigin(0);
|
||||
tile.setStrokeStyle(1, 0x0a1014, 0.5);
|
||||
tile.setDepth(2);
|
||||
});
|
||||
});
|
||||
|
||||
this.add
|
||||
.rectangle(layout.gridX, layout.gridY, layout.gridSize, layout.gridSize)
|
||||
.setOrigin(0)
|
||||
.setStrokeStyle(2, 0xf1d489, 0.48)
|
||||
.setDepth(3);
|
||||
}
|
||||
|
||||
private drawUnits() {
|
||||
const layout = this.layout;
|
||||
|
||||
firstBattleUnits.forEach((unit) => {
|
||||
const unitSize = unit.faction === 'ally' ? layout.tileSize * 0.78 : layout.tileSize * 0.72;
|
||||
const sprite = this.add.image(
|
||||
mapOffset.x + unit.x * tileSize + tileSize / 2,
|
||||
mapOffset.y + unit.y * tileSize + tileSize / 2,
|
||||
unit.faction === 'ally' ? 'unit-ally' : 'unit-enemy'
|
||||
layout.gridX + unit.x * layout.tileSize + layout.tileSize / 2,
|
||||
layout.gridY + unit.y * layout.tileSize + layout.tileSize / 2,
|
||||
unitTexture[unit.id] ?? 'unit-rebel'
|
||||
);
|
||||
sprite.setDisplaySize(unitSize, unitSize);
|
||||
sprite.setDepth(6);
|
||||
sprite.setInteractive({ useHandCursor: unit.faction === 'ally' });
|
||||
sprite.on('pointerdown', () => this.selectUnit(unit));
|
||||
|
||||
this.add.text(sprite.x - 22, sprite.y + 22, unit.name, {
|
||||
const label = this.add.text(sprite.x, sprite.y + layout.tileSize * 0.35, unit.name, {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '13px',
|
||||
color: unit.faction === 'ally' ? '#e7edf7' : '#ffebe7',
|
||||
backgroundColor: '#10131a',
|
||||
padding: { left: 4, right: 4, top: 2, bottom: 2 }
|
||||
backgroundColor: 'rgba(8,10,14,0.78)',
|
||||
padding: { left: 5, right: 5, top: 2, bottom: 2 }
|
||||
});
|
||||
label.setOrigin(0.5, 0);
|
||||
label.setDepth(7);
|
||||
});
|
||||
}
|
||||
|
||||
private drawSidePanel(width: number, height: number) {
|
||||
const panelX = mapOffset.x + firstBattleMap.width * tileSize + 36;
|
||||
this.add.rectangle(panelX, 72, width - panelX - 64, height - 128, palette.panel, 0.96).setOrigin(0);
|
||||
this.add.text(panelX + 30, 108, '첫 전투', {
|
||||
fontSize: '32px',
|
||||
private drawSidePanel() {
|
||||
const { panelX, panelY, panelWidth, panelHeight } = this.layout;
|
||||
this.add.rectangle(panelX, panelY, panelWidth, panelHeight, palette.panel, 0.96).setOrigin(0);
|
||||
this.add.rectangle(panelX, panelY, 3, panelHeight, palette.gold, 0.82).setOrigin(0);
|
||||
|
||||
this.add.text(panelX + 24, panelY + 28, '첫 전투', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '30px',
|
||||
color: '#e8dfca',
|
||||
fontStyle: '700'
|
||||
fontStyle: '700',
|
||||
padding: { top: 4, bottom: 4 }
|
||||
});
|
||||
this.turnText = this.add.text(panelX + 30, 164, '1턴 / 아군', {
|
||||
this.turnText = this.add.text(panelX + 24, panelY + 88, '1턴 / 아군', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '22px',
|
||||
color: '#d8b15f'
|
||||
});
|
||||
this.infoText = this.add.text(panelX + 30, 224, '', {
|
||||
this.infoText = this.add.text(panelX + 24, panelY + 144, '', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '20px',
|
||||
color: '#e8dfca',
|
||||
wordWrap: { width: width - panelX - 124, useAdvancedWrap: true },
|
||||
wordWrap: { width: panelWidth - 48, useAdvancedWrap: true },
|
||||
lineSpacing: 8
|
||||
});
|
||||
|
||||
this.add.text(panelX + 30, height - 186, '승리 조건: 적 전멸', {
|
||||
this.add.text(panelX + 24, panelY + panelHeight - 166, '승리 조건: 적 전멸', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#9aa3ad'
|
||||
});
|
||||
this.add.text(panelX + 30, height - 148, '패배 조건: 유비 퇴각', {
|
||||
this.add.text(panelX + 24, panelY + panelHeight - 126, '패배 조건: 유비 퇴각', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#9aa3ad'
|
||||
});
|
||||
this.add.text(panelX + 30, height - 110, '다음 단계: 이동/공격/AI 구현', {
|
||||
this.add.text(panelX + 24, panelY + panelHeight - 86, '다음 단계: 이동 / 공격 / AI', {
|
||||
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
|
||||
fontSize: '18px',
|
||||
color: '#9aa3ad'
|
||||
});
|
||||
this.turnText.setText('1턴 / 아군');
|
||||
}
|
||||
|
||||
private selectUnit(unit: UnitData) {
|
||||
if (unit.faction !== 'ally') {
|
||||
this.setInfo(`${unit.name}: ${unit.className}, HP ${unit.hp}/${unit.maxHp}`);
|
||||
this.setInfo(`${unit.name}: ${unit.className}\nHP ${unit.hp}/${unit.maxHp}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedUnit = unit;
|
||||
this.clearMarkers();
|
||||
this.showMoveRange(unit);
|
||||
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}, 공격 ${unit.attack}, 이동 ${unit.move}`);
|
||||
this.setInfo(`${unit.name} (${unit.className})\nHP ${unit.hp}/${unit.maxHp}\n공격 ${unit.attack} / 이동 ${unit.move}`);
|
||||
}
|
||||
|
||||
private showMoveRange(unit: UnitData) {
|
||||
const layout = this.layout;
|
||||
|
||||
for (let y = 0; y < firstBattleMap.height; y += 1) {
|
||||
for (let x = 0; x < firstBattleMap.width; x += 1) {
|
||||
const distance = Math.abs(unit.x - x) + Math.abs(unit.y - y);
|
||||
if (distance > 0 && distance <= unit.move) {
|
||||
const marker = this.add.rectangle(
|
||||
mapOffset.x + x * tileSize,
|
||||
mapOffset.y + y * tileSize,
|
||||
tileSize,
|
||||
tileSize,
|
||||
layout.gridX + x * layout.tileSize,
|
||||
layout.gridY + y * layout.tileSize,
|
||||
layout.tileSize,
|
||||
layout.tileSize,
|
||||
palette.blue,
|
||||
0.24
|
||||
);
|
||||
marker.setOrigin(0);
|
||||
marker.setStrokeStyle(1, palette.blue, 0.6);
|
||||
marker.setStrokeStyle(1, palette.blue, 0.68);
|
||||
marker.setDepth(4);
|
||||
this.markers.push(marker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Phaser from 'phaser';
|
||||
import firstBattleMapUrl from '../../assets/images/battle/first-battle-map.png';
|
||||
import titleBackgroundUrl from '../../assets/images/taoyuan-oath-title.png';
|
||||
import storyChaosUrl from '../../assets/images/story/01-yellow-turban-chaos.png';
|
||||
import storyLiuBeiUrl from '../../assets/images/story/02-liu-bei-resolve.png';
|
||||
@@ -22,6 +23,7 @@ export class BootScene extends Phaser.Scene {
|
||||
this.load.image('story-three-heroes', storyThreeHeroesUrl);
|
||||
this.load.image('story-militia', storyMilitiaUrl);
|
||||
this.load.image('story-sortie', storySortieUrl);
|
||||
this.load.image('battle-map-first', firstBattleMapUrl);
|
||||
this.load.image('portrait-liu-bei', liuBeiPortraitUrl);
|
||||
this.load.image('portrait-guan-yu', guanYuPortraitUrl);
|
||||
this.load.image('portrait-zhang-fei', zhangFeiPortraitUrl);
|
||||
@@ -34,8 +36,7 @@ export class BootScene extends Phaser.Scene {
|
||||
|
||||
private createGeneratedTextures() {
|
||||
this.createPetalTexture();
|
||||
this.createUnitTexture('unit-ally', palette.blue, 0xe7edf7);
|
||||
this.createUnitTexture('unit-enemy', palette.red, 0xffebe7);
|
||||
this.createPixelUnitTextures();
|
||||
}
|
||||
|
||||
private createPetalTexture() {
|
||||
@@ -48,17 +49,144 @@ export class BootScene extends Phaser.Scene {
|
||||
graphics.destroy();
|
||||
}
|
||||
|
||||
private createUnitTexture(key: string, primary: number, accent: number) {
|
||||
private createPixelUnitTextures() {
|
||||
this.createPixelUnitTexture('unit-liu-bei', {
|
||||
robe: palette.blue,
|
||||
trim: 0xe7edf7,
|
||||
skin: 0xf0c79a,
|
||||
hair: 0x1e1b17,
|
||||
weapon: 0xd8d0ba,
|
||||
accent: 0xd8b15f
|
||||
});
|
||||
this.createPixelUnitTexture('unit-guan-yu', {
|
||||
robe: 0x2f7d45,
|
||||
trim: 0xd7e6c5,
|
||||
skin: 0xd59b6e,
|
||||
hair: 0x241611,
|
||||
weapon: 0xc7d2d8,
|
||||
accent: 0xb64232,
|
||||
beard: true,
|
||||
halberd: true
|
||||
});
|
||||
this.createPixelUnitTexture('unit-zhang-fei', {
|
||||
robe: 0x5b2b33,
|
||||
trim: 0xefc67c,
|
||||
skin: 0xb97955,
|
||||
hair: 0x16110f,
|
||||
weapon: 0xcfd8d8,
|
||||
accent: 0x15191f,
|
||||
spear: true
|
||||
});
|
||||
this.createPixelUnitTexture('unit-rebel', {
|
||||
robe: 0xc8a032,
|
||||
trim: 0x6e4f24,
|
||||
skin: 0xbc8053,
|
||||
hair: 0x242015,
|
||||
weapon: 0xbfc5bd,
|
||||
accent: 0xf1d157,
|
||||
spear: true
|
||||
});
|
||||
this.createPixelUnitTexture('unit-rebel-cavalry', {
|
||||
robe: 0xd0aa3f,
|
||||
trim: 0x7a4b2a,
|
||||
skin: 0xbc8053,
|
||||
hair: 0x262017,
|
||||
weapon: 0xbfc5bd,
|
||||
accent: 0x56351f,
|
||||
mounted: true
|
||||
});
|
||||
this.createPixelUnitTexture('unit-rebel-leader', {
|
||||
robe: 0x9e7130,
|
||||
trim: 0xf4d66f,
|
||||
skin: 0xc98b5d,
|
||||
hair: 0x1e1710,
|
||||
weapon: 0xd4d7c6,
|
||||
accent: 0xe4c43d,
|
||||
halberd: true,
|
||||
cape: true
|
||||
});
|
||||
}
|
||||
|
||||
private createPixelUnitTexture(
|
||||
key: string,
|
||||
colors: {
|
||||
robe: number;
|
||||
trim: number;
|
||||
skin: number;
|
||||
hair: number;
|
||||
weapon: number;
|
||||
accent: number;
|
||||
beard?: boolean;
|
||||
spear?: boolean;
|
||||
halberd?: boolean;
|
||||
mounted?: boolean;
|
||||
cape?: boolean;
|
||||
}
|
||||
) {
|
||||
const graphics = this.make.graphics({ x: 0, y: 0 });
|
||||
graphics.fillStyle(0x0c0f14, 0.5);
|
||||
graphics.fillEllipse(32, 49, 44, 17);
|
||||
graphics.fillStyle(primary, 1);
|
||||
graphics.fillRoundedRect(14, 12, 36, 38, 8);
|
||||
graphics.fillStyle(accent, 1);
|
||||
graphics.fillCircle(32, 17, 9);
|
||||
graphics.lineStyle(3, 0x0b0d12, 0.55);
|
||||
graphics.strokeRoundedRect(14, 12, 36, 38, 8);
|
||||
graphics.generateTexture(key, 64, 64);
|
||||
graphics.fillStyle(0x07090c, 0.42);
|
||||
graphics.fillRect(6, 27, 20, 3);
|
||||
|
||||
if (colors.mounted) {
|
||||
graphics.fillStyle(0x5b3824, 1);
|
||||
graphics.fillRect(7, 18, 18, 6);
|
||||
graphics.fillRect(5, 21, 5, 3);
|
||||
graphics.fillRect(22, 17, 4, 4);
|
||||
graphics.fillStyle(0x2f2118, 1);
|
||||
graphics.fillRect(9, 24, 3, 5);
|
||||
graphics.fillRect(21, 23, 3, 5);
|
||||
}
|
||||
|
||||
if (colors.cape) {
|
||||
graphics.fillStyle(0x6c2722, 1);
|
||||
graphics.fillRect(10, 13, 13, 13);
|
||||
graphics.fillRect(8, 18, 5, 7);
|
||||
}
|
||||
|
||||
graphics.fillStyle(colors.weapon, 1);
|
||||
if (colors.spear) {
|
||||
graphics.fillRect(24, 6, 2, 22);
|
||||
graphics.fillRect(23, 5, 4, 2);
|
||||
} else if (colors.halberd) {
|
||||
graphics.fillRect(24, 5, 2, 23);
|
||||
graphics.fillRect(22, 5, 6, 2);
|
||||
graphics.fillRect(25, 7, 4, 3);
|
||||
} else {
|
||||
graphics.fillRect(23, 12, 2, 14);
|
||||
graphics.fillRect(22, 11, 5, 2);
|
||||
}
|
||||
|
||||
graphics.fillStyle(colors.robe, 1);
|
||||
graphics.fillRect(11, 13, 10, 12);
|
||||
graphics.fillRect(9, 18, 14, 8);
|
||||
graphics.fillStyle(colors.trim, 1);
|
||||
graphics.fillRect(12, 14, 8, 2);
|
||||
graphics.fillRect(10, 25, 13, 2);
|
||||
|
||||
graphics.fillStyle(colors.skin, 1);
|
||||
graphics.fillRect(12, 7, 8, 7);
|
||||
graphics.fillRect(10, 15, 3, 5);
|
||||
graphics.fillRect(20, 15, 3, 5);
|
||||
graphics.fillStyle(colors.hair, 1);
|
||||
graphics.fillRect(11, 6, 10, 3);
|
||||
graphics.fillRect(10, 8, 2, 5);
|
||||
graphics.fillRect(20, 8, 2, 5);
|
||||
|
||||
if (colors.beard) {
|
||||
graphics.fillStyle(0x2a1712, 1);
|
||||
graphics.fillRect(13, 13, 6, 6);
|
||||
graphics.fillRect(14, 19, 4, 4);
|
||||
}
|
||||
|
||||
graphics.fillStyle(colors.accent, 1);
|
||||
graphics.fillRect(14, 5, 4, 2);
|
||||
graphics.fillRect(14, 17, 5, 2);
|
||||
graphics.fillStyle(0x06080b, 1);
|
||||
graphics.fillRect(13, 10, 2, 1);
|
||||
graphics.fillRect(18, 10, 2, 1);
|
||||
|
||||
graphics.generateTexture(key, 32, 32);
|
||||
graphics.destroy();
|
||||
this.textures.get(key).setFilter(Phaser.Textures.FilterMode.NEAREST);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user