feat: improve spawning, victory logic, and camera controls

This commit is contained in:
2026-05-22 01:37:41 +09:00
parent 75f65e7918
commit 1d0d791001
8 changed files with 223 additions and 44 deletions
+104 -14
View File
@@ -16,7 +16,19 @@ export class ArenaScene extends Phaser.Scene {
this.matchId = 0;
this.matchOver = false;
this.ready = false;
this.setStatus = setStatus;
this.setStatus = (message) => {
// 기존 배너 제거
const oldBanner = document.querySelector(".victory-banner");
if (oldBanner) oldBanner.remove();
// 승리 또는 무승부 메시지인 경우 전용 배너 생성
if (message.includes("승리") || message.includes("무승부")) {
const banner = document.createElement("div");
banner.className = "victory-banner";
banner.textContent = message;
document.querySelector(".arena-shell").appendChild(banner);
}
};
this.teams = [];
}
@@ -30,6 +42,24 @@ export class ArenaScene extends Phaser.Scene {
this.cameras.main.setBackgroundColor("#282819");
drawArena(this);
createFighterAnimations(this, fighterManifest);
// 미니맵 카메라 설정
this.minimapCamera = this.cameras.add(10, 10, 150, 150).setZoom(150 / ARENA_SIZE).setName('minimap');
this.minimapCamera.setBackgroundColor(0x000000);
this.minimapCamera.scrollX = 0;
this.minimapCamera.scrollY = 0;
this.minimapCamera.setAlpha(0); // 기본적으로는 숨김
// 마우스 휠로 줌 조절
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
const zoomStep = 0.1;
const newZoom = Phaser.Math.Clamp(this.cameras.main.zoom + (deltaY > 0 ? -zoomStep : zoomStep), 1, 3);
this.cameras.main.setZoom(newZoom);
// 확대 시 미니맵 표시
this.minimapCamera.setAlpha(newZoom > 1 ? 0.8 : 0);
});
this.ready = true;
this.startMatch(this.getInitialMatchConfig());
}
@@ -60,33 +90,93 @@ export class ArenaScene extends Phaser.Scene {
);
this.setStatus(matchStatusText(this.teams));
this.updateScoreboard();
}
update(time) {
this.fighters.forEach(syncFighterHud);
if (this.matchOver) {
return;
}
update(time) {
this.fighters.forEach(syncFighterHud);
if (this.matchOver) {
return;
// 확대 상태일 때 생존 캐릭터들의 중앙으로 카메라 이동
if (this.cameras.main.zoom > 1) {
const aliveFighters = this.fighters.filter(f => !f.isDead);
if (aliveFighters.length > 0) {
const avgX = aliveFighters.reduce((sum, f) => sum + f.x, 0) / aliveFighters.length;
const avgY = aliveFighters.reduce((sum, f) => sum + f.y, 0) / aliveFighters.length;
// 소수점 단위 변동으로 인한 지터링 방지를 위해 반올림 처리 및 부드러운 이동(Lerp) 적용
const targetX = Math.round(avgX);
const targetY = Math.round(avgY);
// 현재 카메라 위치에서 목표 위치로 서서히 이동 (0.1은 따라가는 속도)
this.cameras.main.scrollX += (targetX - this.cameras.main.centerX) * 0.1;
this.cameras.main.scrollY += (targetY - this.cameras.main.centerY) * 0.1;
}
} else {
// 줌이 1일 때는 경기장 중앙에 고정
this.cameras.main.centerOn(ARENA_SIZE / 2, ARENA_SIZE / 2);
}
this.fighters.forEach((fighter) => {
updateFighter(this, fighter, time, () => this.finishMatch());
this.fighters.forEach((fighter) => {
updateFighter(this, fighter, time, () => {
this.updateScoreboard();
this.finishMatch();
});
});
}
updateScoreboard() {
const scoreLeft = document.getElementById("score-left");
const scoreRight = document.getElementById("score-right");
if (!scoreLeft || !scoreRight) return;
scoreLeft.innerHTML = "";
scoreRight.innerHTML = "";
this.teams.forEach((team, index) => {
const aliveCount = this.fighters.filter(
(f) => f.team.id === team.id && !f.isDead
).length;
const teamEl = document.createElement("div");
teamEl.className = "team-score";
teamEl.style.backgroundColor = `${team.color}44`; // 44 is alpha for 26%
teamEl.style.borderLeft = `4px solid ${team.color}`;
teamEl.innerHTML = `<span>${team.label}</span> <span>${aliveCount}</span>`;
if (index % 2 === 0) {
scoreLeft.appendChild(teamEl);
} else {
scoreRight.appendChild(teamEl);
}
});
}
finishMatch() {
const livingTeams = new Set(
this.fighters.filter((fighter) => !fighter.isDead).map((fighter) => fighter.team.id),
);
const livingFighters = this.fighters.filter((fighter) => !fighter.isDead);
const livingTeams = new Set(livingFighters.map((fighter) => fighter.team.id));
if (livingTeams.size > 1) {
return;
}
const winningTeam = this.teams.find((team) => livingTeams.has(team.id));
this.matchOver = true;
clearCombatObjects(this);
this.fighters.forEach((fighter) => fighter.body.setVelocity(0, 0));
this.setStatus(`${winningTeam?.label ?? "Draw"} 승리`);
this.fighters.forEach((fighter) => {
if (fighter.body) {
fighter.body.setVelocity(0, 0);
}
});
if (livingTeams.size === 1) {
const winningTeamId = Array.from(livingTeams)[0];
const winningTeam = this.teams.find((team) => team.id === winningTeamId);
this.setStatus(`${winningTeam?.label ?? "Unknown"} 승리!`);
} else {
this.setStatus("무승부!");
}
}
}
+4 -1
View File
@@ -213,7 +213,10 @@ function applyHit(scene, attacker, defender, onWinner, matchId, { instantKill =
defender.isLocked = true;
playAnimation(defender, "hurt");
scene.cameras.main.shake(90, 0.002);
if (instantKill) {
scene.cameras.main.shake(90, 0.002);
}
}
function getAttackRange(fighter) {
+1 -1
View File
@@ -1,4 +1,4 @@
export const GRID_SIZE = 16;
export const GRID_SIZE = 50;
export const TILE_SIZE = 64;
export const ARENA_SIZE = GRID_SIZE * TILE_SIZE;
+26 -21
View File
@@ -8,34 +8,39 @@ import {
} from "./config.js";
export function createMatchSetup(names, requestedTeamSize = DEFAULT_TEAM_SIZE) {
const shuffledNames = shuffle([...names]);
const teamSize = resolveTeamSize(shuffledNames.length, requestedTeamSize);
const teams = createTeams(shuffledNames.length, teamSize);
const spawns = createRandomSpawnPoints(shuffledNames.length);
const teamSize = Math.max(1, Math.round(Number(requestedTeamSize) || DEFAULT_TEAM_SIZE));
const teams = names.map((name, index) => ({
color: TEAM_COLORS[index % TEAM_COLORS.length],
id: `team-${index + 1}`,
label: name,
size: teamSize,
}));
const totalFighters = names.length * teamSize;
const spawns = createRandomSpawnPoints(totalFighters);
const fighters = [];
names.forEach((name, teamIndex) => {
for (let i = 0; i < teamSize; i++) {
const globalIndex = teamIndex * teamSize + i;
fighters.push({
...spawns[globalIndex],
name: name,
team: teams[teamIndex],
teamIndex: i,
});
}
});
return {
fighters: shuffledNames.map((name, index) => {
const teamSlot = Math.floor(index / teamSize);
return {
...spawns[index],
name,
team: teams[teamSlot],
teamIndex: index - teamSlot * teamSize,
};
}),
fighters,
teams,
};
}
export function matchStatusText(teams) {
if (teams.length > 8) {
const playerCount = teams.reduce((count, team) => count + team.size, 0);
return `${teams.length}팀 전투: 참가자 ${playerCount}`;
}
return `${teams.length}팀 전투: ${teams.map((team) => team.size).join(" vs ")}`;
const teamSizes = teams.map((team) => team.size).join(", ");
return `${teams.length}팀 전투: ${teamSizes}`;
}
function createTeams(playerCount, teamSize) {
+63 -2
View File
@@ -154,13 +154,74 @@ button:hover {
.arena-shell {
position: relative;
display: grid;
place-items: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
min-height: 100vh;
overflow: hidden;
padding: clamp(16px, 3vw, 36px);
}
.scoreboard {
width: 100%;
display: flex;
justify-content: space-between;
padding: 0.5rem 1rem;
background: rgba(0, 0, 0, 0.5);
border-radius: 8px;
min-height: 40px;
pointer-events: none;
z-index: 10;
}
.score-side {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
width: 48%;
}
.score-side.right {
justify-content: flex-end;
}
.team-score {
padding: 4px 10px;
border-radius: 4px;
font-size: 0.85rem;
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 2px #000;
display: flex;
gap: 6px;
background: rgba(255, 255, 255, 0.1);
}
.victory-banner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.85);
padding: 1.5rem 3rem;
border: 2px solid #d6a94a;
border-radius: 12px;
color: #fff7df;
font-size: 2rem;
font-weight: 900;
text-align: center;
z-index: 100;
box-shadow: 0 0 30px rgba(214, 169, 74, 0.4);
backdrop-filter: blur(4px);
animation: banner-in 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes banner-in {
from { transform: translate(-50%, -60%) scale(0.8); opacity: 0; }
to { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
#game {
width: min(100%, calc(100vh - 72px), 1080px);
aspect-ratio: 1;
+5 -3
View File
@@ -3,7 +3,7 @@ const nicknameLength = 18;
export function createMatchForm() {
const form = getElement("#fighter-form");
const namesInput = getElement("#player-names");
const statusNode = getElement("#match-status");
const statusNode = document.querySelector("#match-status");
const teamSizeInput = getElement("#team-size");
const teamSizeOutput = getElement("#team-size-value");
@@ -26,7 +26,9 @@ export function createMatchForm() {
},
readMatchConfig,
setStatus(message) {
statusNode.textContent = message;
if (statusNode) {
statusNode.textContent = message;
}
},
};
}
@@ -49,5 +51,5 @@ function nicknameValues(value) {
}
function syncTeamSizeOutput(input, output) {
output.textContent = `${input.value} vs ${input.value}`;
output.textContent = input.value;
}