1800 lines
63 KiB
JavaScript
1800 lines
63 KiB
JavaScript
(function() {
|
|
'use strict';
|
|
|
|
// ============================================================
|
|
// VEHICLE CONTEXTS - Avatar placement in vehicle scenarios
|
|
// ============================================================
|
|
|
|
const CONTEXTS = [
|
|
|
|
// ============================================================
|
|
// 1. SPACESHIP COCKPIT
|
|
// ============================================================
|
|
{
|
|
id: 'spaceship-cockpit',
|
|
name: 'Spaceship Cockpit',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_ONLY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['space', 'shooter', 'arcade'],
|
|
description: 'Pilot a spacecraft through space',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.4 },
|
|
avatarScale: 1.0,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Dark space background
|
|
ctx.fillStyle = '#0a0a1a';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Starfield
|
|
var starCount = 50;
|
|
for (var i = 0; i < starCount; i++) {
|
|
var seed = i * 7919;
|
|
var x = ((seed * 13) % 1000) / 1000 * width;
|
|
var baseY = ((seed * 17) % 1000) / 1000 * height;
|
|
var speed = 0.5 + ((seed * 23) % 100) / 100;
|
|
var y = (baseY + time * speed * 100) % height;
|
|
var brightness = 0.3 + ((seed * 31) % 70) / 100;
|
|
var size = 1 + ((seed * 37) % 3);
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, ' + brightness + ')';
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Cockpit interior frame
|
|
ctx.fillStyle = '#1a1a2e';
|
|
ctx.fillRect(0, 0, width * 0.15, height);
|
|
ctx.fillRect(width * 0.85, 0, width * 0.15, height);
|
|
ctx.fillRect(0, height * 0.75, width, height * 0.25);
|
|
|
|
// Interior panel details
|
|
ctx.fillStyle = '#2a2a4e';
|
|
for (var j = 0; j < 3; j++) {
|
|
ctx.fillRect(width * 0.02, height * 0.1 + j * height * 0.2, width * 0.08, height * 0.12);
|
|
ctx.fillRect(width * 0.9, height * 0.1 + j * height * 0.2, width * 0.08, height * 0.12);
|
|
}
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Cockpit glass with slight blue tint
|
|
ctx.strokeStyle = 'rgba(100, 150, 255, 0.3)';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.4, width * 0.35, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// HUD overlay elements
|
|
ctx.strokeStyle = 'rgba(0, 255, 200, 0.5)';
|
|
ctx.lineWidth = 2;
|
|
|
|
// Targeting reticle
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.35, width * 0.08, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.5, height * 0.25);
|
|
ctx.lineTo(width * 0.5, height * 0.3);
|
|
ctx.moveTo(width * 0.5, height * 0.4);
|
|
ctx.lineTo(width * 0.5, height * 0.45);
|
|
ctx.moveTo(width * 0.4, height * 0.35);
|
|
ctx.lineTo(width * 0.45, height * 0.35);
|
|
ctx.moveTo(width * 0.55, height * 0.35);
|
|
ctx.lineTo(width * 0.6, height * 0.35);
|
|
ctx.stroke();
|
|
|
|
// Control panel at bottom
|
|
var gradient = ctx.createLinearGradient(0, height * 0.8, 0, height);
|
|
gradient.addColorStop(0, '#2a2a4e');
|
|
gradient.addColorStop(1, '#1a1a2e');
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, height * 0.8, width, height * 0.2);
|
|
|
|
// Control buttons
|
|
var colors = ['#ff3333', '#33ff33', '#3333ff', '#ffff33'];
|
|
for (var i = 0; i < 4; i++) {
|
|
var blinkPhase = Math.sin(time * 3 + i) * 0.3 + 0.7;
|
|
ctx.fillStyle = colors[i];
|
|
ctx.globalAlpha = blinkPhase;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.25 + i * width * 0.15, height * 0.9, width * 0.02, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
|
|
// Speed indicator
|
|
ctx.fillStyle = 'rgba(0, 255, 200, 0.8)';
|
|
ctx.font = (width * 0.04) + 'px monospace';
|
|
ctx.fillText('SPD: ' + Math.floor(500 + Math.sin(time) * 50), width * 0.05, height * 0.88);
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
boost: { type: 'particles', direction: 'back' },
|
|
damage: { type: 'shake', intensity: 5 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 2. RACE CAR
|
|
// ============================================================
|
|
{
|
|
id: 'race-car',
|
|
name: 'Race Car',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_AND_SHOULDERS',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'turn-left', 'turn-right'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['racing', 'driving', 'arcade'],
|
|
description: 'Race at high speed on the track',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.35 },
|
|
avatarScale: 0.9,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Sky gradient
|
|
var skyGradient = ctx.createLinearGradient(0, 0, 0, height * 0.4);
|
|
skyGradient.addColorStop(0, '#87ceeb');
|
|
skyGradient.addColorStop(1, '#e0f0ff');
|
|
ctx.fillStyle = skyGradient;
|
|
ctx.fillRect(0, 0, width, height * 0.4);
|
|
|
|
// Road rushing past
|
|
var roadGradient = ctx.createLinearGradient(0, height * 0.4, 0, height);
|
|
roadGradient.addColorStop(0, '#555555');
|
|
roadGradient.addColorStop(1, '#333333');
|
|
ctx.fillStyle = roadGradient;
|
|
ctx.fillRect(0, height * 0.4, width, height * 0.6);
|
|
|
|
// Road lines (animated)
|
|
ctx.fillStyle = '#ffffff';
|
|
var lineOffset = (time * 300) % 80;
|
|
for (var i = -1; i < 8; i++) {
|
|
var y = height * 0.5 + i * 80 - lineOffset;
|
|
var lineWidth = 5 + (y - height * 0.4) * 0.05;
|
|
ctx.fillRect(width * 0.48, y, width * 0.04, 40);
|
|
}
|
|
|
|
// Speed lines on sides
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.lineWidth = 2;
|
|
for (var j = 0; j < 10; j++) {
|
|
var lineY = height * 0.5 + (j * 30 + time * 200) % (height * 0.5);
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.1, lineY);
|
|
ctx.lineTo(width * 0.2, lineY + 20);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.9, lineY);
|
|
ctx.lineTo(width * 0.8, lineY + 20);
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Dashboard
|
|
var dashGradient = ctx.createLinearGradient(0, height * 0.7, 0, height);
|
|
dashGradient.addColorStop(0, '#1a1a1a');
|
|
dashGradient.addColorStop(1, '#0a0a0a');
|
|
ctx.fillStyle = dashGradient;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height * 0.75);
|
|
ctx.quadraticCurveTo(width * 0.5, height * 0.65, width, height * 0.75);
|
|
ctx.lineTo(width, height);
|
|
ctx.lineTo(0, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Steering wheel
|
|
ctx.strokeStyle = '#333333';
|
|
ctx.lineWidth = width * 0.03;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.95, width * 0.15, Math.PI * 0.8, Math.PI * 2.2);
|
|
ctx.stroke();
|
|
|
|
ctx.fillStyle = '#222222';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.95, width * 0.06, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Side mirrors
|
|
ctx.fillStyle = '#1a1a1a';
|
|
ctx.fillRect(0, height * 0.3, width * 0.12, height * 0.15);
|
|
ctx.fillRect(width * 0.88, height * 0.3, width * 0.12, height * 0.15);
|
|
|
|
// Mirror reflections (road)
|
|
ctx.fillStyle = '#444444';
|
|
ctx.fillRect(width * 0.01, height * 0.32, width * 0.1, height * 0.11);
|
|
ctx.fillRect(width * 0.89, height * 0.32, width * 0.1, height * 0.11);
|
|
|
|
// Speedometer
|
|
ctx.fillStyle = '#111111';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.25, height * 0.85, width * 0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#ff3300';
|
|
ctx.lineWidth = 2;
|
|
var speedAngle = Math.PI * 0.75 + (0.5 + Math.sin(time * 0.5) * 0.2) * Math.PI;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.25, height * 0.85);
|
|
ctx.lineTo(
|
|
width * 0.25 + Math.cos(speedAngle) * width * 0.06,
|
|
height * 0.85 + Math.sin(speedAngle) * width * 0.06
|
|
);
|
|
ctx.stroke();
|
|
|
|
// RPM gauge
|
|
ctx.fillStyle = '#111111';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.75, height * 0.85, width * 0.08, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#00ff00';
|
|
ctx.lineWidth = 2;
|
|
var rpmAngle = Math.PI * 0.75 + (0.3 + Math.sin(time * 2) * 0.3) * Math.PI;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.75, height * 0.85);
|
|
ctx.lineTo(
|
|
width * 0.75 + Math.cos(rpmAngle) * width * 0.06,
|
|
height * 0.85 + Math.sin(rpmAngle) * width * 0.06
|
|
);
|
|
ctx.stroke();
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
accelerate: { type: 'blur', direction: 'vertical' },
|
|
drift: { type: 'particles', direction: 'side' },
|
|
damage: { type: 'shake', intensity: 8 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 3. AIRPLANE
|
|
// ============================================================
|
|
{
|
|
id: 'airplane',
|
|
name: 'Airplane',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_AND_SHOULDERS',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['flight', 'simulation', 'arcade'],
|
|
description: 'Fly through the clouds as a pilot',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.4 },
|
|
avatarScale: 0.85,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Sky gradient
|
|
var skyGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
skyGradient.addColorStop(0, '#1e90ff');
|
|
skyGradient.addColorStop(0.5, '#87ceeb');
|
|
skyGradient.addColorStop(1, '#b0e0e6');
|
|
ctx.fillStyle = skyGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Clouds streaming past
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
for (var i = 0; i < 8; i++) {
|
|
var seed = i * 3571;
|
|
var baseX = ((seed * 13) % 1000) / 1000 * width * 1.5;
|
|
var x = (baseX - time * 150) % (width * 1.5) - width * 0.25;
|
|
var y = ((seed * 17) % 1000) / 1000 * height * 0.6;
|
|
var cloudWidth = 60 + ((seed * 23) % 80);
|
|
var cloudHeight = 30 + ((seed * 29) % 30);
|
|
|
|
// Draw cloud puffs
|
|
for (var j = 0; j < 4; j++) {
|
|
var puffX = x + j * cloudWidth * 0.25;
|
|
var puffY = y + Math.sin(j * 1.5) * cloudHeight * 0.3;
|
|
var puffR = cloudHeight * 0.5 + ((seed * j) % 10);
|
|
ctx.beginPath();
|
|
ctx.arc(puffX, puffY, puffR, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Cockpit interior sides
|
|
ctx.fillStyle = '#2a3a4a';
|
|
ctx.fillRect(0, 0, width * 0.1, height);
|
|
ctx.fillRect(width * 0.9, 0, width * 0.1, height);
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Windshield frame
|
|
ctx.strokeStyle = '#4a5a6a';
|
|
ctx.lineWidth = width * 0.04;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.1, 0);
|
|
ctx.lineTo(width * 0.2, height * 0.7);
|
|
ctx.lineTo(width * 0.8, height * 0.7);
|
|
ctx.lineTo(width * 0.9, 0);
|
|
ctx.stroke();
|
|
|
|
// Center divider
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.5, 0);
|
|
ctx.lineTo(width * 0.5, height * 0.15);
|
|
ctx.stroke();
|
|
|
|
// Instrument panel
|
|
var panelGradient = ctx.createLinearGradient(0, height * 0.7, 0, height);
|
|
panelGradient.addColorStop(0, '#3a4a5a');
|
|
panelGradient.addColorStop(1, '#2a3a4a');
|
|
ctx.fillStyle = panelGradient;
|
|
ctx.fillRect(0, height * 0.7, width, height * 0.3);
|
|
|
|
// Instruments
|
|
var instrumentPositions = [0.2, 0.35, 0.5, 0.65, 0.8];
|
|
for (var i = 0; i < instrumentPositions.length; i++) {
|
|
ctx.fillStyle = '#1a2a3a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * instrumentPositions[i], height * 0.82, width * 0.055, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Instrument needle
|
|
ctx.strokeStyle = '#ffffff';
|
|
ctx.lineWidth = 1;
|
|
var angle = Math.PI * (0.75 + Math.sin(time + i) * 0.3);
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * instrumentPositions[i], height * 0.82);
|
|
ctx.lineTo(
|
|
width * instrumentPositions[i] + Math.cos(angle) * width * 0.04,
|
|
height * 0.82 + Math.sin(angle) * width * 0.04
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Yoke/control column
|
|
ctx.fillStyle = '#2a2a2a';
|
|
ctx.fillRect(width * 0.45, height * 0.85, width * 0.1, height * 0.15);
|
|
|
|
ctx.fillStyle = '#3a3a3a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, height * 0.88, width * 0.12, width * 0.04, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Altitude display
|
|
ctx.fillStyle = '#00ff00';
|
|
ctx.font = (width * 0.03) + 'px monospace';
|
|
ctx.fillText('ALT: ' + Math.floor(35000 + Math.sin(time * 0.5) * 500), width * 0.05, height * 0.95);
|
|
ctx.fillText('HDG: ' + Math.floor(270 + Math.sin(time * 0.3) * 10), width * 0.75, height * 0.95);
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
turbulence: { type: 'shake', intensity: 3 },
|
|
dive: { type: 'blur', direction: 'vertical' },
|
|
damage: { type: 'shake', intensity: 10 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 4. FLAPPY STYLE
|
|
// ============================================================
|
|
{
|
|
id: 'flappy-style',
|
|
name: 'Flappy Bird Style',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_ONLY',
|
|
animations: ['idle', 'flap', 'fall', 'hurt'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: false, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['flappy', 'casual', 'arcade', 'mobile'],
|
|
description: 'Flap through obstacles as a flying character',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.5 },
|
|
avatarScale: 1.2,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Transparent background - game provides its own
|
|
ctx.clearRect(0, 0, width, height);
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Optional wing sprites
|
|
var flapOffset = Math.sin(time * 15) * 0.15;
|
|
|
|
// Left wing
|
|
ctx.fillStyle = 'rgba(255, 200, 100, 0.9)';
|
|
ctx.save();
|
|
ctx.translate(width * 0.25, height * 0.5);
|
|
ctx.rotate(-0.3 + flapOffset);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, width * 0.15, width * 0.06, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
// Right wing
|
|
ctx.save();
|
|
ctx.translate(width * 0.75, height * 0.5);
|
|
ctx.rotate(0.3 - flapOffset);
|
|
ctx.beginPath();
|
|
ctx.ellipse(0, 0, width * 0.15, width * 0.06, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
|
|
// Wing details
|
|
ctx.strokeStyle = 'rgba(200, 150, 50, 0.7)';
|
|
ctx.lineWidth = 2;
|
|
ctx.save();
|
|
ctx.translate(width * 0.25, height * 0.5);
|
|
ctx.rotate(-0.3 + flapOffset);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-width * 0.1, 0);
|
|
ctx.lineTo(width * 0.1, 0);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
|
|
ctx.save();
|
|
ctx.translate(width * 0.75, height * 0.5);
|
|
ctx.rotate(0.3 - flapOffset);
|
|
ctx.beginPath();
|
|
ctx.moveTo(-width * 0.1, 0);
|
|
ctx.lineTo(width * 0.1, 0);
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
flap: { type: 'blur', direction: 'vertical', intensity: 2 },
|
|
fall: { type: 'stretch', direction: 'down' },
|
|
damage: { type: 'flash', color: '#ff0000' }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 5. TANK
|
|
// ============================================================
|
|
{
|
|
id: 'tank',
|
|
name: 'Tank',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_ONLY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'aim'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['military', 'shooter', 'strategy', 'arcade'],
|
|
description: 'Command a battle tank',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.35 },
|
|
avatarScale: 0.8,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Military green interior
|
|
var interiorGradient = ctx.createRadialGradient(
|
|
width * 0.5, height * 0.5, 0,
|
|
width * 0.5, height * 0.5, width * 0.6
|
|
);
|
|
interiorGradient.addColorStop(0, '#3a4a2a');
|
|
interiorGradient.addColorStop(1, '#2a3a1a');
|
|
ctx.fillStyle = interiorGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Rivets and panels
|
|
ctx.fillStyle = '#4a5a3a';
|
|
for (var i = 0; i < 4; i++) {
|
|
for (var j = 0; j < 4; j++) {
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
width * 0.1 + i * width * 0.05,
|
|
height * 0.1 + j * height * 0.25,
|
|
4, 0, Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(
|
|
width * 0.9 - i * width * 0.05,
|
|
height * 0.1 + j * height * 0.25,
|
|
4, 0, Math.PI * 2
|
|
);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Interior screens
|
|
ctx.fillStyle = '#1a2a1a';
|
|
ctx.fillRect(width * 0.05, height * 0.6, width * 0.2, height * 0.15);
|
|
ctx.fillRect(width * 0.75, height * 0.6, width * 0.2, height * 0.15);
|
|
|
|
// Screen static
|
|
ctx.fillStyle = '#2a4a2a';
|
|
for (var k = 0; k < 20; k++) {
|
|
var sx = width * 0.07 + Math.random() * width * 0.16;
|
|
var sy = height * 0.62 + Math.random() * height * 0.11;
|
|
ctx.fillRect(sx, sy, 2, 2);
|
|
}
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Tank hatch frame (circular opening)
|
|
ctx.strokeStyle = '#5a6a4a';
|
|
ctx.lineWidth = width * 0.08;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.35, width * 0.28, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Hatch lip detail
|
|
ctx.strokeStyle = '#4a5a3a';
|
|
ctx.lineWidth = width * 0.02;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.35, width * 0.32, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Hatch bolts
|
|
ctx.fillStyle = '#3a4a2a';
|
|
for (var i = 0; i < 8; i++) {
|
|
var angle = (i / 8) * Math.PI * 2;
|
|
var bx = width * 0.5 + Math.cos(angle) * width * 0.3;
|
|
var by = height * 0.35 + Math.sin(angle) * width * 0.3;
|
|
ctx.beginPath();
|
|
ctx.arc(bx, by, 6, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Periscope overlay (top)
|
|
ctx.fillStyle = '#2a3a1a';
|
|
ctx.fillRect(width * 0.35, 0, width * 0.3, height * 0.1);
|
|
|
|
ctx.fillStyle = '#1a2a0a';
|
|
ctx.fillRect(width * 0.38, height * 0.02, width * 0.24, height * 0.06);
|
|
|
|
// Periscope view
|
|
ctx.fillStyle = '#87ceeb';
|
|
ctx.fillRect(width * 0.4, height * 0.03, width * 0.2, height * 0.04);
|
|
|
|
// Crosshair in periscope
|
|
ctx.strokeStyle = '#ff0000';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.5, height * 0.03);
|
|
ctx.lineTo(width * 0.5, height * 0.07);
|
|
ctx.moveTo(width * 0.4, height * 0.05);
|
|
ctx.lineTo(width * 0.6, height * 0.05);
|
|
ctx.stroke();
|
|
|
|
// Control panel at bottom
|
|
ctx.fillStyle = '#3a4a2a';
|
|
ctx.fillRect(0, height * 0.8, width, height * 0.2);
|
|
|
|
// Warning lights
|
|
var warningOn = Math.sin(time * 5) > 0;
|
|
ctx.fillStyle = warningOn ? '#ff3333' : '#331111';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.2, height * 0.9, width * 0.025, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#33ff33';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.35, height * 0.9, width * 0.025, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Ammo counter
|
|
ctx.fillStyle = '#ffcc00';
|
|
ctx.font = (width * 0.05) + 'px monospace';
|
|
ctx.fillText('AMMO: 24', width * 0.6, height * 0.92);
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
fire: { type: 'flash', color: '#ffff00', intensity: 10 },
|
|
rumble: { type: 'shake', intensity: 4 },
|
|
damage: { type: 'shake', intensity: 12 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 6. PACMAN STYLE
|
|
// ============================================================
|
|
{
|
|
id: 'pacman-style',
|
|
name: 'Pac-Man Style',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_ONLY',
|
|
animations: ['idle', 'chomp', 'hurt', 'power'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: false, headwear: false, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['maze', 'arcade', 'retro', 'casual'],
|
|
description: 'Navigate mazes and eat pellets',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.5 },
|
|
avatarScale: 1.0,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Fully transparent - game provides maze
|
|
ctx.clearRect(0, 0, width, height);
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// No foreground elements - avatar IS the character
|
|
// The chomp animation is handled by avatar animations
|
|
|
|
// Optional: subtle mouth effect overlay during chomp
|
|
var chompPhase = Math.abs(Math.sin(time * 12));
|
|
if (chompPhase < 0.3) {
|
|
// Mouth "bite" effect - darkens bottom portion slightly
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.5, height * 0.5);
|
|
ctx.lineTo(width * 0.8, height * 0.6);
|
|
ctx.lineTo(width * 0.8, height * 0.7);
|
|
ctx.lineTo(width * 0.5, height * 0.6);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
chomp: { type: 'scale', pulseRate: 12 },
|
|
power: { type: 'glow', color: '#00ffff' },
|
|
damage: { type: 'flash', color: '#ff0000' }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 7. HOVERBOARD
|
|
// ============================================================
|
|
{
|
|
id: 'hoverboard',
|
|
name: 'Hoverboard',
|
|
category: 'vehicle',
|
|
avatarMode: 'UPPER_BODY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'lean-left', 'lean-right', 'jump'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'foreground', 'avatar'],
|
|
recommendedFor: ['runner', 'racing', 'arcade', 'endless'],
|
|
description: 'Ride a futuristic hoverboard',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.35 },
|
|
avatarScale: 0.75,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Ground/environment rushing beneath
|
|
var groundGradient = ctx.createLinearGradient(0, height * 0.6, 0, height);
|
|
groundGradient.addColorStop(0, '#4a4a5a');
|
|
groundGradient.addColorStop(1, '#2a2a3a');
|
|
ctx.fillStyle = groundGradient;
|
|
ctx.fillRect(0, height * 0.6, width, height * 0.4);
|
|
|
|
// Speed lines on ground
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
|
|
ctx.lineWidth = 3;
|
|
for (var i = 0; i < 12; i++) {
|
|
var seed = i * 1847;
|
|
var lx = ((seed * 13) % 1000) / 1000 * width;
|
|
var ly = height * 0.7 + ((seed * 17) % 1000) / 1000 * height * 0.25;
|
|
var lineLen = 30 + ((seed * 23) % 50);
|
|
var offset = (time * 400 + seed) % (width * 1.5);
|
|
var drawX = lx - offset;
|
|
if (drawX < -lineLen) drawX += width * 1.5;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(drawX, ly);
|
|
ctx.lineTo(drawX + lineLen, ly);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Shadow beneath board
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, height * 0.85, width * 0.25, height * 0.03, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Hoverboard
|
|
var hoverOffset = Math.sin(time * 4) * 3;
|
|
var boardY = height * 0.72 + hoverOffset;
|
|
|
|
// Board shadow/glow
|
|
var glowGradient = ctx.createRadialGradient(
|
|
width * 0.5, boardY + 15, 0,
|
|
width * 0.5, boardY + 15, width * 0.3
|
|
);
|
|
glowGradient.addColorStop(0, 'rgba(0, 200, 255, 0.6)');
|
|
glowGradient.addColorStop(0.5, 'rgba(0, 200, 255, 0.2)');
|
|
glowGradient.addColorStop(1, 'rgba(0, 200, 255, 0)');
|
|
ctx.fillStyle = glowGradient;
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, boardY + 15, width * 0.3, height * 0.06, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Board body
|
|
var boardGradient = ctx.createLinearGradient(0, boardY - 10, 0, boardY + 10);
|
|
boardGradient.addColorStop(0, '#3a3a4a');
|
|
boardGradient.addColorStop(0.5, '#5a5a6a');
|
|
boardGradient.addColorStop(1, '#2a2a3a');
|
|
ctx.fillStyle = boardGradient;
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, boardY, width * 0.28, height * 0.04, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Board top surface
|
|
ctx.fillStyle = '#6a6a7a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, boardY - 3, width * 0.25, height * 0.025, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Board lights
|
|
var lightPhase = (time * 3) % 1;
|
|
ctx.fillStyle = 'rgba(0, 255, 255, ' + (0.5 + lightPhase * 0.5) + ')';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.3, boardY, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.7, boardY, 4, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Hover jets
|
|
ctx.fillStyle = 'rgba(0, 200, 255, 0.8)';
|
|
var jetFlicker = 0.7 + Math.random() * 0.3;
|
|
ctx.globalAlpha = jetFlicker;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.35, boardY + 8);
|
|
ctx.lineTo(width * 0.32, boardY + 20 + hoverOffset);
|
|
ctx.lineTo(width * 0.38, boardY + 20 + hoverOffset);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.65, boardY + 8);
|
|
ctx.lineTo(width * 0.62, boardY + 20 + hoverOffset);
|
|
ctx.lineTo(width * 0.68, boardY + 20 + hoverOffset);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.globalAlpha = 1;
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: { type: 'bob', amplitude: 3, rate: 4 },
|
|
boost: { type: 'particles', direction: 'back', color: '#00ffff' },
|
|
jump: { type: 'stretch', direction: 'up' },
|
|
damage: { type: 'shake', intensity: 6 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 8. JETPACK
|
|
// ============================================================
|
|
{
|
|
id: 'jetpack',
|
|
name: 'Jetpack',
|
|
category: 'vehicle',
|
|
avatarMode: 'UPPER_BODY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'boost'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'jetpack', 'avatar', 'foreground'],
|
|
recommendedFor: ['flying', 'endless', 'arcade', 'shooter'],
|
|
description: 'Fly with a jetpack strapped to your back',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.4 },
|
|
avatarScale: 0.7,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Sky gradient
|
|
var skyGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
skyGradient.addColorStop(0, '#1e90ff');
|
|
skyGradient.addColorStop(1, '#87ceeb');
|
|
ctx.fillStyle = skyGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Clouds below
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
for (var i = 0; i < 5; i++) {
|
|
var seed = i * 2347;
|
|
var cx = ((seed * 13) % 1000) / 1000 * width * 1.5 - width * 0.25;
|
|
var cy = height * 0.7 + ((seed * 17) % 1000) / 1000 * height * 0.2;
|
|
var cw = 80 + ((seed * 23) % 60);
|
|
|
|
// Animate clouds moving down
|
|
cy = (cy + time * 30) % (height * 0.5) + height * 0.6;
|
|
|
|
for (var j = 0; j < 4; j++) {
|
|
ctx.beginPath();
|
|
ctx.arc(cx + j * cw * 0.3, cy + Math.sin(j) * 10, cw * 0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// JETPACK (rendered behind avatar)
|
|
var packX = width * 0.5;
|
|
var packY = height * 0.45;
|
|
|
|
// Main pack body
|
|
var packGradient = ctx.createLinearGradient(packX - 30, 0, packX + 30, 0);
|
|
packGradient.addColorStop(0, '#4a4a5a');
|
|
packGradient.addColorStop(0.5, '#6a6a7a');
|
|
packGradient.addColorStop(1, '#4a4a5a');
|
|
ctx.fillStyle = packGradient;
|
|
|
|
// Pack shape
|
|
ctx.beginPath();
|
|
ctx.roundRect(packX - width * 0.12, packY - height * 0.05, width * 0.24, height * 0.25, 8);
|
|
ctx.fill();
|
|
|
|
// Pack details
|
|
ctx.fillStyle = '#3a3a4a';
|
|
ctx.fillRect(packX - width * 0.08, packY, width * 0.16, height * 0.02);
|
|
ctx.fillRect(packX - width * 0.08, packY + height * 0.08, width * 0.16, height * 0.02);
|
|
|
|
// Fuel gauge
|
|
ctx.fillStyle = '#2a2a3a';
|
|
ctx.fillRect(packX - width * 0.06, packY + height * 0.12, width * 0.12, height * 0.04);
|
|
ctx.fillStyle = '#00ff00';
|
|
var fuelLevel = 0.7 + Math.sin(time * 0.5) * 0.1;
|
|
ctx.fillRect(packX - width * 0.055, packY + height * 0.125, width * 0.11 * fuelLevel, height * 0.03);
|
|
|
|
// Exhaust nozzles
|
|
ctx.fillStyle = '#5a5a6a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(packX - width * 0.08, packY + height * 0.2);
|
|
ctx.lineTo(packX - width * 0.1, packY + height * 0.28);
|
|
ctx.lineTo(packX - width * 0.04, packY + height * 0.28);
|
|
ctx.lineTo(packX - width * 0.04, packY + height * 0.2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(packX + width * 0.04, packY + height * 0.2);
|
|
ctx.lineTo(packX + width * 0.04, packY + height * 0.28);
|
|
ctx.lineTo(packX + width * 0.1, packY + height * 0.28);
|
|
ctx.lineTo(packX + width * 0.08, packY + height * 0.2);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Jet flames (rendered in front layer for visibility)
|
|
var packX = width * 0.5;
|
|
var packY = height * 0.45;
|
|
var flameHeight = height * 0.15 + Math.random() * height * 0.05;
|
|
|
|
// Left flame
|
|
var flameGradient1 = ctx.createLinearGradient(0, packY + height * 0.28, 0, packY + height * 0.28 + flameHeight);
|
|
flameGradient1.addColorStop(0, 'rgba(255, 200, 50, 0.9)');
|
|
flameGradient1.addColorStop(0.3, 'rgba(255, 100, 0, 0.8)');
|
|
flameGradient1.addColorStop(1, 'rgba(255, 50, 0, 0)');
|
|
ctx.fillStyle = flameGradient1;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(packX - width * 0.09, packY + height * 0.28);
|
|
ctx.quadraticCurveTo(
|
|
packX - width * 0.07 + Math.sin(time * 20) * 5,
|
|
packY + height * 0.35,
|
|
packX - width * 0.07,
|
|
packY + height * 0.28 + flameHeight
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
packX - width * 0.07 + Math.sin(time * 25) * 5,
|
|
packY + height * 0.35,
|
|
packX - width * 0.05,
|
|
packY + height * 0.28
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Right flame
|
|
ctx.beginPath();
|
|
ctx.moveTo(packX + width * 0.05, packY + height * 0.28);
|
|
ctx.quadraticCurveTo(
|
|
packX + width * 0.07 + Math.sin(time * 22) * 5,
|
|
packY + height * 0.35,
|
|
packX + width * 0.07,
|
|
packY + height * 0.28 + flameHeight
|
|
);
|
|
ctx.quadraticCurveTo(
|
|
packX + width * 0.07 + Math.sin(time * 27) * 5,
|
|
packY + height * 0.35,
|
|
packX + width * 0.09,
|
|
packY + height * 0.28
|
|
);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Smoke trail
|
|
ctx.fillStyle = 'rgba(100, 100, 100, 0.3)';
|
|
for (var i = 0; i < 6; i++) {
|
|
var smokeY = packY + height * 0.35 + i * 15 + (time * 50) % 20;
|
|
var smokeX = packX + Math.sin(time * 3 + i) * 10;
|
|
var smokeSize = 8 + i * 3;
|
|
ctx.globalAlpha = 0.3 - i * 0.04;
|
|
ctx.beginPath();
|
|
ctx.arc(smokeX, smokeY, smokeSize, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
ctx.globalAlpha = 1;
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: { type: 'bob', amplitude: 2, rate: 2 },
|
|
boost: { type: 'particles', direction: 'down', color: '#ff6600' },
|
|
damage: { type: 'shake', intensity: 8 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 9. MECH SUIT
|
|
// ============================================================
|
|
{
|
|
id: 'mech-suit',
|
|
name: 'Mech Suit',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_ONLY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: false, headwear: false, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['mech', 'shooter', 'action', 'combat'],
|
|
description: 'Pilot a powerful mech suit',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.35 },
|
|
avatarScale: 0.7,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Mech interior with screens
|
|
var interiorGradient = ctx.createRadialGradient(
|
|
width * 0.5, height * 0.4, 0,
|
|
width * 0.5, height * 0.4, width * 0.6
|
|
);
|
|
interiorGradient.addColorStop(0, '#2a2a3a');
|
|
interiorGradient.addColorStop(1, '#1a1a2a');
|
|
ctx.fillStyle = interiorGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Side screens
|
|
ctx.fillStyle = '#0a1a2a';
|
|
ctx.fillRect(width * 0.02, height * 0.15, width * 0.15, height * 0.25);
|
|
ctx.fillRect(width * 0.83, height * 0.15, width * 0.15, height * 0.25);
|
|
|
|
// Screen content - radar
|
|
ctx.strokeStyle = '#00ff00';
|
|
ctx.lineWidth = 1;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.095, height * 0.275, width * 0.05, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Radar sweep
|
|
var sweepAngle = (time * 2) % (Math.PI * 2);
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.095, height * 0.275);
|
|
ctx.lineTo(
|
|
width * 0.095 + Math.cos(sweepAngle) * width * 0.05,
|
|
height * 0.275 + Math.sin(sweepAngle) * width * 0.05
|
|
);
|
|
ctx.stroke();
|
|
|
|
// Right screen - systems
|
|
ctx.fillStyle = '#00ff00';
|
|
ctx.font = (width * 0.02) + 'px monospace';
|
|
ctx.fillText('PWR: 87%', width * 0.84, height * 0.2);
|
|
ctx.fillText('ARM: OK', width * 0.84, height * 0.25);
|
|
ctx.fillText('SHD: 65%', width * 0.84, height * 0.3);
|
|
ctx.fillText('FUEL: 72%', width * 0.84, height * 0.35);
|
|
|
|
// Interior frame elements
|
|
ctx.fillStyle = '#3a3a4a';
|
|
ctx.fillRect(0, 0, width * 0.08, height);
|
|
ctx.fillRect(width * 0.92, 0, width * 0.08, height);
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Mech chest window frame (hexagonal)
|
|
ctx.strokeStyle = '#5a5a6a';
|
|
ctx.lineWidth = width * 0.06;
|
|
ctx.beginPath();
|
|
var centerX = width * 0.5;
|
|
var centerY = height * 0.35;
|
|
var radius = width * 0.28;
|
|
for (var i = 0; i < 6; i++) {
|
|
var angle = (i / 6) * Math.PI * 2 - Math.PI / 2;
|
|
var x = centerX + Math.cos(angle) * radius;
|
|
var y = centerY + Math.sin(angle) * radius * 0.8;
|
|
if (i === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
// Inner frame
|
|
ctx.strokeStyle = '#4a4a5a';
|
|
ctx.lineWidth = width * 0.02;
|
|
ctx.beginPath();
|
|
for (var j = 0; j < 6; j++) {
|
|
var angle2 = (j / 6) * Math.PI * 2 - Math.PI / 2;
|
|
var x2 = centerX + Math.cos(angle2) * radius * 1.1;
|
|
var y2 = centerY + Math.sin(angle2) * radius * 0.88;
|
|
if (j === 0) {
|
|
ctx.moveTo(x2, y2);
|
|
} else {
|
|
ctx.lineTo(x2, y2);
|
|
}
|
|
}
|
|
ctx.closePath();
|
|
ctx.stroke();
|
|
|
|
// HUD elements
|
|
ctx.strokeStyle = 'rgba(0, 200, 255, 0.6)';
|
|
ctx.lineWidth = 2;
|
|
|
|
// Targeting brackets
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.35, height * 0.2);
|
|
ctx.lineTo(width * 0.35, height * 0.15);
|
|
ctx.lineTo(width * 0.4, height * 0.15);
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.65, height * 0.2);
|
|
ctx.lineTo(width * 0.65, height * 0.15);
|
|
ctx.lineTo(width * 0.6, height * 0.15);
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.35, height * 0.5);
|
|
ctx.lineTo(width * 0.35, height * 0.55);
|
|
ctx.lineTo(width * 0.4, height * 0.55);
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.65, height * 0.5);
|
|
ctx.lineTo(width * 0.65, height * 0.55);
|
|
ctx.lineTo(width * 0.6, height * 0.55);
|
|
ctx.stroke();
|
|
|
|
// Control panel
|
|
ctx.fillStyle = '#3a3a4a';
|
|
ctx.fillRect(0, height * 0.7, width, height * 0.3);
|
|
|
|
// Control sticks
|
|
ctx.fillStyle = '#2a2a3a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.25, height * 0.85, width * 0.06, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.75, height * 0.85, width * 0.06, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#5a5a6a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.25, height * 0.85, width * 0.025, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.75, height * 0.85, width * 0.025, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Warning lights (blinking)
|
|
var warningPhase = Math.sin(time * 4);
|
|
if (warningPhase > 0.5) {
|
|
ctx.fillStyle = '#ff3333';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.75, width * 0.015, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Status text
|
|
ctx.fillStyle = '#00ffff';
|
|
ctx.font = (width * 0.035) + 'px monospace';
|
|
ctx.fillText('MECH SYSTEMS ONLINE', width * 0.32, height * 0.78);
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
walk: { type: 'shake', intensity: 2 },
|
|
fire: { type: 'flash', color: '#ffff00' },
|
|
damage: { type: 'shake', intensity: 10 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 10. SUBMARINE
|
|
// ============================================================
|
|
{
|
|
id: 'submarine',
|
|
name: 'Submarine',
|
|
category: 'vehicle',
|
|
avatarMode: 'HEAD_AND_SHOULDERS',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['underwater', 'exploration', 'arcade'],
|
|
description: 'Explore the depths in a submarine',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.4 },
|
|
avatarScale: 0.8,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Underwater gradient
|
|
var waterGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
waterGradient.addColorStop(0, '#0a3a5a');
|
|
waterGradient.addColorStop(0.5, '#0a2a4a');
|
|
waterGradient.addColorStop(1, '#0a1a3a');
|
|
ctx.fillStyle = waterGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Bubbles rising
|
|
ctx.fillStyle = 'rgba(150, 200, 255, 0.4)';
|
|
for (var i = 0; i < 15; i++) {
|
|
var seed = i * 1373;
|
|
var bx = ((seed * 13) % 1000) / 1000 * width;
|
|
var by = height - ((time * 40 + seed) % height);
|
|
var bSize = 3 + ((seed * 17) % 8);
|
|
var wobble = Math.sin(time * 2 + i) * 5;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(bx + wobble, by, bSize, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Submarine interior
|
|
ctx.fillStyle = '#3a4a5a';
|
|
ctx.fillRect(0, 0, width * 0.12, height);
|
|
ctx.fillRect(width * 0.88, 0, width * 0.12, height);
|
|
|
|
// Interior pipes
|
|
ctx.strokeStyle = '#5a6a7a';
|
|
ctx.lineWidth = 8;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.06, 0);
|
|
ctx.lineTo(width * 0.06, height);
|
|
ctx.stroke();
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.94, 0);
|
|
ctx.lineTo(width * 0.94, height);
|
|
ctx.stroke();
|
|
|
|
// Pipe joints
|
|
ctx.fillStyle = '#4a5a6a';
|
|
for (var j = 0; j < 4; j++) {
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.06, height * 0.2 + j * height * 0.25, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.94, height * 0.2 + j * height * 0.25, 12, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Porthole frame (circular submarine window)
|
|
ctx.strokeStyle = '#6a7a8a';
|
|
ctx.lineWidth = width * 0.08;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.4, width * 0.3, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Porthole rivets
|
|
ctx.fillStyle = '#5a6a7a';
|
|
for (var i = 0; i < 12; i++) {
|
|
var angle = (i / 12) * Math.PI * 2;
|
|
var rx = width * 0.5 + Math.cos(angle) * width * 0.34;
|
|
var ry = height * 0.4 + Math.sin(angle) * width * 0.34;
|
|
ctx.beginPath();
|
|
ctx.arc(rx, ry, 5, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Glass reflection
|
|
ctx.strokeStyle = 'rgba(150, 200, 255, 0.3)';
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.4, height * 0.3, width * 0.15, Math.PI * 1.2, Math.PI * 1.7);
|
|
ctx.stroke();
|
|
|
|
// Water distortion overlay
|
|
var distortionAlpha = 0.05 + Math.sin(time) * 0.02;
|
|
ctx.fillStyle = 'rgba(100, 150, 200, ' + distortionAlpha + ')';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.4, width * 0.26, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Control panel
|
|
ctx.fillStyle = '#4a5a6a';
|
|
ctx.fillRect(0, height * 0.75, width, height * 0.25);
|
|
|
|
// Depth gauge
|
|
ctx.fillStyle = '#2a3a4a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.2, height * 0.87, width * 0.07, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#00ff00';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.2, height * 0.87, width * 0.05, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Depth needle
|
|
var depthAngle = Math.PI * 0.75 + (0.4 + Math.sin(time * 0.3) * 0.2) * Math.PI;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.2, height * 0.87);
|
|
ctx.lineTo(
|
|
width * 0.2 + Math.cos(depthAngle) * width * 0.045,
|
|
height * 0.87 + Math.sin(depthAngle) * width * 0.045
|
|
);
|
|
ctx.stroke();
|
|
|
|
// Depth reading
|
|
ctx.fillStyle = '#00ff00';
|
|
ctx.font = (width * 0.03) + 'px monospace';
|
|
ctx.fillText('DEPTH', width * 0.155, height * 0.95);
|
|
ctx.fillText(Math.floor(500 + Math.sin(time * 0.3) * 50) + 'm', width * 0.16, height * 0.98);
|
|
|
|
// Pressure gauge
|
|
ctx.fillStyle = '#2a3a4a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.8, height * 0.87, width * 0.07, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#ffaa00';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.8, height * 0.87, width * 0.05, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Sonar ping effect
|
|
var pingPhase = (time * 0.5) % 1;
|
|
ctx.strokeStyle = 'rgba(0, 255, 100, ' + (1 - pingPhase) * 0.5 + ')';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.87, pingPhase * width * 0.15, 0, Math.PI * 2);
|
|
ctx.stroke();
|
|
|
|
// Periscope controls
|
|
ctx.fillStyle = '#5a6a7a';
|
|
ctx.fillRect(width * 0.45, height * 0.78, width * 0.1, height * 0.08);
|
|
ctx.fillStyle = '#3a4a5a';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.5, height * 0.82, width * 0.03, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: { type: 'bob', amplitude: 2, rate: 1 },
|
|
pressure: { type: 'distort', intensity: 3 },
|
|
damage: { type: 'shake', intensity: 6 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 11. DRAGON RIDER
|
|
// ============================================================
|
|
{
|
|
id: 'dragon-rider',
|
|
name: 'Dragon Rider',
|
|
category: 'vehicle',
|
|
avatarMode: 'FULL_BODY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'lean'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['fantasy', 'adventure', 'flying', 'rpg'],
|
|
description: 'Soar through the skies on a dragon',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.3 },
|
|
avatarScale: 0.5,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Sky gradient
|
|
var skyGradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
skyGradient.addColorStop(0, '#ff7700');
|
|
skyGradient.addColorStop(0.3, '#ff9944');
|
|
skyGradient.addColorStop(0.6, '#87ceeb');
|
|
skyGradient.addColorStop(1, '#5588bb');
|
|
ctx.fillStyle = skyGradient;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
// Clouds
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
|
|
for (var i = 0; i < 6; i++) {
|
|
var seed = i * 2741;
|
|
var cx = (((seed * 13) % 1000) / 1000 * width * 2 - time * 30) % (width * 1.5) - width * 0.25;
|
|
var cy = ((seed * 17) % 1000) / 1000 * height * 0.4;
|
|
var cw = 60 + ((seed * 23) % 40);
|
|
|
|
for (var j = 0; j < 3; j++) {
|
|
ctx.beginPath();
|
|
ctx.arc(cx + j * cw * 0.4, cy, cw * 0.35, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
|
|
// Dragon body/neck beneath rider
|
|
var wingFlap = Math.sin(time * 3) * 0.15;
|
|
|
|
// Dragon neck
|
|
var neckGradient = ctx.createLinearGradient(width * 0.3, height * 0.4, width * 0.7, height * 0.6);
|
|
neckGradient.addColorStop(0, '#2a5a3a');
|
|
neckGradient.addColorStop(0.5, '#3a7a4a');
|
|
neckGradient.addColorStop(1, '#2a5a3a');
|
|
ctx.fillStyle = neckGradient;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.35, height * 0.5);
|
|
ctx.quadraticCurveTo(width * 0.5, height * 0.45, width * 0.65, height * 0.5);
|
|
ctx.lineTo(width * 0.6, height * 0.65);
|
|
ctx.quadraticCurveTo(width * 0.5, height * 0.6, width * 0.4, height * 0.65);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Dragon scales on neck
|
|
ctx.fillStyle = '#1a4a2a';
|
|
for (var k = 0; k < 5; k++) {
|
|
var scaleX = width * 0.42 + k * width * 0.04;
|
|
var scaleY = height * 0.52 + Math.sin(k * 0.5) * 5;
|
|
ctx.beginPath();
|
|
ctx.arc(scaleX, scaleY, 6, 0, Math.PI, true);
|
|
ctx.fill();
|
|
}
|
|
|
|
// Dragon wings (behind rider)
|
|
ctx.fillStyle = '#2a6a3a';
|
|
|
|
// Left wing
|
|
ctx.save();
|
|
ctx.translate(width * 0.3, height * 0.5);
|
|
ctx.rotate(-0.5 + wingFlap);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.quadraticCurveTo(-width * 0.3, -height * 0.1, -width * 0.4, height * 0.1);
|
|
ctx.quadraticCurveTo(-width * 0.2, height * 0.05, 0, height * 0.1);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Wing membrane lines
|
|
ctx.strokeStyle = '#1a5a2a';
|
|
ctx.lineWidth = 2;
|
|
for (var l = 0; l < 4; l++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height * 0.02 * l);
|
|
ctx.quadraticCurveTo(-width * 0.2, 0, -width * 0.3 + l * 20, height * 0.08);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
|
|
// Right wing
|
|
ctx.save();
|
|
ctx.translate(width * 0.7, height * 0.5);
|
|
ctx.rotate(0.5 - wingFlap);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, 0);
|
|
ctx.quadraticCurveTo(width * 0.3, -height * 0.1, width * 0.4, height * 0.1);
|
|
ctx.quadraticCurveTo(width * 0.2, height * 0.05, 0, height * 0.1);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#1a5a2a';
|
|
for (var m = 0; m < 4; m++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height * 0.02 * m);
|
|
ctx.quadraticCurveTo(width * 0.2, 0, width * 0.3 - m * 20, height * 0.08);
|
|
ctx.stroke();
|
|
}
|
|
ctx.restore();
|
|
|
|
// Saddle
|
|
ctx.fillStyle = '#5a3a2a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, height * 0.48, width * 0.1, height * 0.04, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#7a4a3a';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, height * 0.46, width * 0.08, height * 0.025, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Dragon head/horns framing view (top)
|
|
var headBob = Math.sin(time * 2) * 3;
|
|
|
|
// Dragon horns
|
|
ctx.fillStyle = '#4a3a2a';
|
|
|
|
// Left horn
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.15, height * 0.15 + headBob);
|
|
ctx.quadraticCurveTo(width * 0.1, 0, width * 0.2, -height * 0.05);
|
|
ctx.quadraticCurveTo(width * 0.18, height * 0.05, width * 0.22, height * 0.12 + headBob);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Right horn
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.85, height * 0.15 + headBob);
|
|
ctx.quadraticCurveTo(width * 0.9, 0, width * 0.8, -height * 0.05);
|
|
ctx.quadraticCurveTo(width * 0.82, height * 0.05, width * 0.78, height * 0.12 + headBob);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Dragon ears/spines
|
|
ctx.fillStyle = '#3a6a4a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.05, height * 0.3 + headBob);
|
|
ctx.lineTo(0, height * 0.15);
|
|
ctx.lineTo(width * 0.12, height * 0.25 + headBob);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.95, height * 0.3 + headBob);
|
|
ctx.lineTo(width, height * 0.15);
|
|
ctx.lineTo(width * 0.88, height * 0.25 + headBob);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Dragon body below (tail hint)
|
|
ctx.fillStyle = '#2a5a3a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.3, height * 0.85);
|
|
ctx.quadraticCurveTo(width * 0.5, height * 0.75, width * 0.7, height * 0.85);
|
|
ctx.lineTo(width * 0.65, height);
|
|
ctx.lineTo(width * 0.35, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Dragon legs/claws
|
|
ctx.fillStyle = '#2a5a3a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.2, height * 0.7);
|
|
ctx.quadraticCurveTo(width * 0.15, height * 0.85, width * 0.1, height);
|
|
ctx.lineTo(width * 0.25, height);
|
|
ctx.quadraticCurveTo(width * 0.25, height * 0.8, width * 0.28, height * 0.7);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.8, height * 0.7);
|
|
ctx.quadraticCurveTo(width * 0.85, height * 0.85, width * 0.9, height);
|
|
ctx.lineTo(width * 0.75, height);
|
|
ctx.quadraticCurveTo(width * 0.75, height * 0.8, width * 0.72, height * 0.7);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Claws
|
|
ctx.fillStyle = '#3a3a3a';
|
|
for (var i = 0; i < 3; i++) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.08 + i * 8, height);
|
|
ctx.lineTo(width * 0.1 + i * 8, height * 0.95);
|
|
ctx.lineTo(width * 0.12 + i * 8, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.88 - i * 8, height);
|
|
ctx.lineTo(width * 0.9 - i * 8, height * 0.95);
|
|
ctx.lineTo(width * 0.92 - i * 8, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: { type: 'bob', amplitude: 5, rate: 2 },
|
|
fireBreath: { type: 'glow', color: '#ff6600' },
|
|
dive: { type: 'blur', direction: 'vertical' },
|
|
damage: { type: 'shake', intensity: 8 }
|
|
}
|
|
},
|
|
|
|
// ============================================================
|
|
// 12. MOTORCYCLE
|
|
// ============================================================
|
|
{
|
|
id: 'motorcycle',
|
|
name: 'Motorcycle',
|
|
category: 'vehicle',
|
|
avatarMode: 'FULL_BODY',
|
|
animations: ['idle', 'bob', 'celebrate', 'hurt', 'lean-left', 'lean-right'],
|
|
defaultAnimation: 'idle',
|
|
accessoryVisibility: { eyewear: true, headwear: true, effects: true },
|
|
layerOrder: ['background', 'avatar', 'foreground'],
|
|
recommendedFor: ['racing', 'endless', 'arcade', 'action'],
|
|
description: 'Race on a high-speed motorcycle',
|
|
|
|
avatarPosition: { x: 0.5, y: 0.3 },
|
|
avatarScale: 0.55,
|
|
|
|
renderBackground: function(ctx, width, height, time, options) {
|
|
// Sky
|
|
var skyGradient = ctx.createLinearGradient(0, 0, 0, height * 0.4);
|
|
skyGradient.addColorStop(0, '#ff7700');
|
|
skyGradient.addColorStop(1, '#ffaa44');
|
|
ctx.fillStyle = skyGradient;
|
|
ctx.fillRect(0, 0, width, height * 0.4);
|
|
|
|
// Road
|
|
var roadGradient = ctx.createLinearGradient(0, height * 0.4, 0, height);
|
|
roadGradient.addColorStop(0, '#444444');
|
|
roadGradient.addColorStop(1, '#222222');
|
|
ctx.fillStyle = roadGradient;
|
|
ctx.fillRect(0, height * 0.4, width, height * 0.6);
|
|
|
|
// Road lines
|
|
ctx.fillStyle = '#ffffff';
|
|
var lineOffset = (time * 400) % 100;
|
|
for (var i = -1; i < 6; i++) {
|
|
var y = height * 0.5 + i * 100 - lineOffset;
|
|
var perspectiveScale = (y - height * 0.4) / (height * 0.6);
|
|
var lineWidth = 4 + perspectiveScale * 10;
|
|
ctx.fillRect(width * 0.48, y, width * 0.04, 50);
|
|
}
|
|
|
|
// Environment blur (side)
|
|
ctx.fillStyle = 'rgba(100, 150, 100, 0.3)';
|
|
for (var j = 0; j < 8; j++) {
|
|
var blurY = height * 0.3 + j * 40;
|
|
var blurOffset = (time * 300 + j * 50) % 200;
|
|
ctx.fillRect(0, blurY, width * 0.1 - blurOffset * 0.05, 30);
|
|
ctx.fillRect(width * 0.9 + blurOffset * 0.05, blurY, width * 0.1, 30);
|
|
}
|
|
|
|
// Motion blur lines
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)';
|
|
ctx.lineWidth = 2;
|
|
for (var k = 0; k < 15; k++) {
|
|
var seed = k * 1931;
|
|
var lx = ((seed * 13) % 1000) / 1000 * width;
|
|
var ly = height * 0.45 + ((seed * 17) % 1000) / 1000 * height * 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(lx, ly);
|
|
ctx.lineTo(lx, ly + 40);
|
|
ctx.stroke();
|
|
}
|
|
},
|
|
|
|
renderForeground: function(ctx, width, height, time, options) {
|
|
// Motorcycle
|
|
var bikeY = height * 0.55;
|
|
var wheelSpin = time * 20;
|
|
|
|
// Bike body
|
|
var bodyGradient = ctx.createLinearGradient(width * 0.3, bikeY, width * 0.7, bikeY);
|
|
bodyGradient.addColorStop(0, '#cc0000');
|
|
bodyGradient.addColorStop(0.5, '#ff3333');
|
|
bodyGradient.addColorStop(1, '#cc0000');
|
|
ctx.fillStyle = bodyGradient;
|
|
|
|
// Main body shape
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.25, bikeY);
|
|
ctx.quadraticCurveTo(width * 0.35, bikeY - height * 0.08, width * 0.5, bikeY - height * 0.1);
|
|
ctx.quadraticCurveTo(width * 0.65, bikeY - height * 0.08, width * 0.75, bikeY);
|
|
ctx.lineTo(width * 0.7, bikeY + height * 0.05);
|
|
ctx.lineTo(width * 0.3, bikeY + height * 0.05);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Fuel tank
|
|
ctx.fillStyle = '#aa0000';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.5, bikeY - height * 0.05, width * 0.12, height * 0.04, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Tank stripe
|
|
ctx.fillStyle = '#ffffff';
|
|
ctx.fillRect(width * 0.48, bikeY - height * 0.08, width * 0.04, height * 0.06);
|
|
|
|
// Engine
|
|
ctx.fillStyle = '#333333';
|
|
ctx.beginPath();
|
|
ctx.rect(width * 0.4, bikeY + height * 0.02, width * 0.2, height * 0.1);
|
|
ctx.fill();
|
|
|
|
// Engine details
|
|
ctx.fillStyle = '#555555';
|
|
for (var i = 0; i < 4; i++) {
|
|
ctx.fillRect(width * 0.42 + i * width * 0.04, bikeY + height * 0.03, width * 0.02, height * 0.08);
|
|
}
|
|
|
|
// Exhaust pipes
|
|
ctx.fillStyle = '#666666';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.6, bikeY + height * 0.08);
|
|
ctx.quadraticCurveTo(width * 0.75, bikeY + height * 0.1, width * 0.85, bikeY + height * 0.15);
|
|
ctx.lineTo(width * 0.85, bikeY + height * 0.18);
|
|
ctx.quadraticCurveTo(width * 0.75, bikeY + height * 0.13, width * 0.6, bikeY + height * 0.11);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Front wheel
|
|
var frontWheelX = width * 0.25;
|
|
var wheelY = bikeY + height * 0.15;
|
|
var wheelRadius = width * 0.1;
|
|
|
|
ctx.fillStyle = '#1a1a1a';
|
|
ctx.beginPath();
|
|
ctx.arc(frontWheelX, wheelY, wheelRadius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Wheel spokes
|
|
ctx.strokeStyle = '#444444';
|
|
ctx.lineWidth = 2;
|
|
for (var j = 0; j < 8; j++) {
|
|
var spokeAngle = wheelSpin + (j / 8) * Math.PI * 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(frontWheelX, wheelY);
|
|
ctx.lineTo(
|
|
frontWheelX + Math.cos(spokeAngle) * wheelRadius * 0.8,
|
|
wheelY + Math.sin(spokeAngle) * wheelRadius * 0.8
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Wheel hub
|
|
ctx.fillStyle = '#888888';
|
|
ctx.beginPath();
|
|
ctx.arc(frontWheelX, wheelY, wheelRadius * 0.25, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Rear wheel
|
|
var rearWheelX = width * 0.75;
|
|
|
|
ctx.fillStyle = '#1a1a1a';
|
|
ctx.beginPath();
|
|
ctx.arc(rearWheelX, wheelY, wheelRadius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = '#444444';
|
|
for (var k = 0; k < 8; k++) {
|
|
var rearSpokeAngle = wheelSpin + (k / 8) * Math.PI * 2;
|
|
ctx.beginPath();
|
|
ctx.moveTo(rearWheelX, wheelY);
|
|
ctx.lineTo(
|
|
rearWheelX + Math.cos(rearSpokeAngle) * wheelRadius * 0.8,
|
|
wheelY + Math.sin(rearSpokeAngle) * wheelRadius * 0.8
|
|
);
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.fillStyle = '#888888';
|
|
ctx.beginPath();
|
|
ctx.arc(rearWheelX, wheelY, wheelRadius * 0.25, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Front fork
|
|
ctx.strokeStyle = '#666666';
|
|
ctx.lineWidth = 6;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.32, bikeY - height * 0.05);
|
|
ctx.lineTo(frontWheelX, wheelY);
|
|
ctx.stroke();
|
|
|
|
// Handlebars
|
|
ctx.strokeStyle = '#444444';
|
|
ctx.lineWidth = 4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.25, bikeY - height * 0.12);
|
|
ctx.lineTo(width * 0.4, bikeY - height * 0.1);
|
|
ctx.stroke();
|
|
|
|
// Mirrors
|
|
ctx.fillStyle = '#333333';
|
|
ctx.beginPath();
|
|
ctx.ellipse(width * 0.22, bikeY - height * 0.14, width * 0.02, height * 0.015, -0.3, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Headlight
|
|
ctx.fillStyle = '#ffff88';
|
|
ctx.beginPath();
|
|
ctx.arc(width * 0.2, bikeY - height * 0.02, width * 0.025, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
|
|
// Seat
|
|
ctx.fillStyle = '#1a1a1a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(width * 0.42, bikeY - height * 0.1);
|
|
ctx.quadraticCurveTo(width * 0.55, bikeY - height * 0.14, width * 0.68, bikeY - height * 0.08);
|
|
ctx.lineTo(width * 0.65, bikeY - height * 0.06);
|
|
ctx.quadraticCurveTo(width * 0.55, bikeY - height * 0.1, width * 0.45, bikeY - height * 0.08);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
|
|
// Speed blur from wheels
|
|
ctx.fillStyle = 'rgba(150, 150, 150, 0.3)';
|
|
ctx.beginPath();
|
|
ctx.ellipse(frontWheelX, wheelY + wheelRadius * 0.8, wheelRadius * 0.8, wheelRadius * 0.15, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
ctx.beginPath();
|
|
ctx.ellipse(rearWheelX, wheelY + wheelRadius * 0.8, wheelRadius * 0.8, wheelRadius * 0.15, 0, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
},
|
|
|
|
contextEffects: {
|
|
idle: null,
|
|
accelerate: { type: 'blur', direction: 'vertical' },
|
|
wheelie: { type: 'rotate', angle: -15 },
|
|
lean: { type: 'rotate', angle: 20 },
|
|
damage: { type: 'shake', intensity: 8 }
|
|
}
|
|
}
|
|
|
|
];
|
|
|
|
// ============================================================
|
|
// PUBLIC API
|
|
// ============================================================
|
|
|
|
window.VehicleContexts = {
|
|
CONTEXTS: CONTEXTS,
|
|
|
|
/**
|
|
* Get a vehicle context by its ID
|
|
* @param {string} id - The context ID
|
|
* @returns {Object|null} The context object or null if not found
|
|
*/
|
|
getById: function(id) {
|
|
for (var i = 0; i < CONTEXTS.length; i++) {
|
|
if (CONTEXTS[i].id === id) {
|
|
return CONTEXTS[i];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get all vehicle contexts
|
|
* @returns {Array} Array of all context objects
|
|
*/
|
|
getAll: function() {
|
|
return CONTEXTS.slice();
|
|
},
|
|
|
|
/**
|
|
* Get contexts filtered by avatar mode
|
|
* @param {string} mode - The avatar mode (HEAD_ONLY, HEAD_AND_SHOULDERS, UPPER_BODY, FULL_BODY)
|
|
* @returns {Array} Array of matching context objects
|
|
*/
|
|
getByMode: function(mode) {
|
|
var results = [];
|
|
for (var i = 0; i < CONTEXTS.length; i++) {
|
|
if (CONTEXTS[i].avatarMode === mode) {
|
|
results.push(CONTEXTS[i]);
|
|
}
|
|
}
|
|
return results;
|
|
},
|
|
|
|
/**
|
|
* Get contexts recommended for a specific game type
|
|
* @param {string} gameType - The game type to match
|
|
* @returns {Array} Array of matching context objects
|
|
*/
|
|
getRecommendedFor: function(gameType) {
|
|
var results = [];
|
|
var lowerType = gameType.toLowerCase();
|
|
for (var i = 0; i < CONTEXTS.length; i++) {
|
|
var recommended = CONTEXTS[i].recommendedFor;
|
|
for (var j = 0; j < recommended.length; j++) {
|
|
if (recommended[j].toLowerCase() === lowerType) {
|
|
results.push(CONTEXTS[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
};
|
|
|
|
})();
|