Add special caster sparkle effect
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user