combat-visual-effects
Custom GLSL filters for hit effects, glow chaining, DisplacementFilter shockwaves, Spine integration, particle emitter combat patterns, z-ordering, and alpha masking in PixiJS v8+
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Custom GLSL filters for hit effects, glow chaining, DisplacementFilter shockwaves, Spine integration, particle emitter combat patterns, z-ordering, and alpha masking in PixiJS v8+
Agent definition
combat-visual-effects.mddescription: Custom GLSL filters for hit effects, glow chaining, DisplacementFilter shockwaves, Spine integration, particle emitter combat patterns, z-ordering, and alpha masking in PixiJS v8+
agent: pixijs-combat-renderer
category: visual-techniques
version_range: "PixiJS v8+"
Combat Visual Effects Reference
> **Scope**: PixiJS v8 visual effects for combat rendering. Performance/batching in pixi-performance.md.
---
PIXI.Filter Custom GLSL for Hit Effects
import { Filter } from 'pixi.js';
const hitFlashVert = `
attribute vec2 aPosition;
varying vec2 vTextureCoord;
uniform vec4 inputSize;
uniform vec4 outputFrame;
vec4 filterVertexPosition() {
vec2 position = aPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;
return vec4((position / (inputSize.xy / inputSize.zw)) * 2.0 - 1.0, 0.0, 1.0);
}
void main() {
gl_Position = filterVertexPosition();
vTextureCoord = aPosition * (outputFrame.zw * inputSize.zw);
}
`;
const hitFlashFrag = `
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float uFlash; // 0.0 = normal, 1.0 = full white
void main() {
vec4 sample = texture2D(uSampler, vTextureCoord);
gl_FragColor = vec4(mix(sample.rgb, vec3(1.0), uFlash * sample.a), sample.a);
}
`;
export class HitFlashFilter extends Filter {
constructor() {
super({ glProgram: { vertex: hitFlashVert, fragment: hitFlashFrag } });
this.resources.uniforms = { uFlash: 0.0 };
}
get flash(): number { return this.resources.uniforms.uFlash; }
set flash(v: number) { this.resources.uniforms.uFlash = v; }
}
// Animate via GSAP
gsap.to(hitFilter, {
flash: 1.0, duration: 0.06, yoyo: true, repeat: 1,
onComplete: () => { characterSprite.filters = []; },
});---
BlurFilter Chaining for Glow
import { AdvancedBloomFilter } from '@pixi/filter-advanced-bloom';
const bloomFilter = new AdvancedBloomFilter({
threshold: 0.4, bloomScale: 0.8, brightness: 1.2, kernelSize: 5, quality: 4,
});
characterContainer.filters = [bloomFilter];
// Manual two-pass glow (no external package)
function addManualGlow(target: Sprite, glowColor: number, blur = 12): Container {
const wrapper = new Container();
const glow = Sprite.from(target.texture);
glow.tint = glowColor;
glow.alpha = 0.7;
glow.filters = [new BlurFilter({ strength: blur })];
glow.anchor.set(target.anchor.x, target.anchor.y);
wrapper.addChild(glow);
wrapper.addChild(target);
return wrapper;
}---
DisplacementFilter for Shockwave
export async function createShockwave(stage: Container, x: number, y: number): Promise<void> {
const dispTexture = await Assets.load('/textures/displacement_circle.png');
const dispSprite = new Sprite(dispTexture);
dispSprite.anchor.set(0.5);
dispSprite.position.set(x, y);
dispSprite.scale.set(0.1);
stage.addChild(dispSprite);
const displacementFilter = new DisplacementFilter({ sprite: dispSprite, scale: { x: 80, y: 80 } });
stage.filters = stage.filters ? [...stage.filters, displacementFilter] : [displacementFilter];
gsap.to(dispSprite.scale, { x: 3.0, y: 3.0, duration: 0.6, ease: 'power2.out' });
gsap.to(displacementFilter.scale, {
x: 0, y: 0, duration: 0.6, ease: 'power3.in',
onComplete: () => { stage.filters = stage.filters!.filter(f => f !== displacementFilter); dispSprite.destroy(); },
});
}---
Spine Animation Integration
import { Spine as PixiSpine } from '@esotericsoftware/spine-pixi-v8';
async function loadSpineCharacter(key: string): Promise<PixiSpine> {
await Assets.load([
{ alias: `${key}-skel`, src: `/spine/${key}.skel` },
{ alias: `${key}-atlas`, src: `/spine/${key}.atlas` },
]);
return PixiSpine.from({ skeleton: `${key}-skel`, atlas: `${key}-atlas` });
}
class SpineCombatCharacter {
private spine: PixiSpine;
private state: string = 'idle';
constructor(spine: PixiSpine) {
this.spine = spine;
this.spine.state.setAnimation(0, 'idle', true);
}
attack(): void {
if (this.state === 'hit') return;
this.state = 'attack';
this.spine.state.setAnimation(0, 'attack', false);
this.spine.state.addAnimation(0, 'idle', true, 0);
this.spine.state.addListener({
complete: (entry) => { if (entry.animation?.name === 'attack') this.state = 'idle'; },
});
}
takeHit(): void {
this.state = 'hit';
this.spine.state.setAnimation(1, 'hit', false);
this.spine.state.addAnimation(1, null!, false, 0);
setTimeout(() => { this.state = 'idle'; }, 300);
}
}---
Particle Emitter — Impact Burst
function burstImpact(stage: Container, x: number, y: number): void {
const particleContainer = new Container();
particleContainer.position.set(x, y);
stage.addChild(particleContainer);
const emitter = new Emitter(particleContainer, {
lifetime: { min: 0.2, max: 0.5 }, frequency: 0.001, spawnChance: 1,
particlesPerWave: 12, maxParticles: 12, pos: { x: 0, y: 0 },
behaviors: [
{ type: 'alpha', config: { alpha: { list: [{ value: 1, time: 0 }, { value: 0, time: 1 }] } } },
{ type: 'scale', config: { scale: { list: [{ value: 0.6, time: 0 }, { value: 0.1, time: 1 }] } } },
{ type: 'color', config: { color: { list: [{ value: 'ff6600', time: 0 }, { value: 'ff2200', time: 0.3 }, { value: '330000', time: 1 }] } } },
{ type: 'moveSpeedStatic', config: { min: 150, max: 400 } },
{ type: 'rotationStatic', config: { min: 0, max: 360 } },
],
});
emitter.emit = true;
setTimeout(() => { emitter.emit = false; setTimeout(() => { emitter.destroy(); particleContainer.destroy(); }, 600); }, 50);
}---
Z-Index and Sorting
// Strategy 1: sortableChildren + zIndex
const stage = new Container();
stage.sortableChildren = true;
background.zIndex = 0; player.zIndex = 10; hitEffect.zIndex = 20; uiContainer.zIndex = 100;
// Strategy 2: explicit layer containers (faster — no sort)
const bgL
Read more
description: Custom GLSL filters for hit effects, glow chaining, DisplacementFilter shockwaves, Spine integration, particle emitter combat patterns, z-ordering, and alpha masking in PixiJS v8+ agent: pixijs-combat-renderer category: visual-techniques version_range: "PixiJS v8+"
Combat Visual Effects Reference
> **Scope**: PixiJS v8 visual effects for combat rendering. Performance/batching in pixi-performance.md.
---
PIXI.Filter Custom GLSL for Hit Effects
import { Filter } from 'pixi.js';
const hitFlashVert = `
attribute vec2 aPosition;
varying vec2 vTextureCoord;
uniform vec4 inputSize;
uniform vec4 outputFrame;
vec4 filterVertexPosition() {
vec2 position = aPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;
return vec4((position / (inputSize.xy / inputSize.zw)) * 2.0 - 1.0, 0.0, 1.0);
}
void main() {
gl_Position = filterVertexPosition();
vTextureCoord = aPosition * (outputFrame.zw * inputSize.zw);
}
`;
const hitFlashFrag = `
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform float uFlash; // 0.0 = normal, 1.0 = full white
void main() {
vec4 sample = texture2D(uSampler, vTextureCoord);
gl_FragColor = vec4(mix(sample.rgb, vec3(1.0), uFlash * sample.a), sample.a);
}
`;
export class HitFlashFilter extends Filter {
constructor() {
super({ glProgram: { vertex: hitFlashVert, fragment: hitFlashFrag } });
this.resources.uniforms = { uFlash: 0.0 };
}
get flash(): number { return this.resources.uniforms.uFlash; }
set flash(v: number) { this.resources.uniforms.uFlash = v; }
}
// Animate via GSAP
gsap.to(hitFilter, {
flash: 1.0, duration: 0.06, yoyo: true, repeat: 1,
onComplete: () => { characterSprite.filters = []; },
});---
BlurFilter Chaining for Glow
import { AdvancedBloomFilter } from '@pixi/filter-advanced-bloom';
const bloomFilter = new AdvancedBloomFilter({
threshold: 0.4, bloomScale: 0.8, brightness: 1.2, kernelSize: 5, quality: 4,
});
characterContainer.filters = [bloomFilter];
// Manual two-pass glow (no external package)
function addManualGlow(target: Sprite, glowColor: number, blur = 12): Container {
const wrapper = new Container();
const glow = Sprite.from(target.texture);
glow.tint = glowColor;
glow.alpha = 0.7;
glow.filters = [new BlurFilter({ strength: blur })];
glow.anchor.set(target.anchor.x, target.anchor.y);
wrapper.addChild(glow);
wrapper.addChild(target);
return wrapper;
}---
DisplacementFilter for Shockwave
export async function createShockwave(stage: Container, x: number, y: number): Promise<void> {
const dispTexture = await Assets.load('/textures/displacement_circle.png');
const dispSprite = new Sprite(dispTexture);
dispSprite.anchor.set(0.5);
dispSprite.position.set(x, y);
dispSprite.scale.set(0.1);
stage.addChild(dispSprite);
const displacementFilter = new DisplacementFilter({ sprite: dispSprite, scale: { x: 80, y: 80 } });
stage.filters = stage.filters ? [...stage.filters, displacementFilter] : [displacementFilter];
gsap.to(dispSprite.scale, { x: 3.0, y: 3.0, duration: 0.6, ease: 'power2.out' });
gsap.to(displacementFilter.scale, {
x: 0, y: 0, duration: 0.6, ease: 'power3.in',
onComplete: () => { stage.filters = stage.filters!.filter(f => f !== displacementFilter); dispSprite.destroy(); },
});
}---
Spine Animation Integration
import { Spine as PixiSpine } from '@esotericsoftware/spine-pixi-v8';
async function loadSpineCharacter(key: string): Promise<PixiSpine> {
await Assets.load([
{ alias: `${key}-skel`, src: `/spine/${key}.skel` },
{ alias: `${key}-atlas`, src: `/spine/${key}.atlas` },
]);
return PixiSpine.from({ skeleton: `${key}-skel`, atlas: `${key}-atlas` });
}
class SpineCombatCharacter {
private spine: PixiSpine;
private state: string = 'idle';
constructor(spine: PixiSpine) {
this.spine = spine;
this.spine.state.setAnimation(0, 'idle', true);
}
attack(): void {
if (this.state === 'hit') return;
this.state = 'attack';
this.spine.state.setAnimation(0, 'attack', false);
this.spine.state.addAnimation(0, 'idle', true, 0);
this.spine.state.addListener({
complete: (entry) => { if (entry.animation?.name === 'attack') this.state = 'idle'; },
});
}
takeHit(): void {
this.state = 'hit';
this.spine.state.setAnimation(1, 'hit', false);
this.spine.state.addAnimation(1, null!, false, 0);
setTimeout(() => { this.state = 'idle'; }, 300);
}
}---
Particle Emitter — Impact Burst
function burstImpact(stage: Container, x: number, y: number): void {
const particleContainer = new Container();
particleContainer.position.set(x, y);
stage.addChild(particleContainer);
const emitter = new Emitter(particleContainer, {
lifetime: { min: 0.2, max: 0.5 }, frequency: 0.001, spawnChance: 1,
particlesPerWave: 12, maxParticles: 12, pos: { x: 0, y: 0 },
behaviors: [
{ type: 'alpha', config: { alpha: { list: [{ value: 1, time: 0 }, { value: 0, time: 1 }] } } },
{ type: 'scale', config: { scale: { list: [{ value: 0.6, time: 0 }, { value: 0.1, time: 1 }] } } },
{ type: 'color', config: { color: { list: [{ value: 'ff6600', time: 0 }, { value: 'ff2200', time: 0.3 }, { value: '330000', time: 1 }] } } },
{ type: 'moveSpeedStatic', config: { min: 150, max: 400 } },
{ type: 'rotationStatic', config: { min: 0, max: 360 } },
],
});
emitter.emit = true;
setTimeout(() => { emitter.emit = false; setTimeout(() => { emitter.destroy(); particleContainer.destroy(); }, 600); }, 50);
}---
Z-Index and Sorting
// Strategy 1: sortableChildren + zIndex const stage = new Container(); stage.sortableChildren = true; background.zIndex = 0; player.zIndex = 10; hitEffect.zIndex = 20; uiContainer.zIndex = 100; // Strategy 2: explicit layer containers (faster — no sort) const bgL
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

