Add visitor tracking and combat selection features
This commit is contained in:
+77
-8
@@ -9,6 +9,7 @@ import {
|
||||
MINIMAP_VIEWPORT_SIZE,
|
||||
MINIMAP_VIEW_FRAME_OUTLINE,
|
||||
MINIMAP_VIEW_FRAME_STROKE,
|
||||
SELECTED_FIGHTER_CAMERA_ZOOM,
|
||||
SPECTATOR_CAMERA_LERP,
|
||||
SPECTATOR_FINAL_FIGHTER_THRESHOLD,
|
||||
SPECTATOR_FINAL_FIGHT_ZOOM,
|
||||
@@ -45,6 +46,7 @@ export class ArenaScene extends Phaser.Scene {
|
||||
}
|
||||
};
|
||||
this.observedCombat = [];
|
||||
this.selectedFighter = null;
|
||||
this.teams = [];
|
||||
}
|
||||
|
||||
@@ -70,7 +72,6 @@ export class ArenaScene extends Phaser.Scene {
|
||||
this.cameras.main.ignore(this.minimapViewportFrame);
|
||||
this.updateMinimapViewportFrame();
|
||||
this.minimapCamera.setAlpha(0); // 기본적으로는 숨김
|
||||
|
||||
// 마우스 휠로 줌 조절
|
||||
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
|
||||
const newZoom = Phaser.Math.Clamp(
|
||||
@@ -83,6 +84,19 @@ export class ArenaScene extends Phaser.Scene {
|
||||
|
||||
// 확대 시 미니맵 표시
|
||||
});
|
||||
this.input.on("gameobjectdown", (pointer, gameObject) => {
|
||||
if (this.fighters.includes(gameObject)) {
|
||||
this.selectFighter(gameObject);
|
||||
}
|
||||
});
|
||||
this.input.on("pointerdown", (pointer, gameObjects = []) => {
|
||||
if (!gameObjects.some((gameObject) => this.fighters.includes(gameObject))) {
|
||||
this.clearSelectedFighter();
|
||||
}
|
||||
});
|
||||
this.input.keyboard?.on("keydown-ESC", () => {
|
||||
this.clearSelectedFighter();
|
||||
});
|
||||
|
||||
this.ready = true;
|
||||
this.startMatch(this.getInitialMatchConfig());
|
||||
@@ -104,6 +118,7 @@ export class ArenaScene extends Phaser.Scene {
|
||||
this.matchId += 1;
|
||||
this.matchOver = false;
|
||||
this.observedCombat = [];
|
||||
this.clearSelectedFighter();
|
||||
this.setMainCameraZoom(CAMERA_MIN_ZOOM);
|
||||
this.cameras.main.centerOn(ARENA_SIZE / 2, ARENA_SIZE / 2);
|
||||
clearCombatObjects(this);
|
||||
@@ -122,6 +137,20 @@ export class ArenaScene extends Phaser.Scene {
|
||||
update(time) {
|
||||
this.fighters.forEach(syncFighterHud);
|
||||
|
||||
if (!this.matchOver) {
|
||||
this.fighters.forEach((fighter) => {
|
||||
updateFighter(this, fighter, time, () => {
|
||||
this.updateScoreboard();
|
||||
this.finishMatch();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.focusSelectedFighter()) {
|
||||
this.updateMinimapViewportFrame();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.matchOver) {
|
||||
this.updateMinimapViewportFrame();
|
||||
return;
|
||||
@@ -150,16 +179,56 @@ update(time) {
|
||||
this.cameras.main.centerOn(ARENA_SIZE / 2, ARENA_SIZE / 2);
|
||||
}
|
||||
|
||||
this.fighters.forEach((fighter) => {
|
||||
updateFighter(this, fighter, time, () => {
|
||||
this.updateScoreboard();
|
||||
this.finishMatch();
|
||||
});
|
||||
});
|
||||
|
||||
this.updateMinimapViewportFrame();
|
||||
}
|
||||
|
||||
selectFighter(fighter) {
|
||||
if (!isLivingFighter(fighter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedFighter === fighter) {
|
||||
this.clearSelectedFighter();
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearSelectedFighter();
|
||||
this.selectedFighter = fighter;
|
||||
fighter.isSelected = true;
|
||||
this.observedCombat = [];
|
||||
this.setMainCameraZoom(Math.max(this.cameras.main.zoom, SELECTED_FIGHTER_CAMERA_ZOOM));
|
||||
this.centerCameraOnFighter(fighter);
|
||||
syncFighterHud(fighter);
|
||||
}
|
||||
|
||||
clearSelectedFighter() {
|
||||
if (this.selectedFighter) {
|
||||
this.selectedFighter.isSelected = false;
|
||||
syncFighterHud(this.selectedFighter);
|
||||
}
|
||||
|
||||
this.selectedFighter = null;
|
||||
}
|
||||
|
||||
focusSelectedFighter() {
|
||||
if (!this.selectedFighter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLivingFighter(this.selectedFighter)) {
|
||||
this.clearSelectedFighter();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.centerCameraOnFighter(this.selectedFighter);
|
||||
return true;
|
||||
}
|
||||
|
||||
centerCameraOnFighter(fighter) {
|
||||
const target = fighter.body?.center ?? fighter;
|
||||
this.cameras.main.centerOn(Math.round(target.x), Math.round(target.y));
|
||||
}
|
||||
|
||||
setMainCameraZoom(zoom) {
|
||||
const newZoom = Phaser.Math.Clamp(zoom, CAMERA_MIN_ZOOM, CAMERA_MAX_ZOOM);
|
||||
|
||||
|
||||
+60
-12
@@ -1,8 +1,14 @@
|
||||
import Phaser from "phaser";
|
||||
import {
|
||||
ATTACK_COOLDOWN,
|
||||
ATTACK_DAMAGE_MAX,
|
||||
ATTACK_DAMAGE_MIN,
|
||||
ATTACK_RANGE,
|
||||
FIGHTER_MAX_HP,
|
||||
FIGHTER_SCALE,
|
||||
KILL_HEALTH_RECOVERY_RATIO,
|
||||
KILL_GROWTH_MULTIPLIER,
|
||||
KILL_GROWTH_TWEEN_DURATION,
|
||||
MELEE_HIT_DELAY,
|
||||
MELEE_CRITICAL_CHANCE,
|
||||
MOVE_SPEED,
|
||||
@@ -41,7 +47,7 @@ export function updateFighter(scene, fighter, time, onWinner) {
|
||||
fighter.setFlipX(enemy.x < fighter.x);
|
||||
|
||||
if (distance > getAttackRange(fighter)) {
|
||||
scene.physics.moveToObject(fighter, enemy, MOVE_SPEED * getMovementSpeedMultiplier());
|
||||
scene.physics.moveToObject(fighter, enemy, MOVE_SPEED * fighterMovementSpeedMultiplier(fighter));
|
||||
playIfNeeded(fighter, "walk");
|
||||
return;
|
||||
}
|
||||
@@ -66,10 +72,11 @@ export function clearCombatObjects(scene) {
|
||||
|
||||
function beginAttack(scene, attacker, defender, time, onWinner) {
|
||||
const attack = createAttackProfile(attacker);
|
||||
attacker.nextAttackAt = time + scaledAttackDelay(attacker.skin.combat?.cooldown ?? ATTACK_COOLDOWN);
|
||||
attacker.nextAttackAt =
|
||||
time + scaledAttackDelay(attacker.skin.combat?.cooldown ?? ATTACK_COOLDOWN, attacker);
|
||||
attacker.isLocked = true;
|
||||
scene.observeCombat?.(attacker, defender);
|
||||
playAnimation(attacker, attack.animation, getAttackSpeedMultiplier());
|
||||
playAnimation(attacker, attack.animation, fighterAttackSpeedMultiplier(attacker));
|
||||
|
||||
switch (getCombatType(attacker)) {
|
||||
case "projectile":
|
||||
@@ -86,7 +93,7 @@ function beginAttack(scene, attacker, defender, time, onWinner) {
|
||||
function queueMeleeHit(scene, attacker, defender, onWinner, attack) {
|
||||
const matchId = scene.matchId;
|
||||
|
||||
scene.time.delayedCall(scaledAttackDelay(MELEE_HIT_DELAY), () => {
|
||||
scene.time.delayedCall(scaledAttackDelay(MELEE_HIT_DELAY, attacker), () => {
|
||||
applyHit(scene, attacker, defender, onWinner, matchId, {
|
||||
instantKill: attack.isCritical,
|
||||
});
|
||||
@@ -96,7 +103,7 @@ function queueMeleeHit(scene, attacker, defender, onWinner, attack) {
|
||||
function queueProjectile(scene, attacker, defender, onWinner) {
|
||||
const matchId = scene.matchId;
|
||||
|
||||
scene.time.delayedCall(scaledAttackDelay(PROJECTILE_FIRE_DELAY), () => {
|
||||
scene.time.delayedCall(scaledAttackDelay(PROJECTILE_FIRE_DELAY, attacker), () => {
|
||||
if (!isAttackValid(scene, attacker, defender, matchId)) {
|
||||
return;
|
||||
}
|
||||
@@ -108,7 +115,7 @@ function queueProjectile(scene, attacker, defender, onWinner) {
|
||||
function queueInstantSpell(scene, attacker, defender, onWinner) {
|
||||
const matchId = scene.matchId;
|
||||
|
||||
scene.time.delayedCall(scaledAttackDelay(SPELL_CAST_DELAY), () => {
|
||||
scene.time.delayedCall(scaledAttackDelay(SPELL_CAST_DELAY, attacker), () => {
|
||||
if (!isAttackValid(scene, attacker, defender, matchId)) {
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +151,8 @@ function spawnProjectile(scene, attacker, defender, onWinner, matchId) {
|
||||
projectile,
|
||||
defenderHitPoint.x,
|
||||
defenderHitPoint.y,
|
||||
(attacker.skin.combat?.projectile?.speed ?? PROJECTILE_SPEED) * getAttackSpeedMultiplier(),
|
||||
(attacker.skin.combat?.projectile?.speed ?? PROJECTILE_SPEED) *
|
||||
fighterAttackSpeedMultiplier(attacker),
|
||||
);
|
||||
trackCombatObject(scene, projectile);
|
||||
|
||||
@@ -209,9 +217,12 @@ function spawnSpellEffect(scene, attacker, defender, onWinner, matchId) {
|
||||
disposeCombatObject(scene, effect);
|
||||
});
|
||||
|
||||
scene.time.delayedCall(scaledAttackDelay(attacker.skin.combat?.attackEffect?.hitDelay ?? SPELL_HIT_DELAY), () => {
|
||||
scene.time.delayedCall(
|
||||
scaledAttackDelay(attacker.skin.combat?.attackEffect?.hitDelay ?? SPELL_HIT_DELAY, attacker),
|
||||
() => {
|
||||
applyHit(scene, attacker, defender, onWinner, matchId);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function applyHit(scene, attacker, defender, onWinner, matchId, { instantKill = false } = {}) {
|
||||
@@ -219,7 +230,9 @@ function applyHit(scene, attacker, defender, onWinner, matchId, { instantKill =
|
||||
return;
|
||||
}
|
||||
|
||||
defender.hp = instantKill ? 0 : Math.max(0, defender.hp - Phaser.Math.Between(14, 24));
|
||||
defender.hp = instantKill
|
||||
? 0
|
||||
: Math.max(0, defender.hp - Phaser.Math.Between(ATTACK_DAMAGE_MIN, ATTACK_DAMAGE_MAX));
|
||||
defender.body.setVelocity(0, 0);
|
||||
|
||||
if (defender.hp === 0) {
|
||||
@@ -336,9 +349,36 @@ function killFighter(defender, winner, onWinner) {
|
||||
winner.isLocked = false;
|
||||
winner.body.setVelocity(0, 0);
|
||||
playAnimation(winner, "idle");
|
||||
applyKillReward(winner);
|
||||
onWinner(winner);
|
||||
}
|
||||
|
||||
function applyKillReward(winner) {
|
||||
winner.killCount = (winner.killCount ?? 0) + 1;
|
||||
|
||||
const rewardMultiplier = KILL_GROWTH_MULTIPLIER ** winner.killCount;
|
||||
winner.killRewardMultiplier = rewardMultiplier;
|
||||
winner.hp = recoveredHealth(winner);
|
||||
|
||||
const nextScaleX = (winner.baseScaleX ?? FIGHTER_SCALE) * rewardMultiplier;
|
||||
const nextScaleY = (winner.baseScaleY ?? FIGHTER_SCALE) * rewardMultiplier;
|
||||
|
||||
winner.scene.tweens.add({
|
||||
targets: winner,
|
||||
scaleX: nextScaleX,
|
||||
scaleY: nextScaleY,
|
||||
duration: KILL_GROWTH_TWEEN_DURATION,
|
||||
ease: "Back.Out",
|
||||
});
|
||||
}
|
||||
|
||||
function recoveredHealth(fighter) {
|
||||
const maxHp = fighter.maxHp ?? FIGHTER_MAX_HP;
|
||||
const recovery = Math.ceil(fighter.hp * KILL_HEALTH_RECOVERY_RATIO);
|
||||
|
||||
return Math.min(maxHp, fighter.hp + recovery);
|
||||
}
|
||||
|
||||
function findNearestEnemy(fighters, fighter) {
|
||||
let nearestEnemy;
|
||||
let nearestDistance = Number.POSITIVE_INFINITY;
|
||||
@@ -381,8 +421,16 @@ function playAnimation(fighter, action, timeScale = 1) {
|
||||
fighter.play(fighterAnimationKey(fighter.skin, action), true);
|
||||
}
|
||||
|
||||
function scaledAttackDelay(duration) {
|
||||
return duration / getAttackSpeedMultiplier();
|
||||
function scaledAttackDelay(duration, fighter) {
|
||||
return duration / fighterAttackSpeedMultiplier(fighter);
|
||||
}
|
||||
|
||||
function fighterAttackSpeedMultiplier(fighter) {
|
||||
return getAttackSpeedMultiplier() * (fighter.killRewardMultiplier ?? 1);
|
||||
}
|
||||
|
||||
function fighterMovementSpeedMultiplier(fighter) {
|
||||
return getMovementSpeedMultiplier() * (fighter.killRewardMultiplier ?? 1);
|
||||
}
|
||||
|
||||
function trackCombatObject(scene, object) {
|
||||
|
||||
+147
-16
@@ -1,4 +1,16 @@
|
||||
import { FIGHTER_ANIMATION_OPTIONS } from "../constants.js";
|
||||
import {
|
||||
FIGHTER_ANIMATION_OPTIONS,
|
||||
FIGHTER_FRAME_HEIGHT,
|
||||
FIGHTER_FRAME_WIDTH,
|
||||
SELECTED_FIGHTER_OUTLINE_ALPHA,
|
||||
SELECTED_FIGHTER_OUTLINE_BLUE,
|
||||
SELECTED_FIGHTER_OUTLINE_GAP,
|
||||
SELECTED_FIGHTER_OUTLINE_GREEN,
|
||||
SELECTED_FIGHTER_OUTLINE_RED,
|
||||
SELECTED_FIGHTER_OUTLINE_WIDTH,
|
||||
} from "../constants.js";
|
||||
|
||||
const SOURCE_ALPHA_THRESHOLD = 8;
|
||||
|
||||
export function fighterSheetKey(skin, action) {
|
||||
return `${skin.key}-${action}`;
|
||||
@@ -8,6 +20,14 @@ export function fighterAnimationKey(skin, action) {
|
||||
return `${fighterSheetKey(skin, action)}-anim`;
|
||||
}
|
||||
|
||||
export function fighterOutlineSheetKey(skin, action) {
|
||||
return `${fighterSheetKey(skin, action)}-outline`;
|
||||
}
|
||||
|
||||
export function fighterOutlineSheetKeyFromSheetKey(sheetKey) {
|
||||
return `${sheetKey}-outline`;
|
||||
}
|
||||
|
||||
export function fighterAttackEffectKey(skin) {
|
||||
return `${skin.key}-attack-effect`;
|
||||
}
|
||||
@@ -26,7 +46,7 @@ export function preloadFighterSheets(scene, skins) {
|
||||
scene.load.spritesheet(
|
||||
fighterSheetKey(skin, action),
|
||||
`${skin.assetRoot}/${animation.file}`,
|
||||
{ frameWidth: 100, frameHeight: 100 },
|
||||
{ frameWidth: FIGHTER_FRAME_WIDTH, frameHeight: FIGHTER_FRAME_HEIGHT },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,21 +59,21 @@ export function createFighterAnimations(scene, skins) {
|
||||
Object.entries(skin.animations).forEach(([action, animation]) => {
|
||||
const key = fighterAnimationKey(skin, action);
|
||||
|
||||
if (scene.anims.exists(key)) {
|
||||
return;
|
||||
if (!scene.anims.exists(key)) {
|
||||
const { frameRate, repeat } = FIGHTER_ANIMATION_OPTIONS[action];
|
||||
|
||||
scene.anims.create({
|
||||
key,
|
||||
frames: scene.anims.generateFrameNumbers(fighterSheetKey(skin, action), {
|
||||
start: 0,
|
||||
end: animation.frames - 1,
|
||||
}),
|
||||
frameRate,
|
||||
repeat,
|
||||
});
|
||||
}
|
||||
|
||||
const { frameRate, repeat } = FIGHTER_ANIMATION_OPTIONS[action];
|
||||
|
||||
scene.anims.create({
|
||||
key,
|
||||
frames: scene.anims.generateFrameNumbers(fighterSheetKey(skin, action), {
|
||||
start: 0,
|
||||
end: animation.frames - 1,
|
||||
}),
|
||||
frameRate,
|
||||
repeat,
|
||||
});
|
||||
createFighterOutlineSheet(scene, skin, action, animation.frames);
|
||||
});
|
||||
|
||||
createAttackEffectAnimation(scene, skin);
|
||||
@@ -72,7 +92,7 @@ function preloadCombatAssets(scene, skin) {
|
||||
scene.load.spritesheet(
|
||||
fighterAttackEffectKey(skin),
|
||||
`${skin.assetRoot}/${attackEffect.file}`,
|
||||
{ frameWidth: 100, frameHeight: 100 },
|
||||
{ frameWidth: FIGHTER_FRAME_WIDTH, frameHeight: FIGHTER_FRAME_HEIGHT },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -100,3 +120,114 @@ function createAttackEffectAnimation(scene, skin) {
|
||||
repeat: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function createFighterOutlineSheet(scene, skin, action, frameCount) {
|
||||
const key = fighterOutlineSheetKey(skin, action);
|
||||
|
||||
if (scene.textures.exists(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceTexture = scene.textures.get(fighterSheetKey(skin, action));
|
||||
const sourceImage = sourceTexture?.getSourceImage?.();
|
||||
|
||||
if (!sourceImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sheetWidth = FIGHTER_FRAME_WIDTH * frameCount;
|
||||
const sheetHeight = FIGHTER_FRAME_HEIGHT;
|
||||
const sourceCanvas = document.createElement("canvas");
|
||||
sourceCanvas.width = sheetWidth;
|
||||
sourceCanvas.height = sheetHeight;
|
||||
|
||||
const sourceContext = sourceCanvas.getContext("2d", { willReadFrequently: true });
|
||||
sourceContext.drawImage(sourceImage, 0, 0);
|
||||
|
||||
const sourceData = sourceContext.getImageData(0, 0, sheetWidth, sheetHeight).data;
|
||||
const outlineCanvas = document.createElement("canvas");
|
||||
outlineCanvas.width = sheetWidth;
|
||||
outlineCanvas.height = sheetHeight;
|
||||
|
||||
const outlineContext = outlineCanvas.getContext("2d");
|
||||
const outlineImage = outlineContext.createImageData(sheetWidth, sheetHeight);
|
||||
const outlineData = outlineImage.data;
|
||||
const gapMask = new Uint8Array(sheetWidth * sheetHeight);
|
||||
const outerMask = new Uint8Array(sheetWidth * sheetHeight);
|
||||
const outlineAlpha = Math.round(SELECTED_FIGHTER_OUTLINE_ALPHA * 255);
|
||||
|
||||
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
|
||||
const frameLeft = frameIndex * FIGHTER_FRAME_WIDTH;
|
||||
|
||||
for (let y = 0; y < FIGHTER_FRAME_HEIGHT; y += 1) {
|
||||
for (let x = 0; x < FIGHTER_FRAME_WIDTH; x += 1) {
|
||||
const sourceIndex = ((y * sheetWidth) + frameLeft + x) * 4;
|
||||
|
||||
if (sourceData[sourceIndex + 3] <= SOURCE_ALPHA_THRESHOLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
markOutlineMasks(gapMask, outerMask, sheetWidth, frameLeft, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paintOutlinePixels(outlineData, gapMask, outerMask, outlineAlpha);
|
||||
outlineContext.putImageData(outlineImage, 0, 0);
|
||||
scene.textures.addSpriteSheet(key, outlineCanvas, {
|
||||
frameWidth: FIGHTER_FRAME_WIDTH,
|
||||
frameHeight: FIGHTER_FRAME_HEIGHT,
|
||||
});
|
||||
}
|
||||
|
||||
function markOutlineMasks(gapMask, outerMask, sheetWidth, frameLeft, sourceX, sourceY) {
|
||||
const outerRadius = SELECTED_FIGHTER_OUTLINE_GAP + SELECTED_FIGHTER_OUTLINE_WIDTH;
|
||||
|
||||
for (
|
||||
let offsetY = -outerRadius;
|
||||
offsetY <= outerRadius;
|
||||
offsetY += 1
|
||||
) {
|
||||
const targetY = sourceY + offsetY;
|
||||
|
||||
if (targetY < 0 || targetY >= FIGHTER_FRAME_HEIGHT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (
|
||||
let offsetX = -outerRadius;
|
||||
offsetX <= outerRadius;
|
||||
offsetX += 1
|
||||
) {
|
||||
const targetX = sourceX + offsetX;
|
||||
|
||||
if (targetX < 0 || targetX >= FIGHTER_FRAME_WIDTH) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const maskIndex = (targetY * sheetWidth) + frameLeft + targetX;
|
||||
const distance = Math.max(Math.abs(offsetX), Math.abs(offsetY));
|
||||
|
||||
outerMask[maskIndex] = 1;
|
||||
|
||||
if (distance <= SELECTED_FIGHTER_OUTLINE_GAP) {
|
||||
gapMask[maskIndex] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function paintOutlinePixels(outlineData, gapMask, outerMask, outlineAlpha) {
|
||||
for (let maskIndex = 0; maskIndex < outerMask.length; maskIndex += 1) {
|
||||
if (!outerMask[maskIndex] || gapMask[maskIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const outlineIndex = maskIndex * 4;
|
||||
|
||||
outlineData[outlineIndex] = SELECTED_FIGHTER_OUTLINE_RED;
|
||||
outlineData[outlineIndex + 1] = SELECTED_FIGHTER_OUTLINE_GREEN;
|
||||
outlineData[outlineIndex + 2] = SELECTED_FIGHTER_OUTLINE_BLUE;
|
||||
outlineData[outlineIndex + 3] = outlineAlpha;
|
||||
}
|
||||
}
|
||||
|
||||
+82
-11
@@ -1,6 +1,21 @@
|
||||
import Phaser from "phaser";
|
||||
import { FIGHTER_SCALE } from "../constants.js";
|
||||
import { fighterAnimationKey, fighterSheetKey } from "./fighterAssets.js";
|
||||
import {
|
||||
FIGHTER_FRAME_HEIGHT,
|
||||
FIGHTER_FRAME_WIDTH,
|
||||
FIGHTER_HITBOX_HEIGHT,
|
||||
FIGHTER_HITBOX_OFFSET_X,
|
||||
FIGHTER_HITBOX_OFFSET_Y,
|
||||
FIGHTER_HITBOX_WIDTH,
|
||||
FIGHTER_MAX_HP,
|
||||
FIGHTER_SCALE,
|
||||
} from "../constants.js";
|
||||
import {
|
||||
fighterAnimationKey,
|
||||
fighterOutlineSheetKeyFromSheetKey,
|
||||
fighterSheetKey,
|
||||
} from "./fighterAssets.js";
|
||||
|
||||
const NAME_LABEL_BOTTOM_GAP = 14;
|
||||
|
||||
export function createFighter(scene, { faceLeft, name, skin, team, teamIndex, x, y }) {
|
||||
const fighter = scene.physics.add.sprite(x, y, fighterSheetKey(skin, "idle"), 0);
|
||||
@@ -8,11 +23,26 @@ export function createFighter(scene, { faceLeft, name, skin, team, teamIndex, x,
|
||||
fighter.setDepth(2);
|
||||
fighter.setCollideWorldBounds(true);
|
||||
fighter.setFlipX(faceLeft);
|
||||
fighter.body.setSize(22, 20);
|
||||
fighter.body.setOffset(39, 60);
|
||||
fighter.body.setSize(FIGHTER_HITBOX_WIDTH, FIGHTER_HITBOX_HEIGHT);
|
||||
fighter.body.setOffset(FIGHTER_HITBOX_OFFSET_X, FIGHTER_HITBOX_OFFSET_Y);
|
||||
fighter.setInteractive(
|
||||
new Phaser.Geom.Rectangle(
|
||||
FIGHTER_HITBOX_OFFSET_X,
|
||||
FIGHTER_HITBOX_OFFSET_Y,
|
||||
FIGHTER_HITBOX_WIDTH,
|
||||
FIGHTER_HITBOX_HEIGHT,
|
||||
),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
fighter.input.cursor = "pointer";
|
||||
fighter.selectionOutline = scene.add
|
||||
.sprite(x, y, fighterOutlineSheetKeyFromSheetKey(fighterSheetKey(skin, "idle")), 0)
|
||||
.setDisplaySize(FIGHTER_FRAME_WIDTH * FIGHTER_SCALE, FIGHTER_FRAME_HEIGHT * FIGHTER_SCALE)
|
||||
.setDepth(1.9)
|
||||
.setVisible(false);
|
||||
|
||||
fighter.nameLabel = scene.add
|
||||
.text(x, y - 68, name, {
|
||||
.text(x, y, name, {
|
||||
color: "#fff2c2",
|
||||
fontFamily: "Inter, Pretendard, sans-serif",
|
||||
fontSize: "18px",
|
||||
@@ -20,7 +50,7 @@ export function createFighter(scene, { faceLeft, name, skin, team, teamIndex, x,
|
||||
stroke: team.color,
|
||||
strokeThickness: 4,
|
||||
})
|
||||
.setOrigin(0.5)
|
||||
.setOrigin(0.5, 0)
|
||||
.setDepth(4);
|
||||
fighter.healthBack = scene.add
|
||||
.rectangle(x, y - 44, 72, 8, 0x17180e, 0.92)
|
||||
@@ -33,7 +63,13 @@ export function createFighter(scene, { faceLeft, name, skin, team, teamIndex, x,
|
||||
fighter.skin = skin;
|
||||
fighter.team = team;
|
||||
fighter.teamIndex = teamIndex;
|
||||
fighter.hp = 100;
|
||||
fighter.baseScaleX = FIGHTER_SCALE;
|
||||
fighter.baseScaleY = FIGHTER_SCALE;
|
||||
fighter.isSelected = false;
|
||||
fighter.killCount = 0;
|
||||
fighter.killRewardMultiplier = 1;
|
||||
fighter.maxHp = FIGHTER_MAX_HP;
|
||||
fighter.hp = fighter.maxHp;
|
||||
fighter.nextAttackAt = 0;
|
||||
fighter.isLocked = false;
|
||||
fighter.isDead = false;
|
||||
@@ -50,21 +86,56 @@ export function createFighter(scene, { faceLeft, name, skin, team, teamIndex, x,
|
||||
});
|
||||
|
||||
attachHudCleanup(fighter);
|
||||
syncFighterHud(fighter);
|
||||
|
||||
return fighter;
|
||||
}
|
||||
|
||||
export function syncFighterHud(fighter) {
|
||||
fighter.nameLabel.setPosition(fighter.x, fighter.y - 68);
|
||||
fighter.healthBack.setPosition(fighter.x, fighter.y - 44);
|
||||
fighter.healthBar.setPosition(fighter.x - 34, fighter.y - 44);
|
||||
fighter.healthBar.width = Math.max(0, 68 * (fighter.hp / 100));
|
||||
const scaleRatio = Math.max(1, Math.abs(fighter.scaleY) / FIGHTER_SCALE);
|
||||
const healthOffset = 44 * scaleRatio;
|
||||
const hitbox = fighter.body;
|
||||
const nameX = hitbox.x + hitbox.width / 2;
|
||||
const nameY = hitbox.y + hitbox.height + NAME_LABEL_BOTTOM_GAP;
|
||||
|
||||
fighter.nameLabel.setPosition(nameX, nameY);
|
||||
fighter.healthBack.setPosition(fighter.x, fighter.y - healthOffset);
|
||||
fighter.healthBar.setPosition(fighter.x - 34, fighter.y - healthOffset);
|
||||
fighter.healthBar.width = Math.max(0, 68 * (fighter.hp / (fighter.maxHp ?? FIGHTER_MAX_HP)));
|
||||
syncSelectionOutline(fighter);
|
||||
}
|
||||
|
||||
function syncSelectionOutline(fighter) {
|
||||
const outline = fighter.selectionOutline;
|
||||
|
||||
if (!outline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isVisible = Boolean(fighter.isSelected && !fighter.isDead);
|
||||
outline.setVisible(isVisible);
|
||||
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outlineTextureKey = fighterOutlineSheetKeyFromSheetKey(fighter.texture.key);
|
||||
|
||||
if (fighter.scene.textures.exists(outlineTextureKey)) {
|
||||
outline.setTexture(outlineTextureKey, fighter.frame.name);
|
||||
}
|
||||
|
||||
outline.setPosition(fighter.x, fighter.y);
|
||||
outline.setScale(fighter.scaleX, fighter.scaleY);
|
||||
outline.setFlipX(fighter.flipX);
|
||||
outline.setDepth(fighter.depth - 0.1);
|
||||
}
|
||||
|
||||
function attachHudCleanup(fighter) {
|
||||
const originalDestroy = fighter.destroy.bind(fighter);
|
||||
|
||||
fighter.destroy = (...args) => {
|
||||
fighter.selectionOutline.destroy();
|
||||
fighter.nameLabel.destroy();
|
||||
fighter.healthBack.destroy();
|
||||
fighter.healthBar.destroy();
|
||||
|
||||
Reference in New Issue
Block a user