diff --git a/index.html b/index.html
index 2e12556..4106757 100644
--- a/index.html
+++ b/index.html
@@ -31,7 +31,7 @@ Player 10
-
+
@@ -39,8 +39,11 @@ Player 10
diff --git a/src/game/ArenaScene.js b/src/game/ArenaScene.js
index 42523df..8e93767 100644
--- a/src/game/ArenaScene.js
+++ b/src/game/ArenaScene.js
@@ -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 = `${team.label} ${aliveCount}`;
+
+ 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("무승부!");
+ }
}
}
diff --git a/src/game/combat.js b/src/game/combat.js
index ddc2258..0552b9f 100644
--- a/src/game/combat.js
+++ b/src/game/combat.js
@@ -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) {
diff --git a/src/game/config.js b/src/game/config.js
index b1c926d..c810ab7 100644
--- a/src/game/config.js
+++ b/src/game/config.js
@@ -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;
diff --git a/src/game/matchSetup.js b/src/game/matchSetup.js
index e92c973..2d5d9e5 100644
--- a/src/game/matchSetup.js
+++ b/src/game/matchSetup.js
@@ -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) {
diff --git a/src/styles.css b/src/styles.css
index 3deee51..a38b389 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -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;
diff --git a/src/ui/matchForm.js b/src/ui/matchForm.js
index f1c7452..7ca7cad 100644
--- a/src/ui/matchForm.js
+++ b/src/ui/matchForm.js
@@ -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;
}
diff --git a/todo.md b/todo.md
index e69de29..369a1c6 100644
--- a/todo.md
+++ b/todo.md
@@ -0,0 +1,15 @@
+1. 투사체 피격 판정이 너무 좋지않음
+- **원인 분석**:
+ - `src/game/combat.js`의 `PROJECTILE_HIT_RADIUS`가 8픽셀로 설정되어 있어 투사체의 물리적 크기에 비해 판정 범위가 좁습니다.
+ - 투사체가 생성될 때 공격자와 대상 사이의 거리가 매우 가까우면 투사체가 대상을 지나쳐버리는 현상이 발생할 수 있습니다.
+ - 현재 `projectilePathHitsDefender` 함수에서 투사체의 궤적(Line)과 대상의 히트박스(Rectangle) 충돌을 검사하고 있지만, 대상의 실제 충돌 영역(`defender.body`)의 위치와 크기가 애니메이션 프레임에 따라 미세하게 변하면서 판정이 어긋날 수 있습니다.
+
+2. 내가 이야기한 참가자 닉네임을 입력하고 팀당인원을 입력하면 참가자 닉네임 별 캐릭터가 스폰돼야해 (완료)
+- **조치 사항**:
+ - `src/game/matchSetup.js`를 수정하여 입력된 각 닉네임을 독립된 팀으로 설정.
+ - 설정된 `teamSize`만큼 각 닉네임의 캐릭터가 소환되도록 로직 변경.
+
+3. 승리판정이 이상함 (완료)
+- **조치 사항**:
+ - `src/game/ArenaScene.js`의 `finishMatch` 로직을 개선하여 생존 팀이 1개일 때 해당 닉네임 승리 표시.
+ - 생존자가 없을 경우 "무승부!"가 표시되도록 예외 처리 추가.