game1 / game.js
pbolo's picture
Upload 5 files
8be9dca verified
Raw
History Blame Contribute Delete
19.7 kB
import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.mjs';
const VIRTUAL_WIDTH = 320;
const VIRTUAL_HEIGHT = 320;
const WORLD_SIZE = 500;
const SPAWN_RADIUS = 200;
const PLAYER_SPEED = 2;
const ENEMY_BASE_SPEED = 0.8;
const PROJECTILE_SPEED = 4;
const FIRE_RATE = 35;
let app, world, player;
let enemies = [], projectiles = [], gems = [], particles = [];
let keys = {};
let level = 1, xp = 0, xpToNext = 100, kills = 0, wave = 1;
let isPaused = false, gameOver = false;
let hudText, hpBarGraphics, levelText, waveText, menuContainer;
let panelX, panelY; // Define globally
let ticker = 0;
let spawnTimer = 0;
let waveKills = 0;
let menuOptions = null;
const textures = {};
async function init() {
app = new PIXI.Application();
await app.init({
width: VIRTUAL_WIDTH,
height: VIRTUAL_HEIGHT,
backgroundColor: 0x111111,
antialias: false,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
resizeTo: document.getElementById('game-container')
});
document.getElementById('game-container').appendChild(app.canvas);
// Scale stage
const scale = Math.min(app.screen.width / VIRTUAL_WIDTH, app.screen.height / VIRTUAL_HEIGHT);
app.stage.scale.set(scale);
// Load assets safely
try {
textures.player = await PIXI.Assets.load('assets/player.png');
textures.player.source.scaleMode = 'nearest';
} catch (e) {
console.warn('Failed to load player.png, using procedural fallback');
}
try {
textures.ground = await PIXI.Assets.load('assets/ground_tile.png');
textures.ground.source.scaleMode = 'nearest';
} catch (e) {
console.warn('Failed to load ground_tile.png, using procedural fallback');
const canvas = document.createElement('canvas');
canvas.width = 32; canvas.height = 32;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#111'; ctx.fillRect(0,0,32,32);
textures.ground = PIXI.Texture.from(canvas);
}
try {
textures.zombie = await PIXI.Assets.load('assets/zombie.png');
textures.zombie.source.scaleMode = 'nearest';
} catch (e) {
console.warn('Failed to load zombie.png');
}
// Always call createTextures to ensure textures exist if fallback needed
createTextures();
// Ensure player texture is set if assets failed
if (!textures.player) {
const canvas = document.createElement('canvas');
canvas.width = 16; canvas.height = 16;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#3498db'; ctx.fillRect(0,0,16,16);
textures.player = PIXI.Texture.from(canvas);
}
// Ensure enemy texture is set if zombie.png failed
if (!textures.zombie) {
textures.zombie = textures.enemy;
}
setupGame();
window.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'KeyP') isPaused = !isPaused;
handleKeyPress(e);
});
window.addEventListener('keyup', (e) => { keys[e.code] = false; });
app.ticker.add((ticker) => wrappedUpdate(ticker));
}
function createTextures() {
const createFromCanvas = (name, draw, w = 16, h = 16) => {
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
draw(canvas.getContext('2d'), w, h);
const tex = PIXI.Texture.from(canvas);
tex.source.scaleMode = 'nearest';
textures[name] = tex;
};
createFromCanvas('enemy', (ctx, w, h) => {
ctx.fillStyle = '#c0392b';
ctx.fillRect(2, 2, 12, 12);
ctx.fillStyle = '#000';
ctx.fillRect(4, 4, 2, 2);
ctx.fillRect(10, 4, 2, 2);
ctx.fillRect(5, 10, 6, 2);
});
createFromCanvas('projectile', (ctx, w, h) => {
ctx.fillStyle = '#f1c40f';
ctx.fillRect(6, 6, 4, 4);
}, 16, 16);
createFromCanvas('gem', (ctx, w, h) => {
ctx.fillStyle = '#2ecc71';
ctx.beginPath();
ctx.moveTo(8, 2); ctx.lineTo(14, 8); ctx.lineTo(8, 14); ctx.lineTo(2, 8);
ctx.closePath();
ctx.fill();
});
createFromCanvas('particle', (ctx, w, h) => {
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, 2, 2);
}, 2, 2);
}
function setupGame() {
world = new PIXI.Container();
app.stage.addChild(world);
// Replace procedural floor with TilingSprite
const floor = new PIXI.TilingSprite(textures.ground, WORLD_SIZE, WORLD_SIZE);
floor.anchor.set(0.5);
floor.tileScale.set(32 / 2048); // Scale 2048px original down to 32px visual
world.addChild(floor);
player = new PIXI.Sprite(textures.player);
player.anchor.set(0.5, 1); // Anchor at bottom-center
player.position.set(0, 0); // World coordinates start at 0,0
// Scale down to ~32px (1500px width)
player.scale.set(32 / textures.player.width);
player.hp = 100;
player.maxHp = 100;
player.damage = 25;
player.fireDelay = 0;
player.range = 150;
player.facing = 1; // Initialize facing
world.addChild(player);
const ui = new PIXI.Container();
app.stage.addChild(ui);
hudText = new PIXI.Text({
text: 'LVL: 1 XP: 0/100',
style: { fontFamily: 'Courier New', fontSize: 10, fill: 0xffffff, fontWeight: 'bold' }
});
hudText.position.set(5, 5);
ui.addChild(hudText);
waveText = new PIXI.Text({
text: 'WAVE 1',
style: { fontFamily: 'Courier New', fontSize: 10, fill: 0xf1c40f }
});
waveText.position.set(5, 18);
ui.addChild(waveText);
hpBarGraphics = new PIXI.Graphics();
ui.addChild(hpBarGraphics);
levelText = new PIXI.Text({
text: '',
style: { fontFamily: 'Courier New', fontSize: 12, fill: 0xffffff, align: 'center' }
});
levelText.anchor.set(0.5);
levelText.position.set(VIRTUAL_WIDTH/2, VIRTUAL_HEIGHT/2);
levelText.visible = false;
ui.addChild(levelText);
// Add styled menu container
menuContainer = new PIXI.Container();
menuContainer.visible = false;
ui.addChild(menuContainer);
// Background overlay
const menuBg = new PIXI.Graphics();
menuBg.rect(0, 0, VIRTUAL_WIDTH, VIRTUAL_HEIGHT).fill({ color: 0x000000, alpha: 0.8 });
menuContainer.addChild(menuBg);
// Centered Panel
const panelW = 220;
const panelH = 160;
panelX = (VIRTUAL_WIDTH - panelW) / 2;
panelY = (VIRTUAL_HEIGHT - panelH) / 2;
const menuPanel = new PIXI.Graphics();
menuPanel.rect(panelX, panelY, panelW, panelH).fill(0x222222).stroke({ width: 2, color: 0x555555 });
menuContainer.addChild(menuPanel);
const title = new PIXI.Text({
text: 'LEVEL UP!',
style: { fontFamily: 'Courier New', fontSize: 18, fill: 0xf1c40f, fontWeight: 'bold' }
});
title.anchor.set(0.5);
title.position.set(VIRTUAL_WIDTH/2, panelY + 30);
menuContainer.addChild(title);
const optionsText = new PIXI.Text({
text: '1: HP +25\n2: DMG +15\n3: Fire Rate +\n4: Full Heal',
style: { fontFamily: 'Courier New', fontSize: 12, fill: 0xffffff, leading: 6 }
});
optionsText.position.set(panelX + 30, panelY + 65);
menuContainer.addChild(optionsText);
// ... (previous setup code)
menuContainer.addChild(optionsText);
// Pause text
const pauseText = new PIXI.Text({
text: 'PAUSED',
style: { fontFamily: 'Courier New', fontSize: 24, fill: 0xffffff, fontWeight: 'bold' }
});
pauseText.anchor.set(0.5);
pauseText.position.set(VIRTUAL_WIDTH/2, VIRTUAL_HEIGHT/2);
pauseText.visible = false;
ui.addChild(pauseText);
// Track pause state for UI visibility
const originalUpdate = update;
update = (ticker) => {
if (isPaused) {
pauseText.visible = !menuContainer.visible;
} else {
pauseText.visible = false;
}
originalUpdate(ticker);
};
// Spawn initial enemies
for (let i = 0; i < 5; i++) {
spawnEnemy();
}
}
function update(ticker) {
if (isPaused || gameOver) return;
const dt = ticker.deltaTime;
ticker += dt;
handleMovement(dt);
updateCamera();
handleCombat(dt);
updateEnemies(dt);
updateProjectiles(dt);
updateGems();
updateParticles(dt);
updateHUD();
// Spawn enemies continuously
spawnTimer += dt;
const spawnInterval = Math.max(15, 50 - wave * 3);
if (spawnTimer >= spawnInterval) {
spawnTimer = 0;
// Always ensure at least 2 enemies exist
const targetCount = Math.max(3, 5 + wave);
const toSpawn = Math.max(1, targetCount - enemies.length);
for (let i = 0; i < toSpawn; i++) {
spawnEnemy();
}
}
// Wave progression
if (waveKills >= 10 + wave * 5) {
wave++;
waveKills = 0;
showWaveText();
}
}
// Wrapper for UI visibility
const wrappedUpdate = (ticker) => {
if (isPaused) {
if (typeof pauseText !== 'undefined') pauseText.visible = !menuContainer.visible;
} else {
if (typeof pauseText !== 'undefined') pauseText.visible = false;
}
update(ticker);
};
function showWaveText() {
waveText.text = `WAVE ${wave}`;
}
function handleMovement(dt) {
let dx = 0, dy = 0;
if (keys['KeyW'] || keys['ArrowUp']) dy -= 1;
if (keys['KeyS'] || keys['ArrowDown']) dy += 1;
if (keys['KeyA'] || keys['ArrowLeft']) dx -= 1;
if (keys['KeyD'] || keys['ArrowRight']) dx += 1;
if (dx !== 0 || dy !== 0) {
const len = Math.sqrt(dx * dx + dy * dy);
const speed = PLAYER_SPEED + (player.speedBoost || 0);
player.x += (dx / len) * speed * dt;
player.y += (dy / len) * speed * dt;
// Direction facing
if (dx > 0) player.facing = 1;
else if (dx < 0) player.facing = -1;
} else {
// Face nearest enemy if idle
let nearest = null, minDist = player.range * 2;
for (const e of enemies) {
const d = dist(player, e);
if (d < minDist) { minDist = d; nearest = e; }
}
if (nearest) {
player.facing = (nearest.x > player.x) ? 1 : -1;
}
}
// Breathing effect (sine wave oscillation)
const breath = 1 + Math.sin(Date.now() * 0.005) * 0.03;
const baseScale = 32 / textures.player.width;
player.scale.set((player.facing || 1) * baseScale, baseScale * breath);
const half = WORLD_SIZE / 2 - 16;
player.x = Math.max(-half, Math.min(half, player.x));
player.y = Math.max(-half, Math.min(half, player.y));
}
function updateCamera() {
// Keep player centered on screen
world.x = VIRTUAL_WIDTH / 2 - player.x;
world.y = VIRTUAL_HEIGHT / 2 - player.y;
}
function handleCombat(dt) {
player.fireDelay -= dt;
if (player.fireDelay <= 0) {
let nearest = null, minDist = player.range;
for (const e of enemies) {
const d = dist(player, e);
if (d < minDist) { minDist = d; nearest = e; }
}
if (nearest) {
shoot(nearest);
player.fireDelay = FIRE_RATE;
}
}
}
function shoot(target) {
const p = new PIXI.Sprite(textures.projectile);
p.anchor.set(0.5);
p.position.set(player.x, player.y);
const angle = Math.atan2(target.y - player.y, target.x - player.x);
p.vx = Math.cos(angle) * PROJECTILE_SPEED;
p.vy = Math.sin(angle) * PROJECTILE_SPEED;
p.damage = player.damage;
p.pierce = player.pierce || 0;
p.scale.set(2);
world.addChild(p);
projectiles.push(p);
}
// Monster Types
const ENEMY_TYPES = [
{ name: 'basic', color: 0xc0392b, hp: 30, speed: 0.8, xp: 15, dmg: 0.5 },
{ name: 'tank', color: 0x8e44ad, hp: 100, speed: 0.4, xp: 40, dmg: 1.5 },
{ name: 'fast', color: 0xf39c12, hp: 20, speed: 1.5, xp: 25, dmg: 0.3 }
];
function spawnEnemy() {
const angle = Math.random() * Math.PI * 2;
const e = new PIXI.Sprite(textures.zombie);
e.anchor.set(0.5);
// Choose enemy type based on wave
const typeIdx = wave > 3 ? Math.floor(Math.random() * ENEMY_TYPES.length) : 0;
const type = ENEMY_TYPES[typeIdx];
e.position.set(player.x + Math.cos(angle) * 150, player.y + Math.sin(angle) * 150);
e.hp = (type.hp + wave * 10) * (1 + wave * 0.1);
e.speed = type.speed + wave * 0.05;
e.xp = type.xp;
e.dmg = type.dmg + wave * 0.1;
e.tint = type.color;
e.typeColor = type.color;
e.flashTimer = 0;
e.scale.set(32 / textures.zombie.width);
world.addChild(e);
enemies.push(e);
}
function updateEnemies(dt) {
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
const angle = Math.atan2(player.y - e.y, player.x - e.x);
e.x += Math.cos(angle) * e.speed * dt;
e.y += Math.sin(angle) * e.speed * dt;
// Face player: texture is facing right, so if player.x > e.x, face right (1), else left (-1)
const baseScale = 32 / textures.zombie.width;
e.scale.x = (player.x > e.x) ? baseScale : -baseScale;
e.scale.y = baseScale;
if (dist(player, e) < 10) damagePlayer(e.dmg * dt);
if (e.flashTimer > 0) {
e.flashTimer -= dt;
e.tint = 0xff0000;
} else {
e.tint = e.typeColor;
}
}
}
function updateProjectiles(dt) {
for (let i = projectiles.length - 1; i >= 0; i--) {
const p = projectiles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (dist(p, e) < 12) {
e.hp -= p.damage;
e.flashTimer = 4;
// Knockback
if (player.knockback) {
const angle = Math.atan2(e.y - player.y, e.x - player.x);
e.x += Math.cos(angle) * player.knockback * 10;
e.y += Math.sin(angle) * player.knockback * 10;
}
// Lifesteal
if (player.lifesteal) player.hp = Math.min(player.maxHp, player.hp + p.damage * player.lifesteal);
spawnParticle(p.x, p.y, 0xf1c40f);
if (e.hp <= 0) {
spawnGem(e.x, e.y, e.xp);
world.removeChild(e);
enemies.splice(j, 1);
kills++;
waveKills++;
}
// Pierce check
if (p.pierce > 0) {
p.pierce--;
} else {
world.removeChild(p);
projectiles.splice(i, 1);
break;
}
}
}
if (projectiles[i] && dist(p, player) > 400) {
world.removeChild(p);
projectiles.splice(i, 1);
}
}
}
function spawnGem(x, y, xpValue) {
const gem = new PIXI.Sprite(textures.gem);
gem.anchor.set(0.5);
gem.position.set(x, y);
gem.scale.set(0.8);
gem.xpValue = xpValue; // Store XP on gem
world.addChild(gem);
gems.push(gem);
}
function updateGems() {
for (let i = gems.length - 1; i >= 0; i--) {
const g = gems[i];
const d = dist(player, g);
if (d < 50) {
const angle = Math.atan2(player.y - g.y, player.x - g.x);
g.x += Math.cos(angle) * 5;
g.y += Math.sin(angle) * 5;
}
if (d < 12) {
world.removeChild(g);
gems.splice(i, 1);
gainXp(g.xpValue);
}
}
}
function gainXp(amount) {
xp += amount;
if (xp >= xpToNext) levelUp();
}
function levelUp() {
isPaused = true;
xp -= xpToNext;
level++;
// Smoother scaling: linear + logarithmic correction
xpToNext = Math.floor(100 + (level * 50) + (Math.log(level) * 50));
menuContainer.visible = true;
// Define all available upgrades
const allOptions = [
{ name: '+25 HP', action: () => { player.maxHp += 25; player.hp += 25; } },
{ name: '+15 DMG', action: () => { player.damage += 15; } },
{ name: 'Fire Rate', action: () => { player.fireDelay = Math.max(10, player.fireDelay - 5); } },
{ name: 'Full Heal', action: () => { player.hp = player.maxHp; } },
{ name: '+Move Spd', action: () => { player.speedBoost = (player.speedBoost || 0) + 0.5; } },
{ name: 'Knockback', action: () => { player.knockback = (player.knockback || 0) + 2; } },
{ name: 'Pierce', action: () => { player.pierce = (player.pierce || 0) + 1; } },
{ name: 'Range', action: () => { player.range += 30; } },
{ name: 'Life Steal', action: () => { player.lifesteal = (player.lifesteal || 0) + 0.05; } }
];
// Select 4 random unique options
const shuffled = allOptions.sort(() => 0.5 - Math.random());
const selected = shuffled.slice(0, 4);
menuOptions = selected.map(o => o.action);
const optionsText = new PIXI.Text({
text: selected.map((o, i) => `${i+1}: ${o.name}`).join('\n'),
style: { fontFamily: 'Courier New', fontSize: 12, fill: 0xffffff, leading: 6 }
});
optionsText.position.set(panelX + 30, panelY + 65);
// Remove previous optionsText if it exists in menuContainer
menuContainer.children.forEach(c => { if (c.text && c.text.includes(':')) menuContainer.removeChild(c); });
menuContainer.addChild(optionsText);
}
function handleKeyPress(e) {
if (gameOver) {
if (e.code === 'Enter') location.reload();
return;
}
if (!isPaused) return;
if (menuOptions) {
if (e.key === '1') menuOptions[0]();
else if (e.key === '2') menuOptions[1]();
else if (e.key === '3') menuOptions[2]();
else if (e.key === '4') menuOptions[3]();
else return;
menuContainer.visible = false;
menuOptions = null;
isPaused = false;
}
}
function damagePlayer(amount) {
player.hp -= amount;
player.tint = 0xff0000;
setTimeout(() => { if (player) player.tint = 0xffffff; }, 80);
if (player.hp <= 0 && !gameOver) {
gameOver = true;
levelText.text = `GAME OVER\n\nKILLS: ${kills}\nLVL: ${level}\n\nENTER: Restart`;
levelText.visible = true;
}
}
function updateHUD() {
hudText.text = `LVL: ${level} XP: ${xp}/${xpToNext}`;
hpBarGraphics.clear();
const pct = Math.max(0, player.hp / player.maxHp);
hpBarGraphics.rect(5, 30, 60, 4).fill(0x333333);
hpBarGraphics.rect(5, 30, 60 * pct, 4).fill(0xe74c3c);
// Debug stats
if (Math.random() < 0.01) console.log(`DMG: ${player.damage}, Pierce: ${player.pierce || 0}, SpdBoost: ${player.speedBoost || 0}`);
}
function spawnParticle(x, y, color) {
for (let i = 0; i < 4; i++) {
const p = new PIXI.Sprite(textures.particle);
p.anchor.set(0.5);
p.position.set(x, y);
p.vx = (Math.random() - 0.5) * 3;
p.vy = (Math.random() - 0.5) * 3;
p.life = 15;
p.tint = color;
world.addChild(p);
particles.push(p);
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
p.alpha = Math.max(0, p.life / 15);
if (p.life <= 0) {
world.removeChild(p);
particles.splice(i, 1);
}
}
}
function dist(a, b) {
const dx = a.x - b.x, dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
init();