/** * Erzeugt alle Platzhalter-Texturen zur Laufzeit aus einfachen Formen. * * Damit ist das Spiel ohne eine einzige Bilddatei vollständig spielbar. Echte * Grafiken (Phase „Assets“) werden unter denselben Schlüsseln geladen; fehlt eine * Datei oder schlägt das Laden fehl, springt hier der Platzhalter ein. * * Konvention: Alle Figuren „schauen“ nach rechts (Winkel 0), damit * `setRotation(zielwinkel)` ohne Versatz funktioniert. * @module systems/Placeholders */ import { COLORS, ENEMY_TYPES, PLAYER } from '../config.js'; /** Alle Texturschlüssel, die das Spiel verwendet. */ export const TEXTURE_KEYS = Object.freeze({ player: 'player', enemy: (typeId) => `enemy-${typeId}`, bulletPlayer: 'bullet-player', bulletEnemy: 'bullet-enemy', bulletTurret: 'bullet-turret', turret: 'turret', supplyPod: 'supply-pod', ground: 'ground', particle: 'particle', crosshair: 'crosshair', emblem: 'emblem', }); /** * Zeichnet mit einer temporären Graphics-Instanz und speichert das Ergebnis als Textur. * Bereits vorhandene Texturen (z. B. geladene Grafiken) werden nicht überschrieben. * @param {Phaser.Scene} scene * @param {string} key * @param {number} width * @param {number} height * @param {(g: Phaser.GameObjects.Graphics) => void} draw */ function makeTexture(scene, key, width, height, draw) { if (scene.textures.exists(key)) return; const g = scene.make.graphics({ x: 0, y: 0 }, false); draw(g); g.generateTexture(key, width, height); g.destroy(); } function drawPlayer(g) { const s = PLAYER.radius * 2 + 12; const c = s / 2; // Umhang (hinten, also links) in den Farben der Vereinigten Erde. g.fillStyle(COLORS.playerCape, 1); g.fillTriangle(c - 4, c - 16, c - 4, c + 16, 2, c); g.fillStyle(COLORS.gold, 1); g.fillRect(c - 8, c - 2, 6, 4); // Rüstung g.fillStyle(COLORS.player, 1); g.fillCircle(c, c, PLAYER.radius - 2); g.lineStyle(3, COLORS.gold, 1); g.strokeCircle(c, c, PLAYER.radius - 2); // Geschlossener Helm mit Visier – bewusst ohne Gesicht. g.fillStyle(COLORS.navy, 1); g.fillRoundedRect(c + 2, c - 7, 9, 14, 3); // Waffe g.fillStyle(COLORS.navy, 1); g.fillRect(c + 6, c + 4, s / 2 - 6, 5); } function drawSwarm(g, size, variant) { const c = size / 2; const r = size / 2 - 3; g.lineStyle(2, COLORS.swarmDark, 1); // Beine for (let i = -1; i <= 1; i += 1) { g.lineBetween(c + i * r * 0.4, c - r * 0.5, c + i * r * 0.55, 1); g.lineBetween(c + i * r * 0.4, c + r * 0.5, c + i * r * 0.55, size - 1); } g.fillStyle(COLORS.swarm, 1); g.fillEllipse(c - r * 0.15, c, r * 1.6, r * 1.2); g.fillStyle(COLORS.swarmDark, 1); g.fillCircle(c + r * 0.6, c, r * 0.35); if (variant === 'medium') { // Sprungkralle: kräftige Klauen vorne g.lineStyle(3, COLORS.swarmDark, 1); g.lineBetween(c + r * 0.6, c - r * 0.3, size - 1, c - r * 0.6); g.lineBetween(c + r * 0.6, c + r * 0.3, size - 1, c + r * 0.6); } if (variant === 'elite') { // Brutmutter: Eiersack g.fillStyle(0xd9c27a, 1); for (let i = 0; i < 5; i += 1) g.fillCircle(c - r * 0.6 + (i % 2) * 6, c - 10 + i * 5, 5); } } function drawLegion(g, size, variant) { const c = size / 2; const r = size / 2 - 3; g.fillStyle(COLORS.legionDark, 1); if (variant === 'elite') { // Walzmeister: Ketten links und rechts g.fillRect(4, 2, size - 8, 8); g.fillRect(4, size - 10, size - 8, 8); } g.fillStyle(COLORS.legion, 1); if (variant === 'small') { g.fillCircle(c, c, r); } else { g.fillRoundedRect(c - r, c - r * 0.8, r * 1.7, r * 1.6, 5); } g.lineStyle(2, COLORS.legionDark, 1); g.strokeCircle(c, c, r * 0.55); // Rotes „Auge“ g.fillStyle(COLORS.enemyBullet, 1); g.fillCircle(c + r * 0.35, c, Math.max(2, r * 0.18)); // Waffenlauf(e) g.fillStyle(COLORS.legionDark, 1); if (variant === 'elite') { g.fillRect(c + r * 0.5, c - 10, r * 0.55, 6); g.fillRect(c + r * 0.5, c + 4, r * 0.55, 6); } else { g.fillRect(c + r * 0.5, c - 2, r * 0.55, 4); } } /** * Erzeugt alle Platzhalter, die noch nicht als echte Grafik geladen wurden. * @param {Phaser.Scene} scene */ export function createPlaceholderTextures(scene) { const playerSize = PLAYER.radius * 2 + 12; makeTexture(scene, TEXTURE_KEYS.player, playerSize, playerSize, drawPlayer); for (const type of Object.values(ENEMY_TYPES)) { const size = type.radius * 2 + 6; makeTexture(scene, TEXTURE_KEYS.enemy(type.id), size, size, (g) => type.faction === 'swarm' ? drawSwarm(g, size, type.tier) : drawLegion(g, size, type.tier), ); } makeTexture(scene, TEXTURE_KEYS.bulletPlayer, 14, 6, (g) => { g.fillStyle(COLORS.playerBullet, 1); g.fillRoundedRect(0, 0, 14, 6, 3); }); makeTexture(scene, TEXTURE_KEYS.bulletTurret, 14, 6, (g) => { g.fillStyle(COLORS.turretBullet, 1); g.fillRoundedRect(0, 0, 14, 6, 3); }); makeTexture(scene, TEXTURE_KEYS.bulletEnemy, 12, 12, (g) => { g.fillStyle(COLORS.enemyBullet, 1); g.fillCircle(6, 6, 6); g.fillStyle(0xffd0c0, 1); g.fillCircle(6, 6, 2.5); }); makeTexture(scene, TEXTURE_KEYS.turret, 48, 48, (g) => { g.fillStyle(COLORS.navy, 1); g.fillCircle(24, 24, 20); g.lineStyle(3, COLORS.turret, 1); g.strokeCircle(24, 24, 20); g.fillStyle(COLORS.turret, 1); g.fillRect(24, 20, 24, 8); }); makeTexture(scene, TEXTURE_KEYS.supplyPod, 44, 44, (g) => { // Kapsel mit Geschenkband – bewusst kein Kreuz (Rotkreuz-Zeichen ist geschützt). g.fillStyle(COLORS.supply, 1); g.fillRoundedRect(4, 4, 36, 36, 8); g.fillStyle(COLORS.cream, 1); g.fillRoundedRect(9, 9, 26, 26, 5); g.fillStyle(COLORS.gold, 1); g.fillRect(20, 9, 4, 26); g.fillRect(9, 20, 26, 4); g.fillCircle(22, 22, 4); }); makeTexture(scene, TEXTURE_KEYS.ground, 128, 128, (g) => { g.fillStyle(COLORS.ground, 1); g.fillRect(0, 0, 128, 128); g.fillStyle(COLORS.groundAlt, 1); g.fillRect(0, 0, 64, 64); g.fillRect(64, 64, 64, 64); g.lineStyle(1, 0x000000, 0.18); g.strokeRect(0, 0, 128, 128); }); makeTexture(scene, TEXTURE_KEYS.particle, 8, 8, (g) => { g.fillStyle(0xffffff, 1); g.fillCircle(4, 4, 4); }); makeTexture(scene, TEXTURE_KEYS.crosshair, 32, 32, (g) => { g.lineStyle(2, COLORS.gold, 1); g.strokeCircle(16, 16, 9); g.lineBetween(16, 0, 16, 9); g.lineBetween(16, 23, 16, 32); g.lineBetween(0, 16, 9, 16); g.lineBetween(23, 16, 32, 16); }); makeTexture(scene, TEXTURE_KEYS.emblem, 160, 160, (g) => { // Emblem der Vereinigten Erde: Planet mit Umlaufbahn (eigener Entwurf). g.fillStyle(COLORS.navy, 1); g.fillCircle(80, 80, 76); g.lineStyle(6, COLORS.gold, 1); g.strokeCircle(80, 80, 76); g.fillStyle(COLORS.crimson, 1); g.fillCircle(80, 80, 34); g.lineStyle(6, COLORS.gold, 1); g.strokeEllipse(80, 80, 132, 44); g.fillStyle(COLORS.cream, 1); g.fillCircle(136, 62, 8); }); }