Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,10 @@ export const BattleSceneCanvas = forwardRef<

const projX = startX + (endX - startX) * t;
const projY = startY + (endY - startY) * t;
drawWeapon(ctx, weaponType, projX, projY, w, h, progress, weaponName, !isPlayerAttack);
// threatScale: tier 1→1.0, tier 2→1.5, tier 3→2.2
const tier = template?.threatTier ?? 1;
const threatScale = tier === 3 ? 2.2 : tier === 2 ? 1.5 : 1.0;
drawWeapon(ctx, weaponType, projX, projY, w, h, progress, weaponName, !isPlayerAttack, threatScale);
}

// ── Render impact effects ───────────────────────────────────────
Expand Down Expand Up @@ -605,10 +608,12 @@ export const BattleSceneCanvas = forwardRef<

ctx.save();

// Subtle shake when player is hit
// Shake when player is hit — amplified by monster threat tier
if (playerFlash > 0) {
const pShakeX = playerFlash * (Math.random() - 0.5) * w * 0.008;
const pShakeY = playerFlash * (Math.random() - 0.5) * h * 0.008;
const tier = template?.threatTier ?? 1;
const shakeAmp = tier === 3 ? 0.020 : tier === 2 ? 0.013 : 0.008;
const pShakeX = playerFlash * (Math.random() - 0.5) * w * shakeAmp;
const pShakeY = playerFlash * (Math.random() - 0.5) * h * shakeAmp;
ctx.translate(pShakeX, pShakeY);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export type MonsterTemplate = {
};
/** Visual size relative to viewport (0-1). 1 = fills viewport, 0.3 = tiny. */
displayScale?: number;
/** Attack visual intensity: 1=minor, 2=moderate, 3=heavy/boss. Scales projectile size, particles, shake. */
threatTier?: 1 | 2 | 3;
/** If true, skip template cache — draw function is animated (e.g. GLB creature) */
dynamic?: boolean;
/** Draw silhouette on a pre-filled black canvas at (w x h) pixels */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2332,27 +2332,27 @@ function drawBasiliskRedux(ctx: CanvasRenderingContext2D, w: number, h: number)
export const MONSTER_TEMPLATES_REDUX: MonsterTemplate[] = [
// ── Zone 1: Dark Cave (Levels 1-12) ──
// displayScale controls visual size relative to viewport (0-1). Progressive: tiny rat → massive basilisk.
{ id: 'redux-dire-rat', name: 'Dire Rat', gridWidth: 6, gridHeight: 4, displayScale: 0.35, dynamic: true, monsterClass: 1, level: 1, atmosphere: { r: 140, g: 110, b: 70, intensity: 0.16 },
{ id: 'redux-dire-rat', name: 'Dire Rat', gridWidth: 6, gridHeight: 4, displayScale: 0.35, threatTier: 1, dynamic: true, monsterClass: 1, level: 1, atmosphere: { r: 140, g: 110, b: 70, intensity: 0.16 },
draw: makeGLBDrawFn('/models/creatures/dire-rat.glb', 6, 4, drawDireRatRedux) },
{ id: 'redux-kobold', name: 'Kobold', gridWidth: 7, gridHeight: 6, displayScale: 0.45, dynamic: true, monsterClass: 2, level: 2, atmosphere: { r: 160, g: 120, b: 50, intensity: 0.16 },
{ id: 'redux-kobold', name: 'Kobold', gridWidth: 7, gridHeight: 6, displayScale: 0.45, threatTier: 1, dynamic: true, monsterClass: 2, level: 2, atmosphere: { r: 160, g: 120, b: 50, intensity: 0.16 },
draw: makeGLBDrawFn('/models/creatures/kobold.glb', 7, 6, drawKoboldRedux) },
{ id: 'redux-goblin', name: 'Goblin', gridWidth: 8, gridHeight: 7, displayScale: 0.50, dynamic: true, monsterClass: 0, level: 3, atmosphere: { r: 96, g: 120, b: 48, intensity: 0.16 },
{ id: 'redux-goblin', name: 'Goblin', gridWidth: 8, gridHeight: 7, displayScale: 0.50, threatTier: 1, dynamic: true, monsterClass: 0, level: 3, atmosphere: { r: 96, g: 120, b: 48, intensity: 0.16 },
draw: makeGLBDrawFn('/models/creatures/goblin.glb', 8, 7, drawGoblinRedux) },
{ id: 'redux-giant-spider', name: 'Giant Spider', gridWidth: 10, gridHeight: 10, displayScale: 0.60, dynamic: true, monsterClass: 2, level: 4, atmosphere: { r: 72, g: 164, b: 226, intensity: 0.22 },
{ id: 'redux-giant-spider', name: 'Giant Spider', gridWidth: 10, gridHeight: 10, displayScale: 0.60, threatTier: 1, dynamic: true, monsterClass: 2, level: 4, atmosphere: { r: 72, g: 164, b: 226, intensity: 0.22 },
renderOverrides: { gamma: 0.52, ambient: 0.70, brightnessBoost: 2.2, charDensityFloor: 0.10 },
draw: makeGLBDrawFn('/models/creatures/giant-spider.glb', 10, 10, drawPhaseSpiderRedux) },
{ id: 'redux-skeleton', name: 'Skeleton', gridWidth: 7, gridHeight: 10, displayScale: 0.55, dynamic: true, monsterClass: 0, level: 5, atmosphere: { r: 96, g: 156, b: 70, intensity: 0.18 },
{ id: 'redux-skeleton', name: 'Skeleton', gridWidth: 7, gridHeight: 10, displayScale: 0.55, threatTier: 2, dynamic: true, monsterClass: 0, level: 5, atmosphere: { r: 96, g: 156, b: 70, intensity: 0.18 },
draw: makeGLBDrawFn('/models/creatures/skeleton.glb', 7, 10, drawSkeletonRedux) },
{ id: 'redux-goblin-shaman', name: 'Goblin Shaman', gridWidth: 8, gridHeight: 9, displayScale: 0.58, dynamic: true, monsterClass: 1, level: 6, atmosphere: { r: 106, g: 70, b: 164, intensity: 0.18 },
{ id: 'redux-goblin-shaman', name: 'Goblin Shaman', gridWidth: 8, gridHeight: 9, displayScale: 0.58, threatTier: 2, dynamic: true, monsterClass: 1, level: 6, atmosphere: { r: 106, g: 70, b: 164, intensity: 0.18 },
draw: makeGLBDrawFn('/models/creatures/goblin-shaman.glb', 8, 9, drawGoblinShamanRedux) },
{ id: 'redux-gelatinous-ooze', name: 'Gelatinous Ooze', gridWidth: 11, gridHeight: 14, displayScale: 0.70, monsterClass: 2, level: 7, atmosphere: { r: 66, g: 182, b: 56, intensity: 0.18 }, draw: drawGelatinousOozeRedux },
{ id: 'redux-bugbear', name: 'Bugbear', gridWidth: 10, gridHeight: 12, displayScale: 0.75, dynamic: true, monsterClass: 0, level: 8, atmosphere: { r: 172, g: 138, b: 72, intensity: 0.16 },
{ id: 'redux-gelatinous-ooze', name: 'Gelatinous Ooze', gridWidth: 11, gridHeight: 14, displayScale: 0.70, threatTier: 2, monsterClass: 2, level: 7, atmosphere: { r: 66, g: 182, b: 56, intensity: 0.18 }, draw: drawGelatinousOozeRedux },
{ id: 'redux-bugbear', name: 'Bugbear', gridWidth: 10, gridHeight: 12, displayScale: 0.75, threatTier: 2, dynamic: true, monsterClass: 0, level: 8, atmosphere: { r: 172, g: 138, b: 72, intensity: 0.16 },
draw: makeGLBDrawFn('/models/creatures/bugbear.glb', 10, 12, drawBugbearRedux) },
{ id: 'redux-carrion-crawler', name: 'Carrion Crawler', gridWidth: 12, gridHeight: 13, displayScale: 0.80, dynamic: true, monsterClass: 1, level: 9, atmosphere: { r: 144, g: 168, b: 208, intensity: 0.20 },
{ id: 'redux-carrion-crawler', name: 'Carrion Crawler', gridWidth: 12, gridHeight: 13, displayScale: 0.80, threatTier: 3, dynamic: true, monsterClass: 1, level: 9, atmosphere: { r: 144, g: 168, b: 208, intensity: 0.20 },
renderOverrides: { gamma: 0.52, ambient: 0.70, brightnessBoost: 2.0, charDensityFloor: 0.10 },
draw: drawCarrionCrawlerRedux },
{ id: 'redux-hook-horror', name: 'Hook Horror', gridWidth: 14, gridHeight: 14, displayScale: 0.85, dynamic: true, monsterClass: 2, level: 10, atmosphere: { r: 136, g: 82, b: 178, intensity: 0.18 },
{ id: 'redux-hook-horror', name: 'Hook Horror', gridWidth: 14, gridHeight: 14, displayScale: 0.85, threatTier: 3, dynamic: true, monsterClass: 2, level: 10, atmosphere: { r: 136, g: 82, b: 178, intensity: 0.18 },
draw: makeGLBDrawFn('/models/creatures/hook-horror.glb', 14, 14, drawDuskDrakeRedux) },
{ id: 'redux-basilisk', name: 'Basilisk', gridWidth: 18, gridHeight: 18, displayScale: 1.0, dynamic: true, monsterClass: 0, level: 12, isBoss: true, atmosphere: { r: 52, g: 86, b: 36, intensity: 0.16 }, renderOverrides: { gamma: 0.52, ambient: 0.60, brightnessBoost: 1.7, charDensityFloor: 0.10 },
{ id: 'redux-basilisk', name: 'Basilisk', gridWidth: 18, gridHeight: 18, displayScale: 1.0, threatTier: 3, dynamic: true, monsterClass: 0, level: 12, isBoss: true, atmosphere: { r: 52, g: 86, b: 36, intensity: 0.16 }, renderOverrides: { gamma: 0.52, ambient: 0.60, brightnessBoost: 1.7, charDensityFloor: 0.10 },
draw: makeGLBDrawFn('/models/creatures/basilisk.glb', 18, 18, drawBasiliskRedux) },
];
112 changes: 73 additions & 39 deletions packages/client/src/components/pretext/game/weaponAnimations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,38 +192,42 @@ function drawMonsterMelee(
w: number,
_h: number,
progress: number,
threatScale = 1,
): void {
// ASCII claw slash — three slash characters that spread on impact
// ASCII claw slash — scales with threat tier
ctx.save();
ctx.translate(x, y);
const sz = Math.round(w * 0.028);
const sz = Math.round(w * 0.028 * threatScale);
const font = `bold ${sz}px "Fira Code", monospace`;
const spread = progress * sz * 0.8;
const alpha = Math.max(0, 1 - progress * 0.3);

const chars = ['╲', '│', '╱'];
const offsets = [
{ dx: -spread, dy: -spread * 0.6 },
{ dx: 0, dy: 0 },
{ dx: spread, dy: -spread * 0.6 },
];
// Tier 3: 5 slashes (full claw), Tier 2: 4, Tier 1: 3
const allChars = ['╲', '╲', '│', '╱', '╱'];
const charCount = threatScale >= 2 ? 5 : threatScale >= 1.4 ? 4 : 3;
const startIdx = Math.floor((5 - charCount) / 2);
const chars = allChars.slice(startIdx, startIdx + charCount);
const halfW = (charCount - 1) / 2;

for (let i = 0; i < 3; i++) {
for (let i = 0; i < chars.length; i++) {
const dx = (i - halfW) * spread;
const dy = -Math.abs(i - halfW) * spread * 0.4;
ctx.font = font;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = `rgba(200,80,60,${alpha})`;
ctx.shadowColor = 'rgba(200,80,60,0.6)';
ctx.shadowBlur = 8;
ctx.fillText(chars[i], offsets[i].dx, offsets[i].dy);
ctx.shadowBlur = 4 + threatScale * 4;
ctx.fillText(chars[i], dx, dy);
}
ctx.shadowBlur = 0;

// ASCII trail
// ASCII trail — more particles for higher tiers
const trailCount = Math.round(3 + threatScale * 2);
ctx.font = `${Math.round(sz * 0.6)}px "Fira Code", monospace`;
for (let i = 1; i <= 3; i++) {
ctx.fillStyle = `rgba(180,60,40,${0.3 - i * 0.08})`;
ctx.fillText('·', -sz * i * 0.7, 0);
for (let i = 1; i <= trailCount; i++) {
ctx.fillStyle = `rgba(180,60,40,${0.4 - i * 0.06})`;
ctx.fillText(i <= 2 ? '×' : '·', -sz * i * 0.6, (i % 2 === 0 ? -1 : 1) * sz * 0.2);
}
ctx.restore();
}
Expand All @@ -235,9 +239,10 @@ function drawMonsterRanged(
w: number,
_h: number,
progress: number,
threatScale = 1,
): void {
// ASCII bone shard — rotating arrow-like characters
const sz = Math.round(w * 0.026);
// ASCII bone shard — scales with threat tier
const sz = Math.round(w * 0.026 * threatScale);
ctx.save();
ctx.translate(x, y);
ctx.rotate(progress * Math.PI * 1.5);
Expand All @@ -247,20 +252,29 @@ function drawMonsterRanged(
ctx.textBaseline = 'middle';
ctx.fillStyle = '#B8A88A';
ctx.shadowColor = 'rgba(184,168,138,0.5)';
ctx.shadowBlur = 6;
ctx.fillText('▶▸', 0, 0);
ctx.shadowBlur = 3 + threatScale * 4;
// Tier 3: full barrage, Tier 2: double shard, Tier 1: single
ctx.fillText(threatScale >= 2 ? '▶▶▸' : threatScale >= 1.4 ? '▶▸' : '▸', 0, 0);
ctx.shadowBlur = 0;

// Extra shards for higher tiers — scatter pattern
if (threatScale >= 1.4) {
ctx.font = `${Math.round(sz * 0.7)}px "Fira Code", monospace`;
ctx.fillStyle = 'rgba(184,168,138,0.6)';
ctx.fillText('▸', sz * 0.4, -sz * 0.5);
ctx.fillText('▸', sz * 0.4, sz * 0.5);
}
ctx.restore();

// ASCII trail
// ASCII trail — more particles for higher tiers
const trailCount = Math.round(3 + threatScale * 2);
const trailSz = Math.round(sz * 0.5);
ctx.font = `${trailSz}px "Fira Code", monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 1; i <= 3; i++) {
ctx.fillStyle = `rgba(160,140,120,${0.3 - i * 0.08})`;
ctx.fillText('·', x - sz * i * 0.6, y);
for (let i = 1; i <= trailCount; i++) {
ctx.fillStyle = `rgba(160,140,120,${0.35 - i * 0.06})`;
ctx.fillText('·', x - sz * i * 0.5, y + (i % 2 === 0 ? -2 : 2));
}
}

Expand All @@ -271,9 +285,10 @@ function drawMonsterSpell(
w: number,
_h: number,
progress: number,
threatScale = 1,
): void {
// ASCII shadow bolt — crisp dark rune with subtle glow
const sz = Math.round(w * 0.028);
// ASCII shadow bolt — scales with threat tier
const sz = Math.round(w * 0.028 * threatScale);
const pulse = 1 + Math.sin(progress * Math.PI * 5) * 0.15;
const pulseSz = Math.round(sz * pulse);
const alpha = Math.max(0, 1 - progress * 0.2);
Expand All @@ -283,26 +298,40 @@ function drawMonsterSpell(
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

// Main rune — tight shadow keeps the ASCII crisp
ctx.shadowColor = 'rgba(140,80,200,0.4)';
ctx.shadowBlur = 5;
// Main rune — more intense glow at higher tiers
ctx.shadowColor = `rgba(140,80,200,${0.3 + threatScale * 0.15})`;
ctx.shadowBlur = 3 + threatScale * 5;
ctx.fillStyle = `rgba(160,100,220,${alpha})`;
ctx.fillText('◆', x, y);
// Tier 3: triple rune, Tier 2: double, Tier 1: single
ctx.fillText(threatScale >= 2 ? '◆◆◆' : threatScale >= 1.4 ? '◆◆' : '◆', x, y);
ctx.shadowBlur = 0;

// Orbiting sigils for higher tiers
if (threatScale >= 1.4) {
const orbitR = sz * 0.8;
const orbitAngle = progress * Math.PI * 4;
ctx.font = `${Math.round(pulseSz * 0.4)}px "Fira Code", monospace`;
ctx.fillStyle = `rgba(200,160,255,${alpha * 0.7})`;
ctx.fillText('✦', x + Math.cos(orbitAngle) * orbitR, y + Math.sin(orbitAngle) * orbitR);
if (threatScale >= 2) {
ctx.fillText('✦', x + Math.cos(orbitAngle + Math.PI) * orbitR, y + Math.sin(orbitAngle + Math.PI) * orbitR);
}
}

// Inner sigil
ctx.font = `${Math.round(pulseSz * 0.45)}px "Fira Code", monospace`;
ctx.fillStyle = `rgba(200,160,255,${alpha})`;
ctx.fillText('*', x, y);

// ASCII particle trail — dark runes that fade
// ASCII particle trail — more runes for higher tiers
const trailCount = Math.round(4 + threatScale * 2);
ctx.font = `${Math.round(sz * 0.5)}px "Fira Code", monospace`;
const trailChars = ['◇', '·', '·', '.'];
for (let i = 0; i < trailChars.length; i++) {
const tx = x - sz * (i + 1) * 0.5;
const ty = y + (i % 2 === 0 ? -2 : 2);
ctx.fillStyle = `rgba(120,60,180,${(0.4 - i * 0.08) * alpha})`;
ctx.fillText(trailChars[i], tx, ty);
const trailChars = ['◇', '·', '◇', '·', '.', '.'];
for (let i = 0; i < trailCount; i++) {
const tx = x - sz * (i + 1) * 0.45;
const ty = y + (i % 2 === 0 ? -2 : 2) * threatScale;
ctx.fillStyle = `rgba(120,60,180,${(0.5 - i * 0.06) * alpha})`;
ctx.fillText(trailChars[i % trailChars.length], tx, ty);
}
ctx.restore();
}
Expand All @@ -320,7 +349,7 @@ const DRAW_FNS: Record<

const MONSTER_DRAW_FNS: Record<
WeaponAnimType,
(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, progress: number) => void
(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, progress: number, threatScale?: number) => void
> = {
melee: drawMonsterMelee,
ranged: drawMonsterRanged,
Expand All @@ -345,6 +374,8 @@ export function drawWeapon(
progress: number,
itemName?: string,
isMonsterAttack?: boolean,
/** Monster threat tier scale: 1=minor, 1.5=moderate, 2.2=heavy. Controls projectile size/particles. */
threatScale?: number,
): void {
// Try ASCII-rendered 3D item model first (player weapons only)
if (itemName && !isMonsterAttack) {
Expand Down Expand Up @@ -374,8 +405,11 @@ export function drawWeapon(
}

// 2D fallback — monster attacks use distinct visuals
const fns = isMonsterAttack ? MONSTER_DRAW_FNS : DRAW_FNS;
fns[type](ctx, x, y, w, h, progress);
if (isMonsterAttack) {
MONSTER_DRAW_FNS[type](ctx, x, y, w, h, progress, threatScale);
} else {
DRAW_FNS[type](ctx, x, y, w, h, progress);
}
}

// ── Animation speeds per weapon type (ms) ───────────────────────────────
Expand Down
Loading