Add special caster sparkle effect

This commit is contained in:
2026-06-01 18:57:07 +09:00
parent 3b1a883787
commit 3b7fa17d06
5 changed files with 151 additions and 1 deletions
+18
View File
@@ -215,6 +215,24 @@ export const SPECIAL_EFFECT = {
BALANCE_NON_MAGIC_TYPES: false,
INVULNERABLE_MS: 4500,
},
CASTER_SPARKLE: {
ENABLED: true,
key: "special-caster-eye-sparkle",
path: "assets/effects/special/effect.png",
frames: 12,
frameWidth: 100,
frameHeight: 100,
frameRate: 12,
frameSequence: [2, 3, 4],
repeat: -1,
scaleMultiplier: 1,
anchorX: 50,
anchorY: 40,
effectAnchorX: 54,
effectAnchorY: 50,
depthOffset: 0.25,
alpha: 1,
},
CAMERA: {
ZOOM: 3,
CENTER_ON_CASTER_AT_START: true,
+131 -1
View File
@@ -1,5 +1,5 @@
import Phaser from "phaser";
import { ARENA, SPECIAL_EFFECT } from "../../constants.js";
import { ARENA, FIGHTER, SPECIAL_EFFECT } from "../../constants.js";
import {
applySpecialEffectInstantKill,
disposeCombatObject,
@@ -14,6 +14,17 @@ import { FIGHTER_TYPES, getFighterType } from "../fighter/fighterStats.js";
const SPECIAL_ANIMATION_SUFFIX = "anim";
export function preloadSpecialEffectAssets(scene) {
if (SPECIAL_EFFECT.CASTER_SPARKLE.ENABLED) {
scene.load.spritesheet(
SPECIAL_EFFECT.CASTER_SPARKLE.key,
SPECIAL_EFFECT.CASTER_SPARKLE.path,
{
frameWidth: SPECIAL_EFFECT.CASTER_SPARKLE.frameWidth,
frameHeight: SPECIAL_EFFECT.CASTER_SPARKLE.frameHeight,
},
);
}
SPECIAL_EFFECT.MELEE.ASSETS.forEach((asset) => {
scene.load.spritesheet(asset.key, asset.path, {
frameWidth: SPECIAL_EFFECT.MELEE.FRAME_WIDTH,
@@ -32,6 +43,16 @@ export function preloadSpecialEffectAssets(scene) {
}
export function createSpecialEffectAnimations(scene) {
if (SPECIAL_EFFECT.CASTER_SPARKLE.ENABLED) {
createSpecialAnimation(scene, {
frameRate: SPECIAL_EFFECT.CASTER_SPARKLE.frameRate,
frameSequence: SPECIAL_EFFECT.CASTER_SPARKLE.frameSequence,
frames: SPECIAL_EFFECT.CASTER_SPARKLE.frames,
key: SPECIAL_EFFECT.CASTER_SPARKLE.key,
repeat: SPECIAL_EFFECT.CASTER_SPARKLE.repeat,
});
}
SPECIAL_EFFECT.MELEE.ASSETS.forEach((asset) => {
createSpecialAnimation(scene, {
frameRate: SPECIAL_EFFECT.MELEE.FRAME_RATE,
@@ -136,10 +157,12 @@ function beginSpecialCast(scene, caster, target, matchId) {
pauseSpecialPreparationCombat(scene, state);
lockCasterInHurtFrame(scene, caster);
createSpecialFocusLayer(scene, state, caster);
spawnCasterSparkle(scene, state, caster);
scene.beginSpecialEffectCameraFocus?.(caster);
addRealtimeStateTimer(state, SPECIAL_EFFECT.CASTER.HURT_HOLD_MS, () => {
resumeSpecialPreparationCombat(scene, state);
disposeCasterSparkle(scene, state);
if (!isSpecialCastValid(scene, caster, matchId)) {
cleanupSpecialCastState(scene, state, { restoreCaster: true });
@@ -498,6 +521,7 @@ function createSpecialCastState(scene, caster, matchId) {
disposed: false,
hitFighters: new Set(),
matchId,
casterSparkle: null,
projectile: null,
timers: new Set(),
};
@@ -529,6 +553,8 @@ function cleanupSpecialCastState(
state.attackCompleteHandler = null;
}
disposeCasterSparkle(scene, state);
if (disposeProjectile && state.projectile?.active) {
disposeCombatObject(scene, state.projectile);
}
@@ -870,6 +896,110 @@ function restoreSpecialFocusCasterDepth(state, focusLayer) {
}
}
function spawnCasterSparkle(scene, state, caster) {
const config = SPECIAL_EFFECT.CASTER_SPARKLE;
if (!config.ENABLED || state.casterSparkle || !scene.textures.exists(config.key)) {
return;
}
const casterScale = resolveCasterScale(caster);
const sparkleScale = resolveCasterSparkleScale(casterScale, config);
const sparkle = scene.add
.sprite(0, 0, config.key, resolveFirstSpecialFrame(config))
.setOrigin(
resolveAnchorRatio(config.effectAnchorX, config.frameWidth),
resolveAnchorRatio(config.effectAnchorY, config.frameHeight),
)
.setDepth(caster.depth + (Number(config.depthOffset) || 0))
.setScale(sparkleScale)
.setAlpha(Phaser.Math.Clamp(Number(config.alpha) || 1, 0, 1))
.setBlendMode(Phaser.BlendModes.ADD);
const syncSparklePosition = () => {
if (!sparkle.active || !caster.active) {
return;
}
const direction = caster.flipX ? -1 : 1;
const position = resolveCasterSparklePosition(caster, config, casterScale);
sparkle.setPosition(
caster.x + position.x * direction,
caster.y + position.y,
);
};
syncSparklePosition();
sparkle.play(specialAnimationKey(config.key));
scene.events.on(Phaser.Scenes.Events.UPDATE, syncSparklePosition);
trackCombatObject(scene, sparkle);
sparkle.cleanup = () => {
scene.events.off(Phaser.Scenes.Events.UPDATE, syncSparklePosition);
if (state.casterSparkle === sparkle) {
state.casterSparkle = null;
}
};
state.casterSparkle = sparkle;
}
function disposeCasterSparkle(scene, state) {
if (state?.casterSparkle?.active) {
disposeCombatObject(scene, state.casterSparkle);
}
}
function resolveCasterScale(caster) {
return Math.max(
Math.abs(caster.scaleX ?? 1),
Math.abs(caster.scaleY ?? 1),
1,
);
}
function resolveCasterSparkleScale(casterScale, config) {
const multiplier = Math.max(0, Number(config.scaleMultiplier) || 1);
return casterScale * multiplier;
}
function resolveCasterSparklePosition(caster, config, casterScale) {
const originX = Number.isFinite(caster.originX) ? caster.originX : 0.5;
const originY = Number.isFinite(caster.originY) ? caster.originY : 0.5;
const anchorX = Number.isFinite(config.anchorX)
? config.anchorX
: FIGHTER.FRAME_WIDTH * originX;
const anchorY = Number.isFinite(config.anchorY)
? config.anchorY
: FIGHTER.FRAME_HEIGHT * originY;
return {
x: (anchorX - FIGHTER.FRAME_WIDTH * originX) * casterScale,
y: (anchorY - FIGHTER.FRAME_HEIGHT * originY) * casterScale,
};
}
function resolveAnchorRatio(anchor, size) {
const safeSize = Math.max(1, Number(size) || 1);
const safeAnchor = Number.isFinite(anchor) ? anchor : safeSize / 2;
return Phaser.Math.Clamp(safeAnchor / safeSize, 0, 1);
}
function resolveFirstSpecialFrame(config) {
const firstFrameNumber = Array.isArray(config.frameSequence)
? config.frameSequence[0]
: 1;
return Phaser.Math.Clamp(
Math.round(Number(firstFrameNumber) || 1) - 1,
0,
Math.max(0, (Number(config.frames) || 1) - 1),
);
}
function selectSpecialCaster(scene) {
const livingFighters = scene.fighters.filter(isLivingFighter);
const summaries = livingTeamSummaries(livingFighters);