Update Healer Man server
This commit is contained in:
1 parent
a64b2cb575
commit
71246e5b75
502 files changed
+4245
-19
No files matched your search
@@ -0,0 +1,278 @@
|
||||
import { memo, useEffect, useMemo, useRef, type MutableRefObject } from "react";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import {
|
||||
AddEquation, BufferAttribute, CustomBlending, DoubleSide, DynamicDrawUsage,
|
||||
InstancedBufferAttribute, InstancedBufferGeometry, OneFactor, OneMinusSrcAlphaFactor,
|
||||
ShaderMaterial, Vector2, ZeroFactor, type Texture,
|
||||
} from "three";
|
||||
import type { CombatPresentationPhase, CombatVfxQuality } from "../game/combatPresentation";
|
||||
import {
|
||||
runeEmissionsBetween, runeLayers, runeSample, type RuneParticleLayer, type RuneVector3,
|
||||
type RunewakerParticleGraph,
|
||||
} from "./runewakerParticleRuntime";
|
||||
|
||||
interface Particle {
|
||||
born: number;
|
||||
life: number;
|
||||
origin: RuneVector3;
|
||||
velocity: RuneVector3;
|
||||
acceleration: RuneVector3;
|
||||
scale: number;
|
||||
orientation: number;
|
||||
rotationSign: number;
|
||||
}
|
||||
|
||||
const vertexShader = /* glsl */ `
|
||||
attribute vec3 instancePosition;
|
||||
attribute vec4 instanceColor;
|
||||
attribute vec2 instanceSize;
|
||||
attribute float instanceRotation;
|
||||
attribute float instanceFrame;
|
||||
varying vec2 vUv;
|
||||
varying vec4 vColor;
|
||||
varying float vFrame;
|
||||
uniform vec2 quadOffset;
|
||||
|
||||
void main() {
|
||||
vec4 viewPosition = modelViewMatrix * vec4(instancePosition, 1.0);
|
||||
vec2 local = (position.xy + quadOffset) * instanceSize;
|
||||
float s = sin(instanceRotation);
|
||||
float c = cos(instanceRotation);
|
||||
vec2 rotated = vec2(local.x * c - local.y * s, local.x * s + local.y * c);
|
||||
viewPosition.xy += rotated * length(modelViewMatrix[0].xyz);
|
||||
gl_Position = projectionMatrix * viewPosition;
|
||||
vUv = uv;
|
||||
vColor = instanceColor;
|
||||
vFrame = instanceFrame;
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentShader = /* glsl */ `
|
||||
uniform sampler2D sourceTexture;
|
||||
uniform vec2 tileUv;
|
||||
uniform float atlasColumns;
|
||||
uniform float frameCount;
|
||||
varying vec2 vUv;
|
||||
varying vec4 vColor;
|
||||
varying float vFrame;
|
||||
|
||||
void main() {
|
||||
if (vColor.a <= 0.002) discard;
|
||||
float frame = mod(floor(max(0.0, vFrame)), max(1.0, frameCount));
|
||||
float column = mod(frame, atlasColumns);
|
||||
float row = floor(frame / atlasColumns);
|
||||
vec2 sourceUv = vec2(column * tileUv.x + vUv.x * tileUv.x,
|
||||
1.0 - (row + 1.0) * tileUv.y + vUv.y * tileUv.y);
|
||||
vec4 sampled = texture2D(sourceTexture, sourceUv);
|
||||
float alpha = sampled.a * vColor.a;
|
||||
if (alpha <= 0.002) discard;
|
||||
gl_FragColor = vec4(sampled.rgb * vColor.rgb * alpha, alpha);
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`;
|
||||
|
||||
function random(seed: number): number {
|
||||
let value = (seed + 0x6d2b79f5) | 0;
|
||||
value = Math.imul(value ^ value >>> 15, value | 1);
|
||||
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
||||
return ((value ^ value >>> 14) >>> 0) / 4_294_967_296;
|
||||
}
|
||||
|
||||
function vectorAt(layer: RuneParticleLayer, track: number, time: number, fallback: RuneVector3): RuneVector3 {
|
||||
return runeSample(layer.emitter.tracks[track] as { time: number; value: RuneVector3 }[], time, fallback);
|
||||
}
|
||||
|
||||
function scalarAt(layer: RuneParticleLayer, track: number, time: number, fallback = 0): number {
|
||||
return runeSample(layer.emitter.tracks[track] as { time: number; value: number }[], time, fallback);
|
||||
}
|
||||
|
||||
function spawn(layer: RuneParticleLayer, time: number, seed: number): Particle {
|
||||
const randomAt = (index: number) => random(seed + index * 1619);
|
||||
const kind = Math.round(scalarAt(layer, 1, time));
|
||||
const emitterPosition = vectorAt(layer, 2, time, [0, 0, 0]);
|
||||
const extents = vectorAt(layer, 3, time, [0, 0, 0]);
|
||||
const radius = scalarAt(layer, 4, time);
|
||||
let offset: RuneVector3 = [0, 0, 0];
|
||||
if (kind === 1) {
|
||||
offset = [(randomAt(1) * 2 - 1) * extents[0],
|
||||
(randomAt(2) * 2 - 1) * extents[1], (randomAt(3) * 2 - 1) * extents[2]];
|
||||
} else if (kind === 4) {
|
||||
const angle = randomAt(1) * Math.PI * 2;
|
||||
const distance = randomAt(2) * radius;
|
||||
const orientation = layer.emitter.emitterOrientation;
|
||||
offset = orientation === 1 || orientation === 4
|
||||
? [0, Math.sin(angle) * distance, Math.cos(angle) * distance]
|
||||
: orientation === 2 || orientation === 5
|
||||
? [Math.sin(angle) * distance, 0, Math.cos(angle) * distance]
|
||||
: [Math.sin(angle) * distance, Math.cos(angle) * distance, 0];
|
||||
}
|
||||
const direction = vectorAt(layer, 10, time, [0, 1, 0]);
|
||||
const deviation = Math.min(Math.PI, Math.abs(scalarAt(layer, 11, time)));
|
||||
const spread = Math.tan(deviation * 0.5);
|
||||
const dx = direction[0] + (randomAt(4) * 2 - 1) * spread;
|
||||
const dy = direction[1] + (randomAt(5) * 2 - 1) * spread;
|
||||
const dz = direction[2] + (randomAt(6) * 2 - 1) * spread;
|
||||
const magnitude = Math.max(0.00001, Math.hypot(dx, dy, dz));
|
||||
const speed = scalarAt(layer, 14, time) + randomAt(7) * (
|
||||
scalarAt(layer, 15, time) - scalarAt(layer, 14, time));
|
||||
const force0 = vectorAt(layer, 20, time, [0, 0, 0]);
|
||||
const force1 = vectorAt(layer, 21, time, [0, 0, 0]);
|
||||
const minLife = scalarAt(layer, 12, time, 0.5);
|
||||
const maxLife = scalarAt(layer, 13, time, 0.5);
|
||||
const minScale = scalarAt(layer, 16, time, 1);
|
||||
const maxScale = scalarAt(layer, 17, time, 1);
|
||||
const minOrientation = vectorAt(layer, 18, time, [0, 0, 0]);
|
||||
const maxOrientation = vectorAt(layer, 19, time, [0, 0, 0]);
|
||||
return {
|
||||
born: time,
|
||||
life: Math.max(0.02, minLife + randomAt(8) * (maxLife - minLife)),
|
||||
origin: [layer.position[0] + emitterPosition[0] + offset[0],
|
||||
layer.position[1] + emitterPosition[1] + offset[1],
|
||||
-layer.position[2] - emitterPosition[2] - offset[2]],
|
||||
velocity: [dx / magnitude * speed, dy / magnitude * speed, -dz / magnitude * speed],
|
||||
acceleration: [force0[0] + force1[0], force0[1] + force1[1], -force0[2] - force1[2]],
|
||||
scale: minScale + randomAt(9) * (maxScale - minScale),
|
||||
orientation: (minOrientation[0] + randomAt(10) * (maxOrientation[0] - minOrientation[0])) * Math.PI / 180,
|
||||
rotationSign: layer.sprite.randomRotation && randomAt(11) < 0.5 ? -1 : 1,
|
||||
};
|
||||
}
|
||||
|
||||
const RuneEmitter = memo(function RuneEmitter({
|
||||
layer, texture, budget, active, eventId, timelineRef,
|
||||
}: {
|
||||
readonly layer: RuneParticleLayer;
|
||||
readonly texture: Texture;
|
||||
readonly budget: number;
|
||||
readonly active: boolean;
|
||||
readonly eventId: number;
|
||||
readonly timelineRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
}) {
|
||||
const system = useRef({ particles: Array<Particle | null>(budget).fill(null), debt: 0, births: 0, lastTime: 0 });
|
||||
const geometry = useMemo(() => {
|
||||
const next = new InstancedBufferGeometry();
|
||||
next.setAttribute("position", new BufferAttribute(new Float32Array([
|
||||
-0.5, -0.5, 0, 0.5, -0.5, 0, 0.5, 0.5, 0, -0.5, 0.5, 0,
|
||||
]), 3));
|
||||
next.setAttribute("uv", new BufferAttribute(new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]), 2));
|
||||
next.setIndex([0, 1, 2, 0, 2, 3]);
|
||||
for (const [name, components] of [
|
||||
["instancePosition", 3], ["instanceColor", 4], ["instanceSize", 2],
|
||||
["instanceRotation", 1], ["instanceFrame", 1],
|
||||
] as const) {
|
||||
const attribute = new InstancedBufferAttribute(new Float32Array(budget * components), components);
|
||||
attribute.setUsage(DynamicDrawUsage);
|
||||
next.setAttribute(name, attribute);
|
||||
}
|
||||
next.instanceCount = budget;
|
||||
return next;
|
||||
}, [budget]);
|
||||
const material = useMemo(() => {
|
||||
const image = texture.image as { width?: number; height?: number } | undefined;
|
||||
const tileWidth = Math.max(1, layer.sprite.tileSizePixels[0]);
|
||||
const tileHeight = Math.max(1, layer.sprite.tileSizePixels[1]);
|
||||
const width = Math.max(tileWidth, image?.width ?? tileWidth);
|
||||
const height = Math.max(tileHeight, image?.height ?? tileHeight);
|
||||
return new ShaderMaterial({
|
||||
name: `runewaker-particle-${layer.sprite.anchor}`,
|
||||
vertexShader, fragmentShader,
|
||||
uniforms: {
|
||||
sourceTexture: { value: texture },
|
||||
tileUv: { value: new Vector2(tileWidth / width, tileHeight / height) },
|
||||
atlasColumns: { value: Math.max(1, Math.floor(width / tileWidth)) },
|
||||
frameCount: { value: Math.max(1, layer.sprite.frameCount) },
|
||||
quadOffset: { value: new Vector2(layer.sprite.quadOffset[0], layer.sprite.quadOffset[1]) },
|
||||
},
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
side: DoubleSide,
|
||||
toneMapped: false,
|
||||
blending: CustomBlending,
|
||||
blendEquation: AddEquation,
|
||||
blendSrc: layer.sprite.blendMode === 2 ? ZeroFactor : OneFactor,
|
||||
blendDst: layer.sprite.blendMode === 1 ? OneFactor : OneMinusSrcAlphaFactor,
|
||||
});
|
||||
}, [layer, texture]);
|
||||
|
||||
useEffect(() => () => { geometry.dispose(); material.dispose(); }, [geometry, material]);
|
||||
|
||||
useFrame(() => {
|
||||
if (!active || budget === 0) return;
|
||||
const time = timelineRef.current.elapsedMs / 1_000;
|
||||
const current = system.current;
|
||||
if (time < current.lastTime) {
|
||||
current.particles.fill(null); current.debt = 0; current.births = 0;
|
||||
}
|
||||
const previousTime = current.lastTime;
|
||||
current.lastTime = time;
|
||||
current.debt = Math.min(budget, current.debt + runeEmissionsBetween(
|
||||
layer.emitter.tracks[9] as { time: number; value: number }[], previousTime, time,
|
||||
));
|
||||
let births = Math.min(budget, Math.floor(current.debt));
|
||||
current.debt -= births;
|
||||
for (let index = 0; index < budget; index++) {
|
||||
const particle = current.particles[index];
|
||||
if (particle && time - particle.born >= particle.life) current.particles[index] = null;
|
||||
if (!current.particles[index] && births > 0) {
|
||||
const seed = eventId * 1009 + current.births * 7919 + index * 97;
|
||||
current.particles[index] = spawn(layer, time, seed);
|
||||
current.births++; births--;
|
||||
}
|
||||
}
|
||||
const positions = geometry.getAttribute("instancePosition") as InstancedBufferAttribute;
|
||||
const colors = geometry.getAttribute("instanceColor") as InstancedBufferAttribute;
|
||||
const sizes = geometry.getAttribute("instanceSize") as InstancedBufferAttribute;
|
||||
const rotations = geometry.getAttribute("instanceRotation") as InstancedBufferAttribute;
|
||||
const frames = geometry.getAttribute("instanceFrame") as InstancedBufferAttribute;
|
||||
for (let index = 0; index < budget; index++) {
|
||||
const particle = current.particles[index];
|
||||
if (!particle) { colors.setXYZW(index, 0, 0, 0, 0); continue; }
|
||||
const age = Math.max(0, time - particle.born);
|
||||
const lifetime = Math.min(1, age / particle.life);
|
||||
const color = runeSample(layer.sprite.tracks[0], lifetime, [1, 1, 1, 1]);
|
||||
const alpha = runeSample(layer.sprite.tracks[1], lifetime, 1);
|
||||
const scale = runeSample(layer.sprite.tracks[2], lifetime, 1) * particle.scale;
|
||||
const rotationSpeed = runeSample(layer.sprite.tracks[3], lifetime, 0) * Math.PI / 180;
|
||||
const ageSquared = age * age * 0.5;
|
||||
positions.setXYZ(index,
|
||||
particle.origin[0] + particle.velocity[0] * age + particle.acceleration[0] * ageSquared,
|
||||
particle.origin[1] + particle.velocity[1] * age + particle.acceleration[1] * ageSquared,
|
||||
particle.origin[2] + particle.velocity[2] * age + particle.acceleration[2] * ageSquared);
|
||||
colors.setXYZW(index, color[0], color[1], color[2], Math.max(0, Math.min(1, alpha * color[3])));
|
||||
sizes.setXY(index, Math.max(0.001, scale * layer.sprite.quadScale[0]),
|
||||
Math.max(0.001, scale * layer.sprite.quadScale[1]));
|
||||
rotations.setX(index, layer.sprite.orientToTravel
|
||||
? Math.atan2(particle.velocity[0], particle.velocity[1])
|
||||
: particle.orientation + age * rotationSpeed * particle.rotationSign);
|
||||
frames.setX(index, Math.floor(age * Math.max(0, layer.sprite.frameRate)));
|
||||
}
|
||||
for (const attribute of [positions, colors, sizes, rotations, frames]) attribute.needsUpdate = true;
|
||||
});
|
||||
|
||||
return <mesh name="original-runewaker-particle-layer" geometry={geometry} material={material} frustumCulled={false} />;
|
||||
});
|
||||
|
||||
export const RunewakerParticleEffect = memo(function RunewakerParticleEffect({
|
||||
graph, textures, phase, quality, active, eventId, playbackRef,
|
||||
}: {
|
||||
readonly graph: RunewakerParticleGraph;
|
||||
readonly textures: ReadonlyMap<string, Texture>;
|
||||
readonly phase: CombatPresentationPhase;
|
||||
readonly quality: Exclude<CombatVfxQuality, "off">;
|
||||
readonly active: boolean;
|
||||
readonly eventId: number;
|
||||
readonly playbackRef: MutableRefObject<{ elapsedMs: number; decaying: boolean }>;
|
||||
}) {
|
||||
const layers = useMemo(() => runeLayers(graph, phase), [graph, phase]);
|
||||
const limit = quality === "high" ? 256 : 96;
|
||||
return <group name="original-runewaker-particles" scale={0.1}>
|
||||
{layers.map((layer, index) => {
|
||||
const texture = textures.get(layer.sprite.textureAssetUrl);
|
||||
if (!texture) return null;
|
||||
const sourceMaximum = Number(layer.emitter.tracks[0][0]?.value ?? 0);
|
||||
const budget = Math.max(1, Math.min(sourceMaximum, Math.floor(limit / Math.max(1, layers.length))));
|
||||
return <RuneEmitter key={`${layer.sprite.anchor}:${index}`} layer={layer} texture={texture}
|
||||
budget={budget} active={active} eventId={eventId + index * 271}
|
||||
timelineRef={playbackRef} />;
|
||||
})}
|
||||
</group>;
|
||||
});
|
||||
Reference in new issue
Block a user