feat: implement elite kill splash and optimize team focus toggle
- Add ELITE_KILL_SPLASH mechanic where elite fighters trigger area damage on kill. - Implement pixel-dot splash visual effects for elite kills. - Toggle camera focus when clicking the same team card in the HUD. - Adjust elite fighter balancing (HP, damage exponents) and world effect intervals. - Update documentation in agent.md and context/ files.
This commit is contained in:
+17
-13
@@ -46,7 +46,7 @@ export const FIGHTER = {
|
||||
attackCooldown: 840,
|
||||
damageMin: 14,
|
||||
damageMax: 24,
|
||||
criticalChance: 0.2,
|
||||
criticalChance: 0.05,
|
||||
windupDelay: 260,
|
||||
},
|
||||
ranged: {
|
||||
@@ -77,12 +77,12 @@ export const FIGHTER = {
|
||||
STACK_SIZE: 100,
|
||||
VISUAL_SCALE_MULTIPLIER: 5,
|
||||
ATTACK_EFFECT_SCALE_MULTIPLIER: 5,
|
||||
HP_BONUS_RATIO: 1,
|
||||
HP_BONUS_RATIO: 2,
|
||||
ATTACK_RANGE_MULTIPLIER: 1.5,
|
||||
ATTACK_DAMAGE_BONUS_MULTIPLIER: 1,
|
||||
ATTACK_DAMAGE_STACK_EXPONENT: 0.1,
|
||||
ATTACK_DAMAGE_BONUS_MULTIPLIER: 1.1,
|
||||
ATTACK_DAMAGE_STACK_EXPONENT: 1,
|
||||
ATTACK_SPEED_BONUS_MULTIPLIER: 1,
|
||||
ATTACK_SPEED_STACK_EXPONENT: 0.1,
|
||||
ATTACK_SPEED_STACK_EXPONENT: 0.4,
|
||||
MOVE_SPEED_BONUS_MULTIPLIER: 1,
|
||||
MOVE_SPEED_STACK_EXPONENT: 0,
|
||||
RANDOMIZED_COMPRESSION: {
|
||||
@@ -116,8 +116,8 @@ export const SPAWN = {
|
||||
},
|
||||
// Caps participant-assigned slots; traits such as slime spawning may add fighters.
|
||||
MAX_FIGHTER_COUNT: 20000,
|
||||
FIGHTERS_PER_STARTING_ZONE: 1000,
|
||||
STARTING_ZONE_RADIUS: 2,
|
||||
FIGHTERS_PER_STARTING_ZONE: 500,
|
||||
STARTING_ZONE_RADIUS: 3,
|
||||
STARTING_ZONE_FILL_ALPHA: 0.07,
|
||||
STARTING_ZONE_BORDER_ALPHA: 0.14,
|
||||
STARTING_ZONE_VISIBLE_DURATION_MS: 2000,
|
||||
@@ -136,6 +136,10 @@ export const COMBAT = {
|
||||
KILL_GROWTH_TWEEN_DURATION: 180,
|
||||
CRITICAL_DAMAGE_PERCENT: 0.1,
|
||||
NORMAL_CRITICAL_DAMAGE_MULTIPLIER: 2,
|
||||
ELITE_KILL_SPLASH_ENABLED: true,
|
||||
ELITE_KILL_SPLASH_DAMAGE_PERCENT: 0.1,
|
||||
ELITE_KILL_SPLASH_RADIUS: TILE_SIZE * 2,
|
||||
ELITE_KILL_SPLASH_CHAIN_ENABLED: false,
|
||||
// 최종교전 슬로우모션 설정
|
||||
FINAL_SLOW_MOTION_ENABLED: false,
|
||||
FINAL_SLOW_MOTION_ENTER_DURATION: 14000,
|
||||
@@ -158,13 +162,13 @@ const WORLD_EFFECT_CONFIG = {
|
||||
// Delay from match start until the first barrage.
|
||||
INTERVAL: 8000,
|
||||
// Delay between barrages after the first one has fired.
|
||||
REPEAT_INTERVAL: 12000,
|
||||
REPEAT_INTERVAL: 20000,
|
||||
AREA_TILES: 40,
|
||||
// How long the large dense-area warning marker remains visible.
|
||||
WARNING_DURATION_MS: 2000,
|
||||
IMPACT_AREA_TILES: 10,
|
||||
IMPACT_COUNT_MIN: 10,
|
||||
IMPACT_COUNT_MAX: 15,
|
||||
IMPACT_COUNT_MIN: 5,
|
||||
IMPACT_COUNT_MAX: 10,
|
||||
IMPACT_STAGGER_MS: 140,
|
||||
IMPACT_VISUAL_SCALE: 15,
|
||||
SIZE_SCALE_VARIANCE: 1,
|
||||
@@ -194,7 +198,7 @@ export const SPECIAL_EFFECT = {
|
||||
ENABLED: true,
|
||||
// A special effect is picked once per battle, no earlier than the first world-effect delay.
|
||||
TRIGGER_DELAY_MIN_MS: 10000,
|
||||
TRIGGER_DELAY_MAX_MS: 50000,
|
||||
TRIGGER_DELAY_MAX_MS: 11000,
|
||||
RETRY_DELAY_MS: 2000,
|
||||
CASTER: {
|
||||
HURT_FRAME_INDEX: 1,
|
||||
@@ -202,8 +206,8 @@ export const SPECIAL_EFFECT = {
|
||||
ATTACK_LAUNCH_DELAY_MS: 360,
|
||||
ATTACK_TIME_SCALE: 0.9,
|
||||
POST_CAST_COOLDOWN_MS: 1200,
|
||||
BALANCE_NON_MAGIC_TYPES: true,
|
||||
INVULNERABLE_MS: 7000,
|
||||
BALANCE_NON_MAGIC_TYPES: false,
|
||||
INVULNERABLE_MS: 4500,
|
||||
},
|
||||
CAMERA: {
|
||||
ZOOM: 3,
|
||||
|
||||
@@ -1112,6 +1112,14 @@ update(time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLivingFighter(this.selectedFighter) && this.selectedFighter.team.id === teamId) {
|
||||
this.clearSelectedFighter();
|
||||
this.setMainCameraZoom(CAMERA.MIN_ZOOM);
|
||||
this.setStatus(matchStatusText(this.teams));
|
||||
this.updateScoreboard();
|
||||
return;
|
||||
}
|
||||
|
||||
const candidates = this.fighters.filter(
|
||||
(fighter) => isLivingFighter(fighter) && fighter.team.id === teamId,
|
||||
);
|
||||
|
||||
+141
-1
@@ -524,7 +524,12 @@ function projectilePathHitsDefender(projectile, defender) {
|
||||
);
|
||||
}
|
||||
|
||||
function killFighter(defender, winner, onWinner) {
|
||||
function killFighter(
|
||||
defender,
|
||||
winner,
|
||||
onWinner,
|
||||
{ triggerEliteKillSplash = true } = {},
|
||||
) {
|
||||
defender.isDead = true;
|
||||
defender.isLocked = true;
|
||||
defender.body.setVelocity(0, 0);
|
||||
@@ -543,6 +548,10 @@ function killFighter(defender, winner, onWinner) {
|
||||
if (COMBAT.KILL_REWARD_ENABLED) {
|
||||
applyKillReward(winner);
|
||||
}
|
||||
|
||||
if (triggerEliteKillSplash) {
|
||||
applyEliteKillSplash(defender, winner);
|
||||
}
|
||||
} else {
|
||||
defender.scene.recordDeath?.(defender);
|
||||
}
|
||||
@@ -552,6 +561,137 @@ function killFighter(defender, winner, onWinner) {
|
||||
scheduleDeadFighterDespawn(defender);
|
||||
}
|
||||
|
||||
function applyEliteKillSplash(defender, winner) {
|
||||
if (
|
||||
!COMBAT.ELITE_KILL_SPLASH_ENABLED
|
||||
|| !winner?.isElite
|
||||
|| !winner.active
|
||||
|| winner.isDead
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scene = winner.scene ?? defender.scene;
|
||||
const radius = Math.max(0, Number(COMBAT.ELITE_KILL_SPLASH_RADIUS) || 0);
|
||||
const damage = eliteKillSplashDamageFor(defender);
|
||||
|
||||
if (!scene || scene.matchOver || radius <= 0 || damage <= 0 || !Array.isArray(scene.fighters)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const center = fighterHitPoint(defender);
|
||||
const radiusSq = radius * radius;
|
||||
|
||||
if (shouldRenderCombatEffects(scene)) {
|
||||
spawnEliteKillSplashEffect(scene, center, radius, winner.team?.color);
|
||||
}
|
||||
|
||||
scene.fighters.forEach((fighter) => {
|
||||
if (
|
||||
scene.matchOver
|
||||
|| fighter === defender
|
||||
|| fighter === winner
|
||||
|| !fighter?.active
|
||||
|| fighter.isDead
|
||||
|| fighter.team?.id === winner.team?.id
|
||||
|| isFighterSpecialInvulnerable(fighter)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = fighterHitPoint(fighter);
|
||||
const deltaX = target.x - center.x;
|
||||
const deltaY = target.y - center.y;
|
||||
|
||||
if (deltaX * deltaX + deltaY * deltaY > radiusSq) {
|
||||
return;
|
||||
}
|
||||
|
||||
fighter.hp = Math.max(0, fighter.hp - damage);
|
||||
fighter.body?.setVelocity(0, 0);
|
||||
|
||||
if (fighter.hp === 0) {
|
||||
killFighter(fighter, winner, undefined, {
|
||||
triggerEliteKillSplash: Boolean(COMBAT.ELITE_KILL_SPLASH_CHAIN_ENABLED),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
fighter.isLocked = true;
|
||||
playAnimation(fighter, "hurt");
|
||||
});
|
||||
}
|
||||
|
||||
function eliteKillSplashDamageFor(defender) {
|
||||
const percentage = Math.max(0, Number(COMBAT.ELITE_KILL_SPLASH_DAMAGE_PERCENT) || 0);
|
||||
const maxHp = Math.max(1, Number(defender.maxHp ?? combatStatsFor(defender).maxHp) || 1);
|
||||
|
||||
return Math.ceil(maxHp * percentage);
|
||||
}
|
||||
|
||||
function spawnEliteKillSplashEffect(scene, center, radius, color) {
|
||||
const parsedColor = Number.parseInt(String(color ?? "#f6d365").replace(/^#/, ""), 16);
|
||||
const effectColor = Number.isFinite(parsedColor) ? parsedColor : 0xf6d365;
|
||||
const splash = scene.add
|
||||
.graphics()
|
||||
.setPosition(Math.round(center.x), Math.round(center.y))
|
||||
.setDepth(5);
|
||||
|
||||
drawEliteKillSplashPixels(splash, radius, effectColor);
|
||||
splash.cleanup = () => {
|
||||
scene.tweens.killTweensOf(splash);
|
||||
};
|
||||
|
||||
trackCombatObject(scene, splash);
|
||||
scene.tweens.add({
|
||||
targets: splash,
|
||||
alpha: 0,
|
||||
duration: 260,
|
||||
ease: "Cubic.Out",
|
||||
onComplete: () => disposeCombatObject(scene, splash),
|
||||
});
|
||||
}
|
||||
|
||||
function drawEliteKillSplashPixels(graphics, radius, color) {
|
||||
const dotSize = 8;
|
||||
const step = 12;
|
||||
const halfDot = dotSize / 2;
|
||||
const radiusSq = radius * radius;
|
||||
const edgeThickness = Math.max(step * 2, radius * 0.18);
|
||||
const innerRadius = Math.max(0, radius - edgeThickness);
|
||||
const innerRadiusSq = innerRadius * innerRadius;
|
||||
const coreRadiusSq = (radius * 0.42) ** 2;
|
||||
|
||||
for (let y = -radius; y <= radius; y += step) {
|
||||
for (let x = -radius; x <= radius; x += step) {
|
||||
const distanceSq = x * x + y * y;
|
||||
|
||||
if (distanceSq > radiusSq) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const gridX = Math.round((x + radius) / step);
|
||||
const gridY = Math.round((y + radius) / step);
|
||||
const isEdgeDot = distanceSq >= innerRadiusSq;
|
||||
const isInteriorDot =
|
||||
distanceSq > coreRadiusSq &&
|
||||
((gridX * 3 + gridY * 5) % 7 === 0 || (gridX + gridY) % 11 === 0);
|
||||
|
||||
if (!isEdgeDot && !isInteriorDot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
graphics.fillStyle(color, isEdgeDot ? 0.72 : 0.28);
|
||||
graphics.fillRect(
|
||||
Math.round(x - halfDot),
|
||||
Math.round(y - halfDot),
|
||||
dotSize,
|
||||
dotSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleDeadFighterDespawn(fighter) {
|
||||
const scene = fighter.scene;
|
||||
const delay = resolveDeadDespawnDelay(scene);
|
||||
|
||||
Reference in New Issue
Block a user