arena/src/game/fighterAssets.js

267 lines
7.2 KiB
JavaScript

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;
}
}