import { FIGHTER_ANIMATION_OPTIONS, FIGHTER_FRAME_HEIGHT, FIGHTER_FRAME_WIDTH, KILL_HEAL_EFFECT_FRAME_RATE, KILL_HEAL_EFFECT_FRAMES, SELECTED_FIGHTER_OUTLINE_ALPHA, SELECTED_FIGHTER_OUTLINE_GAP, SELECTED_FIGHTER_OUTLINE_WIDTH, } from "../constants.js"; const SOURCE_ALPHA_THRESHOLD = 8; const HEAL_EFFECT_PATH = "assets/effects/heal/Heal_Effect.png"; const HEAL_EFFECT_KEY = "kill-heal-effect"; const HEAL_EFFECT_ANIMATION_KEY = `${HEAL_EFFECT_KEY}-anim`; export function fighterSheetKey(skin, action) { return `${skin.key}-${action}`; } 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`; } export function fighterAttackEffectAnimationKey(skin) { return `${fighterAttackEffectKey(skin)}-anim`; } export function fighterProjectileKey(skin) { return `${skin.key}-projectile`; } export function healEffectKey() { return HEAL_EFFECT_KEY; } export function healEffectAnimationKey() { return HEAL_EFFECT_ANIMATION_KEY; } export function preloadFighterSheets(scene, skins) { scene.load.spritesheet(healEffectKey(), HEAL_EFFECT_PATH, { frameWidth: FIGHTER_FRAME_WIDTH, frameHeight: FIGHTER_FRAME_HEIGHT, }); skins.forEach((skin) => { Object.entries(skin.animations).forEach(([action, animation]) => { scene.load.spritesheet( fighterSheetKey(skin, action), `${skin.assetRoot}/${animation.file}`, { frameWidth: FIGHTER_FRAME_WIDTH, frameHeight: FIGHTER_FRAME_HEIGHT }, ); }); preloadCombatAssets(scene, skin); }); } export function createFighterAnimations(scene, skins) { skins.forEach((skin) => { Object.entries(skin.animations).forEach(([action, animation]) => { const key = fighterAnimationKey(skin, action); 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, }); } createFighterOutlineSheet(scene, skin, action, animation.frames); }); createAttackEffectAnimation(scene, skin); }); createHealEffectAnimation(scene); } function preloadCombatAssets(scene, skin) { const projectile = skin.combat?.projectile; const attackEffect = skin.combat?.attackEffect; if (projectile) { scene.load.image(fighterProjectileKey(skin), `${skin.assetRoot}/${projectile.file}`); } if (attackEffect) { scene.load.spritesheet( fighterAttackEffectKey(skin), `${skin.assetRoot}/${attackEffect.file}`, { frameWidth: FIGHTER_FRAME_WIDTH, frameHeight: FIGHTER_FRAME_HEIGHT }, ); } } function createAttackEffectAnimation(scene, skin) { const attackEffect = skin.combat?.attackEffect; if (!attackEffect) { return; } const key = fighterAttackEffectAnimationKey(skin); if (scene.anims.exists(key)) { return; } scene.anims.create({ key, frames: scene.anims.generateFrameNumbers(fighterAttackEffectKey(skin), { start: 0, end: attackEffect.frames - 1, }), frameRate: attackEffect.frameRate ?? 14, repeat: 0, }); } function createHealEffectAnimation(scene) { if (scene.anims.exists(healEffectAnimationKey())) { return; } scene.anims.create({ key: healEffectAnimationKey(), frames: scene.anims.generateFrameNumbers(healEffectKey(), { start: 0, end: KILL_HEAL_EFFECT_FRAMES - 1, }), frameRate: KILL_HEAL_EFFECT_FRAME_RATE, 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] = 255; outlineData[outlineIndex + 1] = 255; outlineData[outlineIndex + 2] = 255; outlineData[outlineIndex + 3] = outlineAlpha; } }