// FLAW FIGHTER COMBAT - Side-scrolling argument slasher // Built with Phaser 3 class FlawFighterCombat extends Phaser.Scene { constructor() { super({ key: 'FlawFighterCombat' }); } init() { this.score = 0; this.combo = 0; this.maxCombo = 0; this.lives = 5; this.wave = 1; this.enemiesKilled = 0; this.selectedWeapon = null; this.gameOver = false; this.isPaused = false; } preload() { // We'll create graphics programmatically } create() { // Background layers for parallax this.createBackground(); // Player this.createPlayer(); // Weapon selector (bottom HUD) this.createWeaponHUD(); // Score/combo HUD this.createScoreHUD(); // Enemy group this.enemies = this.add.group(); // Particles this.createParticles(); // Start spawning this.spawnTimer = this.time.addEvent({ delay: 2500, callback: this.spawnEnemy, callbackScope: this, loop: true }); // Wave timer - speeds up over time this.time.addEvent({ delay: 15000, callback: this.nextWave, callbackScope: this, loop: true }); // Spawn first enemy quickly this.time.delayedCall(500, () => this.spawnEnemy()); // Instructions this.showInstructions(); } createBackground() { // Gradient background const bg = this.add.graphics(); bg.fillGradientStyle(0x1a1a2e, 0x1a1a2e, 0x16213e, 0x16213e, 1); bg.fillRect(0, 0, 800, 500); // Parallax stars/particles for (let i = 0; i < 50; i++) { const star = this.add.circle( Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 400), Phaser.Math.Between(1, 3), 0xffffff, Phaser.Math.FloatBetween(0.1, 0.5) ); this.tweens.add({ targets: star, alpha: { from: star.alpha, to: star.alpha * 0.3 }, duration: Phaser.Math.Between(1000, 3000), yoyo: true, repeat: -1 }); } // Ground line this.add.rectangle(400, 420, 800, 4, 0x4ecdc4, 0.3); } createPlayer() { // Player character - a logic warrior this.player = this.add.container(100, 350); // Body const body = this.add.circle(0, 0, 30, 0x4ecdc4); const face = this.add.circle(0, -5, 20, 0x1a1a2e); const eyeL = this.add.circle(-8, -8, 5, 0xffffff); const eyeR = this.add.circle(8, -8, 5, 0xffffff); const pupilL = this.add.circle(-8, -8, 2, 0x000000); const pupilR = this.add.circle(8, -8, 2, 0x000000); // Sword this.sword = this.add.rectangle(40, 0, 50, 8, 0xf9d423); this.sword.setOrigin(0, 0.5); this.player.add([body, face, eyeL, eyeR, pupilL, pupilR, this.sword]); // Idle animation this.tweens.add({ targets: this.player, y: 345, duration: 1000, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); } createWeaponHUD() { this.flawTypes = [ { name: "CAUSE", full: "Causation ≠ Correlation", color: 0xff6b6b, key: '1' }, { name: "GENERAL", full: "Overgeneralization", color: 0x4ecdc4, key: '2' }, { name: "AD HOM", full: "Ad Hominem", color: 0xf9d423, key: '3' }, { name: "CIRCULAR", full: "Circular Reasoning", color: 0x667eea, key: '4' }, { name: "SAMPLE", full: "Sampling Error", color: 0x44a08d, key: '5' }, ]; this.weaponButtons = []; // HUD background this.add.rectangle(400, 470, 800, 60, 0x000000, 0.7); this.add.text(400, 442, '⚔️ SELECT WEAPON (1-5) THEN CLICK ENEMY', { fontSize: '10px', color: '#888888' }).setOrigin(0.5); this.flawTypes.forEach((flaw, i) => { const x = 90 + (i * 155); const btn = this.add.rectangle(x, 472, 145, 40, flaw.color, 0.8) .setInteractive({ useHandCursor: true }) .on('pointerdown', () => this.selectWeapon(i)) .on('pointerover', () => { if (this.selectedWeapon !== i) btn.setAlpha(1); }) .on('pointerout', () => { if (this.selectedWeapon !== i) btn.setAlpha(0.8); }); const txt = this.add.text(x, 472, `[${flaw.key}] ${flaw.name}`, { fontSize: '12px', color: '#ffffff', fontStyle: 'bold' }).setOrigin(0.5); this.weaponButtons.push({ btn, txt, flaw }); }); // Keyboard shortcuts this.input.keyboard.on('keydown-ONE', () => this.selectWeapon(0)); this.input.keyboard.on('keydown-TWO', () => this.selectWeapon(1)); this.input.keyboard.on('keydown-THREE', () => this.selectWeapon(2)); this.input.keyboard.on('keydown-FOUR', () => this.selectWeapon(3)); this.input.keyboard.on('keydown-FIVE', () => this.selectWeapon(4)); } selectWeapon(index) { if (this.gameOver) return; // Deselect previous this.weaponButtons.forEach((wb, i) => { wb.btn.setStrokeStyle(i === index ? 4 : 0, 0xffffff); wb.btn.setAlpha(i === index ? 1 : 0.6); }); this.selectedWeapon = index; // Change sword color this.sword.setFillStyle(this.flawTypes[index].color); // Sword flourish this.tweens.add({ targets: this.sword, angle: 30, duration: 100, yoyo: true, ease: 'Power2' }); } createScoreHUD() { // Score this.scoreText = this.add.text(20, 15, 'SCORE: 0', { fontSize: '20px', color: '#ffffff', fontStyle: 'bold' }); // Combo this.comboText = this.add.text(20, 40, '', { fontSize: '16px', color: '#f9d423', fontStyle: 'bold' }); // Lives this.livesText = this.add.text(700, 15, '❤️'.repeat(this.lives), { fontSize: '18px' }); // Wave this.waveText = this.add.text(400, 15, 'WAVE 1', { fontSize: '18px', color: '#4ecdc4', fontStyle: 'bold' }).setOrigin(0.5, 0); // Big combo popup (hidden initially) this.bigComboText = this.add.text(400, 200, '', { fontSize: '48px', color: '#f9d423', fontStyle: 'bold', stroke: '#000000', strokeThickness: 6 }).setOrigin(0.5).setAlpha(0); } createParticles() { // Hit particles this.hitEmitter = this.add.particles(0, 0, { speed: { min: 100, max: 300 }, angle: { min: 0, max: 360 }, scale: { start: 0.5, end: 0 }, lifespan: 500, gravityY: 300, emitting: false }); // We'll create manual particle bursts instead } showInstructions() { const overlay = this.add.rectangle(400, 250, 600, 300, 0x000000, 0.9); const title = this.add.text(400, 130, '⚔️ FLAW FIGHTER ⚔️', { fontSize: '32px', color: '#4ecdc4', fontStyle: 'bold' }).setOrigin(0.5); const instructions = this.add.text(400, 220, '🎮 HOW TO PLAY:\n\n' + '1. Enemies (bad arguments) march toward you\n' + '2. Read their flaw type on the sign above them\n' + '3. Select the matching weapon (1-5 keys or click)\n' + '4. Click the enemy to SLASH them!\n\n' + '⚡ Build COMBOS for bonus points!\n' + '💀 Miss 5 enemies = Game Over', { fontSize: '14px', color: '#ffffff', align: 'center', lineSpacing: 8 } ).setOrigin(0.5); const startBtn = this.add.rectangle(400, 350, 200, 50, 0x4ecdc4) .setInteractive({ useHandCursor: true }) .on('pointerdown', () => { overlay.destroy(); title.destroy(); instructions.destroy(); startBtn.destroy(); startTxt.destroy(); }) .on('pointerover', () => startBtn.setFillStyle(0x6ee7e7)) .on('pointerout', () => startBtn.setFillStyle(0x4ecdc4)); const startTxt = this.add.text(400, 350, '🎮 START BATTLE', { fontSize: '18px', color: '#000000', fontStyle: 'bold' }).setOrigin(0.5); } spawnEnemy() { if (this.gameOver || this.isPaused) return; const flaws = [ { text: "Coffee sales rose when crime dropped.\nCoffee prevents crime!", flaw: "CAUSE" }, { text: "My friend tried it and failed.\nIt never works.", flaw: "GENERAL" }, { text: "You can't trust him.\nHe's biased.", flaw: "AD HOM" }, { text: "This is good because\nit's beneficial.", flaw: "CIRCULAR" }, { text: "3 people I know love it.\nEveryone does!", flaw: "SAMPLE" }, { text: "Rooster crows before sunrise.\nRoosters cause the sun!", flaw: "CAUSE" }, { text: "One study showed X.\nX is always true.", flaw: "GENERAL" }, { text: "She's wrong because she\nprofits from being right.", flaw: "AD HOM" }, { text: "We should do this because\nit's the right thing.", flaw: "CIRCULAR" }, { text: "My Twitter followers agree.\nThe public agrees!", flaw: "SAMPLE" }, { text: "Ice cream sales correlate with\ndrowning. Ice cream kills!", flaw: "CAUSE" }, { text: "I met two rude New Yorkers.\nAll New Yorkers are rude.", flaw: "GENERAL" }, ]; const data = Phaser.Utils.Array.GetRandom(flaws); const flawInfo = this.flawTypes.find(f => f.name === data.flaw); // Enemy container const enemy = this.add.container(850, 320); enemy.flawType = data.flaw; enemy.speed = 0.5 + (this.wave * 0.1); enemy.isAlive = true; // Enemy body (menacing blob) const body = this.add.circle(0, 0, 35, 0xff4757); const eyeL = this.add.circle(-12, -10, 8, 0xffffff); const eyeR = this.add.circle(12, -10, 8, 0xffffff); const pupilL = this.add.circle(-14, -10, 4, 0x000000); const pupilR = this.add.circle(10, -10, 4, 0x000000); const mouth = this.add.rectangle(0, 12, 25, 6, 0x000000); // Flaw sign above enemy const signBg = this.add.rectangle(0, -80, 160, 60, 0x000000, 0.8); const signText = this.add.text(0, -80, data.text, { fontSize: '10px', color: '#ffffff', align: 'center', wordWrap: { width: 150 } }).setOrigin(0.5); // Flaw type indicator const flawBadge = this.add.rectangle(0, -110, 80, 20, flawInfo.color); const flawLabel = this.add.text(0, -110, `??? ${data.flaw}`, { fontSize: '10px', color: '#ffffff', fontStyle: 'bold' }).setOrigin(0.5); enemy.add([body, eyeL, eyeR, pupilL, pupilR, mouth, signBg, signText, flawBadge, flawLabel]); enemy.body = body; // Make clickable body.setInteractive({ useHandCursor: true }) .on('pointerdown', () => this.attackEnemy(enemy)); // Hover effect body.on('pointerover', () => { if (enemy.isAlive) body.setScale(1.1); }); body.on('pointerout', () => { if (enemy.isAlive) body.setScale(1); }); // Bobbing animation this.tweens.add({ targets: enemy, y: 330, duration: 500, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' }); this.enemies.add(enemy); } attackEnemy(enemy) { if (this.gameOver || !enemy.isAlive) return; if (this.selectedWeapon === null) { // Flash weapon bar this.cameras.main.flash(100, 255, 0, 0); return; } const weaponFlaw = this.flawTypes[this.selectedWeapon].name; if (weaponFlaw === enemy.flawType) { // HIT! this.hitEnemy(enemy); } else { // MISS - wrong weapon this.missAttack(enemy); } } hitEnemy(enemy) { enemy.isAlive = false; this.enemiesKilled++; this.combo++; if (this.combo > this.maxCombo) this.maxCombo = this.combo; // Score with combo multiplier const multiplier = Math.min(Math.floor(this.combo / 3) + 1, 5); const points = 100 * multiplier; this.score += points; this.scoreText.setText(`SCORE: ${this.score}`); // Combo display if (this.combo >= 3) { this.comboText.setText(`🔥 ${this.combo}x COMBO!`); // Big combo popup for milestones if (this.combo === 5 || this.combo === 10 || this.combo === 15 || this.combo === 20) { this.showBigCombo(this.combo); } } // Slash animation this.tweens.add({ targets: this.sword, angle: 90, duration: 100, yoyo: true, ease: 'Power4' }); // Screen shake this.cameras.main.shake(100, 0.01); // Particle burst this.createHitParticles(enemy.x, enemy.y, this.flawTypes[this.selectedWeapon].color); // Points popup const pointsPopup = this.add.text(enemy.x, enemy.y - 50, `+${points}`, { fontSize: '24px', color: '#4ecdc4', fontStyle: 'bold', stroke: '#000000', strokeThickness: 4 }).setOrigin(0.5); this.tweens.add({ targets: pointsPopup, y: enemy.y - 120, alpha: 0, duration: 800, ease: 'Power2', onComplete: () => pointsPopup.destroy() }); // Death animation this.tweens.add({ targets: enemy, scaleX: 1.5, scaleY: 0, alpha: 0, duration: 200, ease: 'Power2', onComplete: () => { enemy.destroy(); } }); // Flash green this.cameras.main.flash(50, 0, 255, 100); } createHitParticles(x, y, color) { for (let i = 0; i < 15; i++) { const particle = this.add.circle( x, y, Phaser.Math.Between(3, 8), color ); const angle = Phaser.Math.FloatBetween(0, Math.PI * 2); const speed = Phaser.Math.Between(100, 300); this.tweens.add({ targets: particle, x: x + Math.cos(angle) * speed, y: y + Math.sin(angle) * speed + 100, alpha: 0, scale: 0, duration: 500, ease: 'Power2', onComplete: () => particle.destroy() }); } } showBigCombo(combo) { this.bigComboText.setText(`🔥 ${combo}x COMBO! 🔥`); this.bigComboText.setAlpha(1); this.bigComboText.setScale(0.5); this.tweens.add({ targets: this.bigComboText, scale: 1.2, duration: 200, ease: 'Back.easeOut', onComplete: () => { this.tweens.add({ targets: this.bigComboText, alpha: 0, duration: 1000, delay: 500 }); } }); } missAttack(enemy) { // Wrong weapon - enemy gets angry but doesn't die this.combo = 0; this.comboText.setText(''); // Flash enemy red this.tweens.add({ targets: enemy.body, fillColor: 0xffffff, duration: 50, yoyo: true, repeat: 2 }); // Screen shake this.cameras.main.shake(50, 0.005); // Wrong popup const wrongPopup = this.add.text(enemy.x, enemy.y - 50, '❌ WRONG WEAPON!', { fontSize: '14px', color: '#ff6b6b', fontStyle: 'bold', stroke: '#000000', strokeThickness: 3 }).setOrigin(0.5); this.tweens.add({ targets: wrongPopup, y: enemy.y - 100, alpha: 0, duration: 800, onComplete: () => wrongPopup.destroy() }); } enemyReachedPlayer(enemy) { if (!enemy.isAlive) return; enemy.isAlive = false; this.lives--; this.combo = 0; this.comboText.setText(''); this.livesText.setText('❤️'.repeat(Math.max(0, this.lives))); // Big shake this.cameras.main.shake(300, 0.02); this.cameras.main.flash(200, 255, 0, 0); // Damage popup const dmgPopup = this.add.text(150, 300, '💀 -1 LIFE', { fontSize: '24px', color: '#ff0000', fontStyle: 'bold', stroke: '#000000', strokeThickness: 4 }).setOrigin(0.5); this.tweens.add({ targets: dmgPopup, y: 200, alpha: 0, duration: 1000, onComplete: () => dmgPopup.destroy() }); // Destroy enemy this.tweens.add({ targets: enemy, alpha: 0, duration: 200, onComplete: () => enemy.destroy() }); if (this.lives <= 0) { this.endGame(); } } nextWave() { if (this.gameOver) return; this.wave++; this.waveText.setText(`WAVE ${this.wave}`); // Speed up spawning this.spawnTimer.delay = Math.max(1000, 2500 - (this.wave * 200)); // Wave announcement const waveAnnounce = this.add.text(400, 200, `⚔️ WAVE ${this.wave} ⚔️`, { fontSize: '36px', color: '#f9d423', fontStyle: 'bold', stroke: '#000000', strokeThickness: 6 }).setOrigin(0.5).setAlpha(0); this.tweens.add({ targets: waveAnnounce, alpha: 1, scale: { from: 0.5, to: 1.2 }, duration: 500, ease: 'Back.easeOut', onComplete: () => { this.tweens.add({ targets: waveAnnounce, alpha: 0, duration: 500, delay: 1000, onComplete: () => waveAnnounce.destroy() }); } }); } endGame() { this.gameOver = true; this.spawnTimer.remove(); // Stop all enemies this.enemies.getChildren().forEach(e => { e.speed = 0; }); // Overlay const overlay = this.add.rectangle(400, 250, 800, 500, 0x000000, 0.85); // Game Over text const goText = this.add.text(400, 120, '💀 GAME OVER 💀', { fontSize: '42px', color: '#ff6b6b', fontStyle: 'bold' }).setOrigin(0.5); // Stats const stats = this.add.text(400, 220, `Final Score: ${this.score}\n\n` + `Enemies Slain: ${this.enemiesKilled}\n` + `Max Combo: ${this.maxCombo}x\n` + `Waves Survived: ${this.wave}`, { fontSize: '20px', color: '#ffffff', align: 'center', lineSpacing: 10 } ).setOrigin(0.5); // Restart button const restartBtn = this.add.rectangle(400, 380, 200, 50, 0x4ecdc4) .setInteractive({ useHandCursor: true }) .on('pointerdown', () => this.scene.restart()) .on('pointerover', () => restartBtn.setFillStyle(0x6ee7e7)) .on('pointerout', () => restartBtn.setFillStyle(0x4ecdc4)); this.add.text(400, 380, '🔄 FIGHT AGAIN', { fontSize: '18px', color: '#000000', fontStyle: 'bold' }).setOrigin(0.5); } update() { if (this.gameOver) return; // Move enemies toward player this.enemies.getChildren().forEach(enemy => { if (!enemy.isAlive) return; enemy.x -= enemy.speed; // Check if reached player if (enemy.x < 150) { this.enemyReachedPlayer(enemy); } }); } } // Export for use if (typeof window !== 'undefined') { window.FlawFighterCombat = FlawFighterCombat; }