Release Healer Man 0.1.9

This commit is contained in:
phenom committed 2026-08-30 14:16:56 -04:00
1 parent c245a199b6
commit d060296ade
74 files changed
+5901 -334

No files matched your search

+41 -27
View File
@@ -1,8 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import {
AdditiveBlending,
Color,
DoubleSide,
Group,
MeshBasicMaterial,
@@ -19,34 +18,32 @@ import {
type CombatVfxQuality,
} from "../game/combatPresentation";
import {
combatAudioOutputEnabled,
configureCombatAudio,
playCombatAudio,
resetCombatAudio,
setCombatAudioListener,
stopCombatAudioGroup,
} from "../game/combatAudio";
interface ActiveCombatEffect {
readonly key: string;
readonly event: CombatPresentationEvent;
}
import {
advanceCombatEffectFrame,
capCombatEffects,
updateCombatEffectQueue,
type ActiveCombatEffect,
} from "./combatEffectRuntime";
export interface CombatEffectsProps {
readonly active: boolean;
readonly safeGraphics?: boolean;
}
const QUALITY_SYSTEM_LIMIT: Readonly<Record<CombatVfxQuality, number>> = {
off: 0,
low: 24,
high: 64,
};
const QUALITY_PARTICLE_COUNT: Readonly<Record<Exclude<CombatVfxQuality, "off">, number>> = {
low: 48,
high: 128,
};
const COMBAT_EFFECT_DATASET_KEY = "combatEffectLoad";
function effectDurationMs(event: CombatPresentationEvent): number {
if (event.phase === "cast-start") return Math.max(280, Math.min(4_500, event.durationMs ?? 900));
if (event.phase === "tick") return 320;
@@ -77,7 +74,7 @@ function deterministicParticles(seed: number, count: number): Float32Array {
return result;
}
function CombatEffectPrimitive({
const CombatEffectPrimitive = memo(function CombatEffectPrimitive({
event,
quality,
active,
@@ -119,15 +116,16 @@ function CombatEffectPrimitive({
useFrame((_state, delta) => {
if (!active || doneRef.current || !groupRef.current) return;
elapsedRef.current += Math.min(0.05, delta) * 1_000;
const progress = Math.min(1, elapsedRef.current / durationMs);
const frame = advanceCombatEffectFrame(elapsedRef.current, delta, durationMs);
elapsedRef.current = frame.elapsedMs;
const progress = frame.progress;
const eased = 1 - (1 - progress) ** 3;
if (projectile) {
groupRef.current.position.lerpVectors(origin, target, eased);
groupRef.current.position.y += 1.05 + Math.sin(progress * Math.PI) * 0.28;
groupRef.current.scale.setScalar(0.72 + Math.sin(progress * Math.PI) * 0.32);
} else {
groupRef.current.rotation.y += delta * (event.school === "physical" ? 4 : 1.8);
groupRef.current.rotation.y += frame.motionDeltaSeconds * (event.school === "physical" ? 4 : 1.8);
const scale = event.phase === "cast-start"
? 0.85 + Math.sin(progress * Math.PI * 3) * 0.08
: 0.35 + eased * (event.delivery === "area" ? Math.max(1.2, event.radius ?? 2.4) : 1.75);
@@ -140,7 +138,7 @@ function CombatEffectPrimitive({
if (pointsMaterialRef.current) opacity >= 0 && (pointsMaterialRef.current.opacity = opacity * 0.9);
if (primaryMaterialRef.current) primaryMaterialRef.current.opacity = opacity * 0.72;
if (secondaryMaterialRef.current) secondaryMaterialRef.current.opacity = opacity * 0.55;
if (progress >= 1) {
if (frame.done) {
doneRef.current = true;
onDone();
}
@@ -234,12 +232,13 @@ function CombatEffectPrimitive({
</mesh>
</>
)}
{quality === "high" && event.school !== "physical" ? (
<pointLight color={new Color(style.primary)} intensity={style.emissive * 1.5} distance={5} decay={2} />
) : null}
</group>
);
}
}, (previous, next) => (
previous.event === next.event
&& previous.quality === next.quality
&& previous.active === next.active
));
export function CombatEffects({ active, safeGraphics = false }: CombatEffectsProps) {
const settings = useShellStore((state) => state.combatPresentationSettings);
@@ -253,6 +252,21 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
const qualityRef = useRef(effectiveQuality);
const camera = useThree((state) => state.camera);
const forward = useMemo(() => new Vector3(), []);
const audioOutputEnabled = combatAudioOutputEnabled(settings, masterVolume);
useEffect(() => {
if (!import.meta.env.DEV) return;
document.documentElement.dataset[COMBAT_EFFECT_DATASET_KEY] = JSON.stringify({
quality: effectiveQuality,
systems: effects.length,
activeLights: 0,
allocatedLights: 0,
});
}, [effectiveQuality, effects.length]);
useEffect(() => () => {
if (import.meta.env.DEV) delete document.documentElement.dataset[COMBAT_EFFECT_DATASET_KEY];
}, []);
useEffect(() => {
settingsRef.current = settings;
@@ -260,11 +274,11 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
configureCombatAudio(settings, masterVolume);
if (
qualityRef.current !== "off"
|| Object.values(settings.audioEnabled).some(Boolean)
|| combatAudioOutputEnabled(settings, masterVolume)
) void preloadCombatEffectManifest();
setEffects((current) => current.filter(({ event }) => (
setEffects((current) => capCombatEffects(current.filter(({ event }) => (
qualityRef.current !== "off" && settings.vfxEnabled[event.actorGroup]
)));
)), qualityRef.current));
for (const group of ["player", "party", "enemy"] as const) {
if (!settings.audioEnabled[group] || settings.audioVolume[group] <= 0) stopCombatAudioGroup(group);
}
@@ -298,14 +312,14 @@ export function CombatEffects({ active, safeGraphics = false }: CombatEffectsPro
|| quality === "off"
|| !presentationSettings.vfxEnabled[event.actorGroup]
) return withoutCast;
const limit = QUALITY_SYSTEM_LIMIT[quality];
return [...withoutCast, { key: `${event.sessionId}:${event.id}`, event }].slice(-limit);
return updateCombatEffectQueue(withoutCast, event, quality);
});
}), []);
useEffect(() => () => resetCombatAudio(), []);
useFrame(() => {
if (!active || !audioOutputEnabled) return;
camera.getWorldDirection(forward);
setCombatAudioListener(
[camera.position.x, camera.position.y, camera.position.z],