Add class and terrain battle rules

This commit is contained in:
2026-06-22 02:12:57 +09:00
parent 6db887e2da
commit f99884ea29
4 changed files with 314 additions and 83 deletions

View File

@@ -5,19 +5,12 @@ import {
firstBattleMap,
firstBattleUnits,
type BattleBond,
type TerrainType,
type UnitData,
type UnitStats
} from '../data/scenario';
import { getTerrainRule, getUnitClass, type TerrainType } from '../data/battleRules';
import { palette } from '../ui/palette';
const terrainColor: Record<TerrainType, number> = {
plain: 0x6d8056,
forest: 0x355f42,
hill: 0x82724e,
fort: 0x8b6f48
};
const unitTexture: Record<string, string> = {
'liu-bei': 'unit-liu-bei',
'guan-yu': 'unit-guan-yu',
@@ -208,13 +201,14 @@ export class BattleScene extends Phaser.Scene {
firstBattleMap.terrain.forEach((row, y) => {
row.forEach((terrain, x) => {
const terrainRule = getTerrainRule(terrain);
const tile = this.add.rectangle(
layout.gridX + x * layout.tileSize,
layout.gridY + y * layout.tileSize,
layout.tileSize,
layout.tileSize,
terrainColor[terrain],
this.terrainAlpha(terrain)
terrainRule.color,
terrainRule.alpha
);
tile.setOrigin(0);
tile.setStrokeStyle(1, 0x0a1014, 0.5);
@@ -328,34 +322,28 @@ export class BattleScene extends Phaser.Scene {
}
private showMoveRange(unit: UnitData) {
for (let y = 0; y < firstBattleMap.height; y += 1) {
for (let x = 0; x < firstBattleMap.width; x += 1) {
if (!this.canMoveTo(unit, x, y)) {
continue;
this.reachableTiles(unit).forEach(({ x, y }) => {
const marker = this.add.rectangle(
this.layout.gridX + x * this.layout.tileSize,
this.layout.gridY + y * this.layout.tileSize,
this.layout.tileSize,
this.layout.tileSize,
palette.blue,
0.24
);
marker.setOrigin(0);
marker.setStrokeStyle(1, palette.blue, 0.68);
marker.setDepth(4);
marker.setInteractive({ useHandCursor: true });
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.36));
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.24));
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (pointer.leftButtonDown()) {
this.moveSelectedUnit(x, y, pointer);
}
const marker = this.add.rectangle(
this.layout.gridX + x * this.layout.tileSize,
this.layout.gridY + y * this.layout.tileSize,
this.layout.tileSize,
this.layout.tileSize,
palette.blue,
0.24
);
marker.setOrigin(0);
marker.setStrokeStyle(1, palette.blue, 0.68);
marker.setDepth(4);
marker.setInteractive({ useHandCursor: true });
marker.on('pointerover', () => marker.setFillStyle(palette.blue, 0.36));
marker.on('pointerout', () => marker.setFillStyle(palette.blue, 0.24));
marker.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (pointer.leftButtonDown()) {
this.moveSelectedUnit(x, y, pointer);
}
});
this.markers.push(marker);
}
}
});
this.markers.push(marker);
});
}
private moveSelectedUnit(x: number, y: number, pointer?: Phaser.Input.Pointer) {
@@ -386,35 +374,69 @@ export class BattleScene extends Phaser.Scene {
}
private canMoveTo(unit: UnitData, x: number, y: number) {
const distance = Math.abs(unit.x - x) + Math.abs(unit.y - y);
return (
distance > 0 &&
distance <= unit.move &&
x >= 0 &&
y >= 0 &&
x < firstBattleMap.width &&
y < firstBattleMap.height &&
!this.isOccupied(x, y, unit.id)
);
return this.reachableTiles(unit).some((tile) => tile.x === x && tile.y === y);
}
private reachableTiles(unit: UnitData) {
const directions = [
{ x: 1, y: 0 },
{ x: -1, y: 0 },
{ x: 0, y: 1 },
{ x: 0, y: -1 }
];
const costs = new Map<string, number>([[this.tileKey(unit.x, unit.y), 0]]);
const queue = [{ x: unit.x, y: unit.y, cost: 0 }];
const reachable = new Map<string, { x: number; y: number; cost: number }>();
while (queue.length > 0) {
queue.sort((a, b) => a.cost - b.cost);
const current = queue.shift();
if (!current) {
break;
}
directions.forEach((direction) => {
const nextX = current.x + direction.x;
const nextY = current.y + direction.y;
if (!this.isInBounds(nextX, nextY) || this.isOccupied(nextX, nextY, unit.id)) {
return;
}
const terrain = firstBattleMap.terrain[nextY][nextX];
const stepCost = this.movementCost(unit, terrain);
const nextCost = current.cost + stepCost;
const key = this.tileKey(nextX, nextY);
if (nextCost > unit.move || nextCost >= (costs.get(key) ?? Number.POSITIVE_INFINITY)) {
return;
}
costs.set(key, nextCost);
const tile = { x: nextX, y: nextY, cost: nextCost };
reachable.set(key, tile);
queue.push(tile);
});
}
return Array.from(reachable.values());
}
private movementCost(unit: UnitData, terrain: TerrainType) {
const unitClass = getUnitClass(unit.classKey);
return unitClass.movementCosts?.[terrain] ?? getTerrainRule(terrain).moveCost;
}
private isInBounds(x: number, y: number) {
return x >= 0 && y >= 0 && x < firstBattleMap.width && y < firstBattleMap.height;
}
private tileKey(x: number, y: number) {
return `${x},${y}`;
}
private isOccupied(x: number, y: number, exceptUnitId?: string) {
return firstBattleUnits.some((unit) => unit.id !== exceptUnitId && unit.x === x && unit.y === y);
}
private terrainAlpha(terrain: TerrainType) {
if (terrain === 'forest') {
return 0.16;
}
if (terrain === 'hill') {
return 0.13;
}
if (terrain === 'fort') {
return 0.18;
}
return 0.03;
}
private tileCenterX(x: number) {
return this.layout.gridX + x * this.layout.tileSize + this.layout.tileSize / 2;
}
@@ -1028,6 +1050,7 @@ export class BattleScene extends Phaser.Scene {
private renderRosterRow(unit: UnitData, x: number, y: number, width: number) {
const acted = this.actedUnitIds.has(unit.id);
const active = this.selectedUnit?.id === unit.id;
const unitClass = getUnitClass(unit.classKey);
const rowBg = this.trackSideObject(this.add.rectangle(x, y, width, 42, active ? 0x2f4050 : 0x101820, acted ? 0.54 : 0.9));
rowBg.setOrigin(0);
rowBg.setStrokeStyle(1, active ? palette.gold : unit.faction === 'ally' ? palette.blue : 0xb86b55, active ? 0.88 : 0.52);
@@ -1041,7 +1064,8 @@ export class BattleScene extends Phaser.Scene {
color: nameColor,
fontStyle: '700'
}));
this.trackSideObject(this.add.text(x + 88, y + 9, unit.className, {
this.trackSideObject(this.add.text(x + 88, y + 9, unitClass.name,
{
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: acted ? '#8c8c8c' : '#9fb0bf'
@@ -1076,18 +1100,22 @@ export class BattleScene extends Phaser.Scene {
const top = panelY + 124;
const acted = this.actedUnitIds.has(unit.id);
const factionColor = unit.faction === 'ally' ? palette.blue : 0xb86b55;
const unitClass = getUnitClass(unit.classKey);
const terrain = firstBattleMap.terrain[unit.y][unit.x];
const terrainRule = getTerrainRule(terrain);
const terrainRating = unitClass.terrainRatings[terrain];
const header = this.trackSideObject(this.add.rectangle(left, top, width, 60, 0x101820, 0.94));
header.setOrigin(0);
header.setStrokeStyle(1, factionColor, 0.76);
this.trackSideObject(this.add.text(left + 14, top + 9, rosterLabels[unit.faction], {
this.trackSideObject(this.add.text(left + 14, top + 9, `${rosterLabels[unit.faction]} / ${unitClass.family}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '14px',
color: unit.faction === 'ally' ? '#9fd0ff' : '#ffb2a4',
fontStyle: '700'
}));
this.trackSideObject(this.add.text(left + 14, top + 29, `${unit.name} ${unit.className}`, {
this.trackSideObject(this.add.text(left + 14, top + 29, `${unit.name} ${unitClass.name}`, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '22px',
color: acted ? '#bdbdbd' : '#f2e3bf',
@@ -1133,7 +1161,7 @@ export class BattleScene extends Phaser.Scene {
this.renderStatRow(stat.label, unit.stats[stat.key], left, statTop + index * 32, width);
});
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1}${acted ? ' / 행동완료' : ''}`;
const positionText = `위치 ${unit.x + 1}, ${unit.y + 1} / ${terrainRule.label} ${terrainRating}%${acted ? ' / 행동완료' : ''}`;
this.trackSideObject(this.add.text(left, top + 328, positionText, {
fontFamily: '"Malgun Gothic", "Noto Sans KR", sans-serif',
fontSize: '15px',