Files
gamercomp/frontend/public/assets/avatar/contexts/pure.js
T
2026-04-30 02:14:25 +00:00

1105 lines
34 KiB
JavaScript

/**
* Pure Contexts - Minimal modifications to avatar for standard gameplay
* Uses FULL_BODY mode with the avatar fully visible
*/
(function() {
'use strict';
// ============================================
// SHARED EFFECT RENDERERS
// ============================================
/**
* Render dust puff effects
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X position
* @param {number} y - Ground Y position
* @param {number} scale - Avatar scale
* @param {number} intensity - Effect intensity (0-1)
* @param {number} time - Animation time in seconds
* @param {Object} options - Additional options
*/
function renderDust(ctx, x, y, scale, intensity, time, options = {}) {
const {
color = 'rgba(139, 119, 101, 0.6)',
particleCount = 5,
spread = 20 * scale,
riseSpeed = 30,
fadeTime = 0.5
} = options;
const progress = Math.min(time / fadeTime, 1);
const alpha = (1 - progress) * intensity;
if (alpha <= 0) return;
ctx.save();
for (let i = 0; i < particleCount; i++) {
const angle = (i / particleCount) * Math.PI + Math.random() * 0.2;
const distance = spread * progress * (0.5 + Math.random() * 0.5);
const px = x + Math.cos(angle) * distance;
const py = y - Math.sin(angle) * distance * 0.5 - riseSpeed * progress * scale;
const size = (8 + Math.random() * 6) * scale * (1 - progress * 0.5);
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = color.replace(/[\d.]+\)$/, (alpha * 0.6) + ')');
ctx.fill();
}
ctx.restore();
}
/**
* Render speed lines behind avatar
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Avatar center X
* @param {number} y - Avatar center Y
* @param {number} scale - Avatar scale
* @param {number} direction - Direction (-1 = left, 1 = right)
* @param {number} intensity - Effect intensity (0-1)
* @param {Object} options - Additional options
*/
function renderSpeedLines(ctx, x, y, scale, direction, intensity, options = {}) {
const {
lineCount = 8,
maxLength = 60,
color = 'rgba(255, 255, 255, 0.7)',
spread = 80
} = options;
if (intensity <= 0) return;
ctx.save();
ctx.lineCap = 'round';
for (let i = 0; i < lineCount; i++) {
const yOffset = (i - lineCount / 2) * (spread / lineCount) * scale;
const lineY = y + yOffset;
const lineLength = (maxLength * (0.4 + Math.random() * 0.6)) * scale * intensity;
const lineWidth = (2 + Math.random() * 2) * scale;
const startX = x - direction * 30 * scale;
const alpha = intensity * (0.3 + Math.random() * 0.4);
ctx.beginPath();
ctx.moveTo(startX, lineY);
ctx.lineTo(startX - direction * lineLength, lineY);
ctx.strokeStyle = color.replace(/[\d.]+\)$/, alpha + ')');
ctx.lineWidth = lineWidth;
ctx.stroke();
}
ctx.restore();
}
/**
* Render cartoon stars circling around head
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X (head position)
* @param {number} y - Center Y (head position)
* @param {number} scale - Avatar scale
* @param {number} count - Number of stars
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderStars(ctx, x, y, scale, count, time, options = {}) {
const {
radius = 25,
starSize = 8,
rotationSpeed = 3,
color = '#FFD700',
highlightColor = '#FFFFFF'
} = options;
ctx.save();
const orbitRadius = radius * scale;
const size = starSize * scale;
for (let i = 0; i < count; i++) {
const angle = (time * rotationSpeed) + (i * Math.PI * 2 / count);
const sx = x + Math.cos(angle) * orbitRadius;
const sy = y + Math.sin(angle) * orbitRadius * 0.4; // Elliptical orbit
// Draw 4-pointed star
ctx.beginPath();
for (let j = 0; j < 8; j++) {
const starAngle = (j * Math.PI / 4) + time * 2;
const pointRadius = j % 2 === 0 ? size : size * 0.4;
const px = sx + Math.cos(starAngle) * pointRadius;
const py = sy + Math.sin(starAngle) * pointRadius;
if (j === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
// Highlight
ctx.beginPath();
ctx.arc(sx - size * 0.2, sy - size * 0.2, size * 0.2, 0, Math.PI * 2);
ctx.fillStyle = highlightColor;
ctx.fill();
}
ctx.restore();
}
/**
* Render celebratory confetti
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Start Y
* @param {number} scale - Avatar scale
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderConfetti(ctx, x, y, scale, time, options = {}) {
const {
particleCount = 30,
spread = 100,
colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8'],
gravity = 150,
initialVelocity = 200
} = options;
if (time > 3) return; // Stop after 3 seconds
ctx.save();
// Use seeded random for consistent particles
const seed = 12345;
const random = (i, offset = 0) => {
const x = Math.sin(seed + i * 9999 + offset) * 10000;
return x - Math.floor(x);
};
for (let i = 0; i < particleCount; i++) {
const angle = random(i, 0) * Math.PI - Math.PI / 2;
const velocity = initialVelocity * (0.5 + random(i, 1) * 0.5) * scale;
const vx = Math.cos(angle) * velocity * (random(i, 2) - 0.5) * 2;
const vy = -Math.abs(Math.sin(angle) * velocity);
const px = x + vx * time;
const py = y + vy * time + 0.5 * gravity * time * time * scale;
// Skip if fallen too far
if (py > y + 200 * scale) continue;
const rotation = time * (3 + random(i, 3) * 4) * (random(i, 4) > 0.5 ? 1 : -1);
const width = (4 + random(i, 5) * 4) * scale;
const height = (8 + random(i, 6) * 6) * scale;
const color = colors[Math.floor(random(i, 7) * colors.length)];
ctx.save();
ctx.translate(px, py);
ctx.rotate(rotation);
ctx.fillStyle = color;
ctx.fillRect(-width / 2, -height / 2, width, height);
ctx.restore();
}
ctx.restore();
}
/**
* Render shadow beneath avatar
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Ground Y
* @param {number} width - Shadow base width
* @param {number} height - Shadow base height
* @param {number} elevation - Avatar elevation (affects size)
* @param {Object} options - Additional options
*/
function renderShadow(ctx, x, y, width, height, elevation, options = {}) {
const {
color = 'rgba(0, 0, 0, 0.3)',
maxElevation = 100,
minScale = 0.5
} = options;
const elevationFactor = Math.max(0, 1 - elevation / maxElevation);
const scale = minScale + (1 - minScale) * elevationFactor;
const alpha = 0.3 * elevationFactor;
if (alpha <= 0) return;
ctx.save();
ctx.beginPath();
ctx.ellipse(x, y, width * scale / 2, height * scale / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = color.replace(/[\d.]+\)$/, alpha + ')');
ctx.fill();
ctx.restore();
}
/**
* Render sparkle/shine effect
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Center Y
* @param {number} scale - Effect scale
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderSparkle(ctx, x, y, scale, time, options = {}) {
const {
count = 5,
spread = 40,
color = '#FFFFFF',
maxSize = 6,
twinkleSpeed = 4
} = options;
ctx.save();
const seed = 54321;
const random = (i, offset = 0) => {
const v = Math.sin(seed + i * 7777 + offset) * 10000;
return v - Math.floor(v);
};
for (let i = 0; i < count; i++) {
const px = x + (random(i, 0) - 0.5) * spread * 2 * scale;
const py = y + (random(i, 1) - 0.5) * spread * 2 * scale;
const phase = random(i, 2) * Math.PI * 2;
const twinkle = Math.abs(Math.sin(time * twinkleSpeed + phase));
const size = maxSize * scale * twinkle;
if (size < 0.5) continue;
// Draw 4-pointed sparkle
ctx.beginPath();
ctx.moveTo(px, py - size);
ctx.lineTo(px + size * 0.3, py);
ctx.lineTo(px, py + size);
ctx.lineTo(px - size * 0.3, py);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.beginPath();
ctx.moveTo(px - size, py);
ctx.lineTo(px, py + size * 0.3);
ctx.lineTo(px + size, py);
ctx.lineTo(px, py - size * 0.3);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
/**
* Render flash/impact effect
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Center Y
* @param {number} width - Flash width
* @param {number} height - Flash height
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderFlash(ctx, x, y, width, height, time, options = {}) {
const {
color = 'rgba(255, 255, 255, 0.8)',
duration = 0.3,
pulseCount = 2
} = options;
const progress = time / duration;
if (progress > 1) return;
const pulse = Math.sin(progress * Math.PI * pulseCount);
const alpha = pulse * (1 - progress);
if (alpha <= 0) return;
ctx.save();
ctx.beginPath();
ctx.ellipse(x, y, width / 2, height / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = color.replace(/[\d.]+\)$/, alpha + ')');
ctx.fill();
ctx.restore();
}
/**
* Render heroic glow effect
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Center Y
* @param {number} width - Glow width
* @param {number} height - Glow height
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderGlow(ctx, x, y, width, height, time, options = {}) {
const {
color = 'rgba(255, 215, 0, 0.15)',
pulseSpeed = 2,
pulseAmount = 0.2
} = options;
const pulse = 1 + Math.sin(time * pulseSpeed) * pulseAmount;
ctx.save();
const gradient = ctx.createRadialGradient(x, y, 0, x, y, Math.max(width, height) * pulse / 2);
gradient.addColorStop(0, color);
gradient.addColorStop(1, 'rgba(255, 215, 0, 0)');
ctx.beginPath();
ctx.ellipse(x, y, width * pulse / 2, height * pulse / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
/**
* Render motion blur trail
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Current X
* @param {number} y - Current Y
* @param {number} scale - Avatar scale
* @param {number} direction - Direction (-1 = left, 1 = right)
* @param {number} intensity - Blur intensity
* @param {Object} options - Additional options
*/
function renderMotionBlur(ctx, x, y, scale, direction, intensity, options = {}) {
const {
trailCount = 4,
spacing = 15,
color = 'rgba(200, 200, 255, 0.3)',
width = 20,
height = 60
} = options;
if (intensity <= 0) return;
ctx.save();
for (let i = 1; i <= trailCount; i++) {
const alpha = (1 - i / (trailCount + 1)) * intensity * 0.3;
const tx = x - direction * spacing * i * scale;
ctx.beginPath();
ctx.ellipse(tx, y, width * scale / 2, height * scale / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = color.replace(/[\d.]+\)$/, alpha + ')');
ctx.fill();
}
ctx.restore();
}
/**
* Render friction sparks
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Center X
* @param {number} y - Ground Y
* @param {number} scale - Effect scale
* @param {number} direction - Direction of movement
* @param {number} time - Animation time
* @param {Object} options - Additional options
*/
function renderSparks(ctx, x, y, scale, direction, time, options = {}) {
const {
count = 8,
color = '#FFA500',
highlightColor = '#FFFF00',
spread = 30
} = options;
ctx.save();
const seed = 98765;
const random = (i, offset = 0) => {
const v = Math.sin(seed + i * 3333 + offset + time * 100) * 10000;
return v - Math.floor(v);
};
for (let i = 0; i < count; i++) {
const lifetime = random(i, 0) * 0.3;
const sparkTime = (time % 0.3);
if (sparkTime > lifetime) continue;
const angle = -direction * (Math.PI / 6 + random(i, 1) * Math.PI / 3);
const velocity = (50 + random(i, 2) * 50) * scale;
const px = x + Math.cos(angle) * velocity * sparkTime;
const py = y + Math.sin(angle) * velocity * sparkTime - 20 * sparkTime * scale;
const size = (2 + random(i, 3) * 2) * scale * (1 - sparkTime / lifetime);
ctx.beginPath();
ctx.arc(px, py, size, 0, Math.PI * 2);
ctx.fillStyle = random(i, 4) > 0.5 ? color : highlightColor;
ctx.fill();
}
ctx.restore();
}
/**
* Render small backpack
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Avatar center X
* @param {number} y - Avatar center Y
* @param {number} scale - Avatar scale
* @param {boolean} facingRight - Avatar facing direction
*/
function renderBackpack(ctx, x, y, scale, facingRight) {
const offsetX = facingRight ? -25 : 25;
const bx = x + offsetX * scale;
const by = y - 10 * scale;
ctx.save();
// Main bag
ctx.fillStyle = '#8B4513';
ctx.beginPath();
ctx.roundRect(bx - 10 * scale, by - 15 * scale, 20 * scale, 25 * scale, 3 * scale);
ctx.fill();
// Straps
ctx.strokeStyle = '#654321';
ctx.lineWidth = 2 * scale;
ctx.beginPath();
ctx.moveTo(bx - 8 * scale, by - 15 * scale);
ctx.lineTo(bx - 5 * scale, by - 25 * scale);
ctx.moveTo(bx + 8 * scale, by - 15 * scale);
ctx.lineTo(bx + 5 * scale, by - 25 * scale);
ctx.stroke();
// Pocket
ctx.fillStyle = '#A0522D';
ctx.beginPath();
ctx.roundRect(bx - 6 * scale, by - 5 * scale, 12 * scale, 10 * scale, 2 * scale);
ctx.fill();
// Flap
ctx.fillStyle = '#654321';
ctx.beginPath();
ctx.roundRect(bx - 8 * scale, by - 15 * scale, 16 * scale, 6 * scale, [3 * scale, 3 * scale, 0, 0]);
ctx.fill();
ctx.restore();
}
/**
* Render utility belt
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Avatar center X
* @param {number} y - Avatar waist Y
* @param {number} scale - Avatar scale
*/
function renderUtilityBelt(ctx, x, y, scale) {
ctx.save();
// Belt
ctx.fillStyle = '#4A4A4A';
ctx.fillRect(x - 25 * scale, y, 50 * scale, 6 * scale);
// Buckle
ctx.fillStyle = '#C0C0C0';
ctx.fillRect(x - 5 * scale, y - 1 * scale, 10 * scale, 8 * scale);
// Pouches
ctx.fillStyle = '#5A5A5A';
ctx.fillRect(x - 22 * scale, y + 4 * scale, 8 * scale, 10 * scale);
ctx.fillRect(x + 14 * scale, y + 4 * scale, 8 * scale, 10 * scale);
ctx.restore();
}
/**
* Render sweatband
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Head center X
* @param {number} y - Forehead Y
* @param {number} scale - Avatar scale
* @param {string} color - Band color
*/
function renderSweatband(ctx, x, y, scale, color = '#FF4444') {
ctx.save();
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, y, 22 * scale, 4 * scale, 0, 0, Math.PI * 2);
ctx.fill();
// Stripe
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.ellipse(x, y - 1 * scale, 20 * scale, 1.5 * scale, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
/**
* Render jersey number
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Back center X
* @param {number} y - Back center Y
* @param {number} scale - Avatar scale
* @param {number} number - Jersey number
* @param {boolean} facingRight - Avatar facing direction
*/
function renderJerseyNumber(ctx, x, y, scale, number, facingRight) {
// Only show when avatar is facing away (back visible)
if (facingRight) return;
ctx.save();
ctx.font = `bold ${16 * scale}px Arial`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(number), x, y);
ctx.restore();
}
/**
* Render wind-swept hair effect
* @param {CanvasRenderingContext2D} ctx
* @param {number} x - Head X
* @param {number} y - Head top Y
* @param {number} scale - Avatar scale
* @param {number} intensity - Wind intensity
* @param {number} time - Animation time
*/
function renderWindEffect(ctx, x, y, scale, intensity, time) {
ctx.save();
const lineCount = 5;
ctx.strokeStyle = 'rgba(200, 220, 255, 0.4)';
ctx.lineWidth = 2 * scale;
ctx.lineCap = 'round';
for (let i = 0; i < lineCount; i++) {
const startY = y + (i - lineCount / 2) * 8 * scale;
const waveOffset = Math.sin(time * 8 + i) * 3 * scale;
const length = (20 + Math.random() * 15) * scale * intensity;
ctx.beginPath();
ctx.moveTo(x + 15 * scale, startY + waveOffset);
ctx.quadraticCurveTo(
x + 15 * scale + length / 2, startY + waveOffset + 5 * scale,
x + 15 * scale + length, startY + waveOffset
);
ctx.stroke();
}
ctx.restore();
}
// ============================================
// PURE CONTEXTS
// ============================================
const platformerStandard = {
id: 'platformer-standard',
name: 'Platformer Hero',
category: 'pure',
avatarMode: 'FULL_BODY',
animations: ['idle', 'walk', 'run', 'jump', 'fall', 'crouch', 'hurt', 'celebrate'],
defaultAnimation: 'idle',
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
layerOrder: ['background-effects', 'avatar', 'foreground-effects'],
recommendedFor: ['platformer', 'adventure', 'action'],
description: 'Standard platforming character with no costume modifications',
avatarPosition: { x: 0.5, y: 0.5 },
avatarScale: 1.0,
renderEffectsBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
const groundY = y + (avatar.height || 100) * scale / 2;
const elevation = state.jumping || state.falling ? (state.elevation || 30) : 0;
// Shadow
renderShadow(ctx, x, groundY, 50 * scale, 15 * scale, elevation);
// Launch dust when jumping starts
if (state.justJumped && state.jumpTime !== undefined) {
renderDust(ctx, x, groundY, scale, 1.0, state.jumpTime, {
particleCount: 8,
spread: 30 * scale,
color: 'rgba(139, 119, 101, 0.7)'
});
}
// Landing impact dust
if (state.justLanded && state.landTime !== undefined) {
renderDust(ctx, x, groundY, scale, 1.2, state.landTime, {
particleCount: 10,
spread: 40 * scale,
color: 'rgba(139, 119, 101, 0.8)'
});
}
},
renderEffectsOver: function(ctx, avatar, x, y, scale, transforms, time, state) {
const groundY = y + (avatar.height || 100) * scale / 2;
const headY = y - (avatar.height || 100) * scale / 2 + 15 * scale;
// Running dust
if (state.running && !state.jumping && !state.falling) {
const dustTime = (time * 10) % 0.5;
if (dustTime < 0.3) {
renderDust(ctx, x + (state.facingRight ? -15 : 15) * scale, groundY, scale, 0.5, dustTime, {
particleCount: 3,
spread: 15 * scale
});
}
}
// Hurt stars
if (state.hurt && state.hurtTime !== undefined && state.hurtTime < 0.5) {
renderStars(ctx, x, headY, scale, 4, time);
}
// Celebration confetti
if (state.celebrating && state.celebrateTime !== undefined) {
renderConfetti(ctx, x, y - 50 * scale, scale, state.celebrateTime);
}
},
stateEffects: {
running: { type: 'dust', frequency: 0.1 },
jumping: { type: 'launch-dust', once: true },
landing: { type: 'impact-dust', once: true },
hurt: { type: 'stars', duration: 0.5 },
celebrating: { type: 'confetti', duration: 3.0 }
}
};
const sportsPlayer = {
id: 'sports-player',
name: 'Sports Player',
category: 'pure',
avatarMode: 'FULL_BODY',
animations: ['idle', 'walk', 'run', 'jump', 'fall', 'crouch', 'hurt', 'celebrate', 'throw', 'catch', 'kick'],
defaultAnimation: 'idle',
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
layerOrder: ['background-effects', 'costume-back', 'avatar', 'costume-front', 'foreground-effects'],
recommendedFor: ['sports', 'basketball', 'soccer', 'tennis', 'racing'],
description: 'Athletic character with minimal sports equipment',
avatarPosition: { x: 0.5, y: 0.5 },
avatarScale: 1.0,
jerseyNumber: 7,
renderEffectsBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
const groundY = y + (avatar.height || 100) * scale / 2;
const elevation = state.jumping || state.falling ? (state.elevation || 30) : 0;
// Court/field shadow (slightly elongated)
renderShadow(ctx, x, groundY, 60 * scale, 12 * scale, elevation, {
color: 'rgba(0, 0, 0, 0.25)'
});
// Speed lines when running fast
if (state.running && state.speed > 0.7) {
const direction = state.facingRight ? 1 : -1;
renderSpeedLines(ctx, x, y, scale, direction, state.speed * 0.8, {
lineCount: 6,
maxLength: 50,
color: 'rgba(255, 255, 255, 0.5)'
});
}
// Athletic motion blur when jumping
if (state.jumping && state.speed > 0.5) {
const direction = state.facingRight ? 1 : -1;
renderMotionBlur(ctx, x, y, scale, direction, state.speed * 0.6);
}
},
renderCostumeBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
// Jersey number on back
const backY = y - 10 * scale;
renderJerseyNumber(ctx, x, backY, scale, this.jerseyNumber, state.facingRight);
},
renderCostumeFront: function(ctx, avatar, x, y, scale, transforms, time, state) {
// Sweatband on forehead
const headY = y - (avatar.height || 100) * scale / 2 + 20 * scale;
renderSweatband(ctx, x, headY, scale, '#FF4444');
},
renderEffectsOver: function(ctx, avatar, x, y, scale, transforms, time, state) {
const headY = y - (avatar.height || 100) * scale / 2 + 15 * scale;
// Scoring celebration sparkles
if (state.scoring && state.scoreTime !== undefined) {
renderSparkle(ctx, x, y - 30 * scale, scale, state.scoreTime, {
count: 8,
spread: 60,
color: '#FFD700',
maxSize: 10
});
}
// Hurt impact flash
if (state.hurt && state.hurtTime !== undefined && state.hurtTime < 0.3) {
renderFlash(ctx, x, y, 80 * scale, 120 * scale, state.hurtTime, {
color: 'rgba(255, 100, 100, 0.5)',
duration: 0.3
});
}
// Celebration
if (state.celebrating && state.celebrateTime !== undefined) {
renderSparkle(ctx, x, y, scale * 1.5, state.celebrateTime, {
count: 12,
spread: 80,
color: '#FFFFFF'
});
}
},
stateEffects: {
running: { type: 'speed-lines', threshold: 0.7 },
jumping: { type: 'motion-blur', once: false },
scoring: { type: 'sparkles', duration: 1.0 },
hurt: { type: 'flash', duration: 0.3 },
celebrating: { type: 'sparkles', duration: 2.0 }
}
};
const adventureHero = {
id: 'adventure-hero',
name: 'Adventure Hero',
category: 'pure',
avatarMode: 'FULL_BODY',
animations: ['idle', 'walk', 'run', 'jump', 'fall', 'crouch', 'hurt', 'celebrate', 'climb', 'push', 'pull'],
defaultAnimation: 'idle',
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
layerOrder: ['background-effects', 'costume-back', 'avatar', 'costume-front', 'foreground-effects'],
recommendedFor: ['adventure', 'puzzle', 'exploration', 'rpg-lite'],
description: 'Adventurous character with backpack and utility belt',
avatarPosition: { x: 0.5, y: 0.5 },
avatarScale: 1.0,
lightSource: { x: -0.5, y: -1 }, // Top-left light
renderEffectsBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
const groundY = y + (avatar.height || 100) * scale / 2;
const elevation = state.jumping || state.falling ? (state.elevation || 30) : 0;
// Dynamic shadow based on light source
const shadowOffsetX = -this.lightSource.x * 20 * scale;
const shadowOffsetY = -this.lightSource.y * 5 * scale;
renderShadow(
ctx,
x + shadowOffsetX * (1 - elevation / 100),
groundY + shadowOffsetY,
55 * scale,
15 * scale,
elevation
);
// Subtle heroic glow when idle
if (state.idle || (!state.running && !state.jumping && !state.falling)) {
renderGlow(ctx, x, y, 100 * scale, 140 * scale, time, {
color: 'rgba(255, 215, 0, 0.08)',
pulseSpeed: 1.5,
pulseAmount: 0.15
});
}
// Determination lines when running
if (state.running) {
const direction = state.facingRight ? 1 : -1;
renderSpeedLines(ctx, x, y - 20 * scale, scale, direction, 0.4, {
lineCount: 4,
maxLength: 30,
color: 'rgba(255, 200, 100, 0.4)',
spread: 50
});
}
},
renderCostumeBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
// Small backpack
renderBackpack(ctx, x, y, scale, state.facingRight !== false);
},
renderCostumeFront: function(ctx, avatar, x, y, scale, transforms, time, state) {
// Thin utility belt
const waistY = y + 15 * scale;
renderUtilityBelt(ctx, x, waistY, scale * 0.8);
},
renderEffectsOver: function(ctx, avatar, x, y, scale, transforms, time, state) {
const headY = y - (avatar.height || 100) * scale / 2 + 15 * scale;
// Cape-like motion effect when jumping (no actual cape)
if (state.jumping || state.falling) {
const direction = state.facingRight ? -1 : 1;
ctx.save();
ctx.strokeStyle = 'rgba(100, 150, 255, 0.3)';
ctx.lineWidth = 3 * scale;
ctx.lineCap = 'round';
for (let i = 0; i < 3; i++) {
const startX = x + direction * 20 * scale;
const startY = y - 20 * scale + i * 15 * scale;
const waveTime = time * 5 + i * 0.5;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.quadraticCurveTo(
startX + direction * 20 * scale,
startY + Math.sin(waveTime) * 10 * scale,
startX + direction * 35 * scale,
startY + 10 * scale
);
ctx.stroke();
}
ctx.restore();
}
// Discovery sparkle/shine
if (state.discovering && state.discoverTime !== undefined) {
renderSparkle(ctx, x + 40 * scale, y - 20 * scale, scale * 1.2, state.discoverTime, {
count: 6,
spread: 30,
color: '#FFFF00',
maxSize: 8,
twinkleSpeed: 6
});
}
// Damage flash when hurt
if (state.hurt && state.hurtTime !== undefined && state.hurtTime < 0.4) {
renderFlash(ctx, x, y, 90 * scale, 130 * scale, state.hurtTime, {
color: 'rgba(255, 50, 50, 0.4)',
duration: 0.4,
pulseCount: 3
});
}
},
stateEffects: {
idle: { type: 'glow', continuous: true },
running: { type: 'determination-lines', continuous: true },
jumping: { type: 'cape-motion', continuous: true },
discovering: { type: 'sparkle', duration: 1.5 },
hurt: { type: 'damage-flash', duration: 0.4 }
}
};
const runner = {
id: 'runner',
name: 'Speed Runner',
category: 'pure',
avatarMode: 'FULL_BODY',
animations: ['idle', 'run', 'jump', 'slide', 'fall', 'hurt', 'celebrate'],
defaultAnimation: 'run',
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
layerOrder: ['background-effects', 'avatar', 'foreground-effects'],
recommendedFor: ['runner', 'endless-runner', 'racing', 'speed'],
description: 'Streamlined character optimized for endless runner games',
avatarPosition: { x: 0.4, y: 0.5 }, // Slightly left to show more forward space
avatarScale: 1.0,
renderEffectsBehind: function(ctx, avatar, x, y, scale, transforms, time, state) {
const groundY = y + (avatar.height || 100) * scale / 2;
const speed = state.speed || 0.5;
const elevation = state.jumping || state.falling ? (state.elevation || 30) : 0;
// Motion-blurred shadow
ctx.save();
const shadowCount = Math.ceil(speed * 3);
for (let i = shadowCount; i >= 0; i--) {
const offsetX = i * 8 * scale;
const alpha = 0.2 * (1 - i / (shadowCount + 1)) * (1 - elevation / 100);
ctx.beginPath();
ctx.ellipse(x - offsetX, groundY, (50 - i * 5) * scale / 2, (12 - i * 2) * scale / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = `rgba(0, 0, 0, ${alpha})`;
ctx.fill();
}
ctx.restore();
// Constant speed lines (intensity based on speed)
renderSpeedLines(ctx, x, y, scale, 1, speed, {
lineCount: Math.ceil(6 + speed * 6),
maxLength: 40 + speed * 40,
color: 'rgba(255, 255, 255, 0.6)',
spread: 90
});
// Intensified speed effect when accelerating
if (state.accelerating) {
renderSpeedLines(ctx, x - 20 * scale, y, scale, 1, speed * 1.5, {
lineCount: 10,
maxLength: 80,
color: 'rgba(255, 200, 100, 0.5)',
spread: 100
});
}
// Air trail when jumping
if (state.jumping || state.falling) {
const trailCount = 5;
ctx.save();
for (let i = 1; i <= trailCount; i++) {
const trailX = x - i * 15 * scale;
const trailY = y + (state.falling ? -1 : 1) * i * 8 * scale;
const alpha = 0.3 * (1 - i / (trailCount + 1));
ctx.beginPath();
ctx.ellipse(trailX, trailY, 15 * scale, 25 * scale, 0, 0, Math.PI * 2);
ctx.fillStyle = `rgba(200, 220, 255, ${alpha})`;
ctx.fill();
}
ctx.restore();
}
},
renderEffectsOver: function(ctx, avatar, x, y, scale, transforms, time, state) {
const headY = y - (avatar.height || 100) * scale / 2;
const groundY = y + (avatar.height || 100) * scale / 2;
// Wind-swept hair effect
const windIntensity = (state.speed || 0.5) + (state.accelerating ? 0.3 : 0);
renderWindEffect(ctx, x, headY + 10 * scale, scale, windIntensity, time);
// Ground friction sparks when sliding
if (state.sliding && state.slideTime !== undefined) {
renderSparks(ctx, x, groundY, scale, 1, state.slideTime, {
count: 10,
color: '#FFA500',
highlightColor: '#FFFF00'
});
}
// Pickup sparkle when collecting
if (state.collecting && state.collectTime !== undefined && state.collectTime < 0.5) {
renderSparkle(ctx, x + 30 * scale, y - 20 * scale, scale, state.collectTime, {
count: 8,
spread: 25,
color: '#00FF00',
maxSize: 10,
twinkleSpeed: 8
});
}
// Stumble effect when hurt
if (state.hurt && state.hurtTime !== undefined && state.hurtTime < 0.6) {
// Shake effect
const shakeIntensity = Math.max(0, 1 - state.hurtTime / 0.6);
const shakeX = Math.sin(state.hurtTime * 50) * 5 * scale * shakeIntensity;
const shakeY = Math.sin(state.hurtTime * 40) * 3 * scale * shakeIntensity;
// Motion lines indicating stumble
ctx.save();
ctx.strokeStyle = `rgba(255, 100, 100, ${shakeIntensity * 0.5})`;
ctx.lineWidth = 2 * scale;
ctx.setLineDash([5 * scale, 5 * scale]);
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2 + state.hurtTime * 10;
const radius = 40 * scale * shakeIntensity;
ctx.beginPath();
ctx.moveTo(x + shakeX, y + shakeY);
ctx.lineTo(
x + shakeX + Math.cos(angle) * radius,
y + shakeY + Math.sin(angle) * radius
);
ctx.stroke();
}
ctx.restore();
// Stars for big hits
if (state.bigHit) {
renderStars(ctx, x, headY + 10 * scale, scale, 3, time);
}
}
},
stateEffects: {
running: { type: 'speed-lines', continuous: true },
accelerating: { type: 'intensified-speed', continuous: true },
jumping: { type: 'air-trail', continuous: true },
sliding: { type: 'sparks', continuous: true },
collecting: { type: 'sparkle', duration: 0.5 },
hurt: { type: 'stumble', duration: 0.6 }
}
};
// ============================================
// PUBLIC API
// ============================================
const CONTEXTS = [
platformerStandard,
sportsPlayer,
adventureHero,
runner
];
window.PureContexts = {
CONTEXTS: CONTEXTS,
/**
* Get a pure context by ID
* @param {string} id - Context ID
* @returns {Object|null} Context object or null if not found
*/
getById: function(id) {
return CONTEXTS.find(function(ctx) { return ctx.id === id; }) || null;
},
/**
* Get all pure contexts
* @returns {Array} Array of all pure contexts
*/
getAll: function() {
return CONTEXTS.slice();
},
/**
* Get contexts recommended for a specific game type
* @param {string} gameType - Game type to match
* @returns {Array} Array of matching contexts
*/
getRecommendedFor: function(gameType) {
var type = gameType.toLowerCase();
return CONTEXTS.filter(function(ctx) {
return ctx.recommendedFor.some(function(rec) {
return rec.toLowerCase() === type || type.indexOf(rec.toLowerCase()) !== -1;
});
});
},
// Shared effect renderers for reuse by other modules
effects: {
dust: renderDust,
speedLines: renderSpeedLines,
stars: renderStars,
confetti: renderConfetti,
shadow: renderShadow,
sparkle: renderSparkle,
flash: renderFlash,
glow: renderGlow,
motionBlur: renderMotionBlur,
sparks: renderSparks,
windEffect: renderWindEffect
},
// Shared costume renderers for reuse
costumes: {
backpack: renderBackpack,
utilityBelt: renderUtilityBelt,
sweatband: renderSweatband,
jerseyNumber: renderJerseyNumber
}
};
})();