import Phaser from 'phaser'; import { fontSize } from '../utils/responsive.js'; export default class HuntingScene extends Phaser.Scene { constructor() { super({ key: 'HuntingScene' }); } init(data) { this.inventory = data.inventory || { whaleOil: 0, fuel: 100, penguins: 0 }; this.whalesHunted = 0; this.harpoons = []; this.currentWhale = null; this.crosshairX = 400; this.crosshairY = 300; this.crosshairSpeed = 5; this.crosshairShakeAmount = 2; // Pixels of shake this.crosshairShakeTime = 0; // Timer for smooth shake oscillation this.useKeyboard = false; // Toggle between mouse and keyboard } create() { // Detect mobile device this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); // Default to mouse/touch mode on all platforms this.useKeyboard = false; // Ocean background this.add.rectangle(400, 300, 800, 600, 0x1e5a8e); this.createOceanWaves(); // UI elements this.createHUD(); this.createCrosshair(); this.createInstructions(); // Spawn single whale this.spawnWhale(); // Input setup this.setupInput(); // Setup ocean movement (camera sway) this.setupOceanMovement(); // Message display const shootMessage = this.isMobile ? 'Hunt whales! Tap to shoot harpoon.' : 'Hunt whales! Click or press SPACE to shoot harpoon.'; this.showMessage(shootMessage); } update() { // Update crosshair position based on input mode if (this.useKeyboard) { this.updateCrosshairKeyboard(); } else { this.updateCrosshairMouse(); } // Update crosshair sprite position with smooth shake this.crosshairShakeTime += 0.15; // Increment for smooth oscillation const shakeX = Math.sin(this.crosshairShakeTime * 3) * this.crosshairShakeAmount; const shakeY = Math.cos(this.crosshairShakeTime * 2.5) * this.crosshairShakeAmount; this.crosshair.setPosition(this.crosshairX + shakeX, this.crosshairY + shakeY); // Update harpoons this.updateHarpoons(); // Update whale this.updateWhale(); // Check collisions this.checkCollisions(); } setupInput() { // Keyboard controls this.cursors = this.input.keyboard.createCursorKeys(); this.wasd = this.input.keyboard.addKeys({ up: Phaser.Input.Keyboard.KeyCodes.W, down: Phaser.Input.Keyboard.KeyCodes.S, left: Phaser.Input.Keyboard.KeyCodes.A, right: Phaser.Input.Keyboard.KeyCodes.D }); this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); this.tabKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.TAB); // Mouse click to shoot this.input.on('pointerdown', (pointer) => { if (!this.useKeyboard) { this.shootHarpoon(); } }); // Space to shoot when using keyboard this.spaceKey.on('down', () => { this.shootHarpoon(); }); // Tab to toggle control mode (desktop only) if (!this.isMobile) { this.tabKey.on('down', () => { this.useKeyboard = !this.useKeyboard; this.controlModeText.setText(`Controls: ${this.useKeyboard ? 'KEYBOARD' : 'MOUSE'} (TAB to switch)`); if (this.useKeyboard) { this.input.setDefaultCursor('default'); } else { this.input.setDefaultCursor('none'); } }); } // Start with mouse control (hide cursor on desktop only) if (!this.isMobile) { this.input.setDefaultCursor(this.useKeyboard ? 'default' : 'none'); } } updateCrosshairMouse() { this.crosshairX = this.input.x; this.crosshairY = this.input.y; } updateCrosshairKeyboard() { // Arrow keys or WASD if (this.cursors.left.isDown || this.wasd.left.isDown) { this.crosshairX -= this.crosshairSpeed; } if (this.cursors.right.isDown || this.wasd.right.isDown) { this.crosshairX += this.crosshairSpeed; } if (this.cursors.up.isDown || this.wasd.up.isDown) { this.crosshairY -= this.crosshairSpeed; } if (this.cursors.down.isDown || this.wasd.down.isDown) { this.crosshairY += this.crosshairSpeed; } // Keep within bounds this.crosshairX = Phaser.Math.Clamp(this.crosshairX, 0, 800); this.crosshairY = Phaser.Math.Clamp(this.crosshairY, 0, 600); } createCrosshair() { const graphics = this.add.graphics(); graphics.lineStyle(3, 0xff0000, 1); // Crosshair lines graphics.beginPath(); graphics.moveTo(-20, 0); graphics.lineTo(-5, 0); graphics.moveTo(5, 0); graphics.lineTo(20, 0); graphics.moveTo(0, -20); graphics.lineTo(0, -5); graphics.moveTo(0, 5); graphics.lineTo(0, 20); graphics.strokePath(); // Center circle graphics.lineStyle(2, 0xff0000, 1); graphics.strokeCircle(0, 0, 3); // Convert to texture and create sprite graphics.generateTexture('crosshair', 40, 40); graphics.destroy(); this.crosshair = this.add.sprite(400, 300, 'crosshair'); this.crosshair.setDepth(100); this.crosshair.setScrollFactor(0); // Fixed to screen, ignores camera movement } createOceanWaves() { const graphics = this.add.graphics(); graphics.lineStyle(2, 0x4a90c4, 0.5); for (let i = 0; i < 12; i++) { const y = 50 + i * 50; graphics.beginPath(); for (let x = 0; x < 800; x += 20) { const waveY = y + Math.sin((x + i * 30) * 0.02) * 10; if (x === 0) { graphics.moveTo(x, waveY); } else { graphics.lineTo(x, waveY); } } graphics.strokePath(); } } createHUD() { // HUD background (fixed to screen, ignores camera movement) const hudBg = this.add.rectangle(400, 30, 780, 50, 0x000000, 0.7); hudBg.setStrokeStyle(2, 0xffffff); hudBg.setScrollFactor(0); // Stats (fixed to screen) this.statsText = this.add.text(20, 15, '', { fontSize: fontSize(16), fill: '#fff' }); this.statsText.setScrollFactor(0); // Control mode indicator (fixed to screen, desktop only) if (!this.isMobile) { this.controlModeText = this.add.text(400, 15, 'Controls: MOUSE (TAB to switch)', { fontSize: fontSize(16), fill: '#ffff00' }).setOrigin(0.5, 0); this.controlModeText.setScrollFactor(0); } // Return button (fixed to screen) const returnBtn = this.add.rectangle(750, 30, 80, 35, 0x8B0000); returnBtn.setInteractive({ useHandCursor: true }); returnBtn.setStrokeStyle(2, 0xffffff); returnBtn.setScrollFactor(0); const returnText = this.add.text(750, 30, 'RETURN', { fontSize: fontSize(14), fill: '#fff' }).setOrigin(0.5); returnText.setScrollFactor(0); returnBtn.on('pointerdown', () => { this.returnToMap(); }); this.updateStats(); } updateStats() { this.statsText.setText([ `Fuel: ${this.inventory.fuel}/100`, `Oil: ${this.inventory.whaleOil}/50`, `Whales: ${this.whalesHunted}` ]); } createInstructions() { this.messageText = this.add.text(400, 570, '', { fontSize: fontSize(16), fill: '#fff', backgroundColor: '#000000', padding: { x: 10, y: 5 } }).setOrigin(0.5); this.messageText.setScrollFactor(0); // Fixed to screen } showMessage(text) { this.messageText.setText(text); } spawnWhale() { // Don't spawn if there's already a whale if (this.currentWhale && this.currentWhale.getData('alive')) { return; } // Spawn randomly all over the screen const x = 100 + Math.random() * 600; // 100-700 (across screen with margins) const y = 150 + Math.random() * 350; // 150-500 (across screen with margins) const direction = Math.random() > 0.5 ? 1 : -1; // Create whale body const whale = this.add.container(x, y); // Whale shape (doubled size) const body = this.add.ellipse(0, 0, 160, 80, 0x2a2a2a); const tail = this.add.triangle( direction > 0 ? -90 : 90, 0, 0, -30, 0, 30, direction > 0 ? -40 : 40, 0, 0x1a1a1a ); const fin = this.add.triangle( direction > 0 ? 40 : -40, -10, direction > 0 ? 20 : -20, -50, direction > 0 ? 60 : -60, -10, 0x1a1a1a ); // Health bar background (doubled size and offset) const healthBg = this.add.rectangle(0, -70, 120, 12, 0x000000); // Health bar (will be updated as whale takes damage) const healthBar = this.add.rectangle(0, -70, 120, 12, 0x00ff00); whale.add([body, tail, fin, healthBg, healthBar]); whale.setData('direction', direction); whale.setData('alive', true); whale.setData('health', 3); // Requires 3 hits whale.setData('maxHealth', 3); whale.setData('bodyWidth', 160); whale.setData('bodyHeight', 80); whale.setData('healthBar', healthBar); whale.setData('bobTime', 0); // Timer for bobbing animation // Swimming movement whale.setData('swimSpeedX', direction * (0.2 + Math.random() * 0.3)); // 0.2-0.5 speed whale.setData('swimSpeedY', (Math.random() - 0.5) * 0.15); // Slight vertical drift // Diving mechanics whale.setData('diving', false); whale.setData('diveTimer', 0); whale.setData('nextDiveTime', 3 + Math.random() * 4); // Dive every 3-7 seconds // Flip if moving right if (direction > 0) { whale.setScale(-1, 1); } this.currentWhale = whale; } updateWhale() { if (!this.currentWhale) { return; } const whale = this.currentWhale; if (!whale.getData('alive')) { return; } // Update dive timer let diveTimer = whale.getData('diveTimer'); diveTimer += 0.016; // ~60fps whale.setData('diveTimer', diveTimer); // Handle diving behavior const diving = whale.getData('diving'); const nextDiveTime = whale.getData('nextDiveTime'); if (!diving && diveTimer >= nextDiveTime) { // Start diving this.startDive(whale); } else if (diving && diveTimer >= nextDiveTime + 2) { // Resurface after 2 seconds this.endDive(whale); } // Swimming movement let swimSpeedX = whale.getData('swimSpeedX'); let swimSpeedY = whale.getData('swimSpeedY'); // Update position with swimming whale.x += swimSpeedX; whale.y += swimSpeedY; // Bobbing animation (on top of swimming) let bobTime = whale.getData('bobTime'); bobTime += 0.02; whale.setData('bobTime', bobTime); const bobOffsetX = Math.sin(bobTime) * 15; const bobOffsetY = Math.cos(bobTime * 0.7) * 10; whale.x += bobOffsetX; whale.y += bobOffsetY; // Keep whale within visible bounds with better bounce behavior const margin = 100; // Larger margin to keep whale away from edges const minX = margin; const maxX = 800 - margin; const minY = 150; const maxY = 500; // Bounce off screen edges with push into play area if (whale.x < minX) { whale.x = minX + 20; // Push away from edge // Add random variation to bounce direction const newSpeed = Math.abs(swimSpeedX) + (Math.random() * 0.2); whale.setData('swimSpeedX', newSpeed); whale.setData('swimSpeedY', (Math.random() - 0.5) * 0.3); // Random Y direction whale.setScale(-Math.abs(whale.scaleX), whale.scaleY); } else if (whale.x > maxX) { whale.x = maxX - 20; // Push away from edge const newSpeed = -(Math.abs(swimSpeedX) + (Math.random() * 0.2)); whale.setData('swimSpeedX', newSpeed); whale.setData('swimSpeedY', (Math.random() - 0.5) * 0.3); // Random Y direction whale.setScale(Math.abs(whale.scaleX), whale.scaleY); } if (whale.y < minY) { whale.y = minY + 20; // Push away from edge whale.setData('swimSpeedY', Math.abs(swimSpeedY) + (Math.random() * 0.1)); } else if (whale.y > maxY) { whale.y = maxY - 20; // Push away from edge whale.setData('swimSpeedY', -(Math.abs(swimSpeedY) + (Math.random() * 0.1))); } // Update opacity based on diving if (diving) { whale.alpha = Math.max(0.2, whale.alpha - 0.05); } } startDive(whale) { whale.setData('diving', true); this.showMessage('Whale is diving!'); } endDive(whale) { whale.setData('diving', false); whale.setData('diveTimer', 0); whale.setData('nextDiveTime', 3 + Math.random() * 4); // Next dive in 3-7 seconds whale.alpha = 1; // Full opacity this.showMessage('Whale surfaced!'); } shootHarpoon() { // Create harpoon at crosshair position (fixed to screen) const harpoon = this.add.rectangle(this.crosshairX, this.crosshairY, 4, 20, 0x8B4513); harpoon.setData('speed', 8); harpoon.setData('active', true); harpoon.setScrollFactor(0); // Fixed to screen this.harpoons.push(harpoon); // Sound effect simulation (flash) - fixed to screen const flash = this.add.circle(this.crosshairX, this.crosshairY, 10, 0xffff00, 0.8); flash.setScrollFactor(0); this.tweens.add({ targets: flash, alpha: 0, scale: 2, duration: 200, onComplete: () => flash.destroy() }); } updateHarpoons() { for (let i = this.harpoons.length - 1; i >= 0; i--) { const harpoon = this.harpoons[i]; if (!harpoon.getData('active')) { continue; } // Move harpoon up harpoon.y -= harpoon.getData('speed'); // Remove if off screen if (harpoon.y < -20) { harpoon.destroy(); this.harpoons.splice(i, 1); } } } checkCollisions() { if (!this.currentWhale || !this.currentWhale.getData('alive')) { return; } const whale = this.currentWhale; // Can't hit whale while diving if (whale.getData('diving')) { return; } for (let h = this.harpoons.length - 1; h >= 0; h--) { const harpoon = this.harpoons[h]; if (!harpoon.getData('active')) { continue; } // Collision detection accounting for camera offset // Harpoon is in screen space (scrollFactor 0) // Whale is in world space, so convert to screen space const whaleScreenX = whale.x - this.cameras.main.scrollX; const whaleScreenY = whale.y - this.cameras.main.scrollY; const distance = Phaser.Math.Distance.Between( harpoon.x, harpoon.y, whaleScreenX, whaleScreenY ); if (distance < 80) { // Hit! (doubled radius for doubled whale size) this.hitWhale(whale, harpoon); break; } } } hitWhale(whale, harpoon) { harpoon.setData('active', false); harpoon.destroy(); // Reduce whale health let health = whale.getData('health'); health -= 1; whale.setData('health', health); // Update health bar const healthBar = whale.getData('healthBar'); const maxHealth = whale.getData('maxHealth'); const healthPercent = health / maxHealth; healthBar.setDisplaySize(120 * healthPercent, 12); // Doubled size // Change health bar color based on health if (healthPercent > 0.6) { healthBar.setFillStyle(0x00ff00); // Green } else if (healthPercent > 0.3) { healthBar.setFillStyle(0xffff00); // Yellow } else { healthBar.setFillStyle(0xff0000); // Red } // Hit flash effect const hitFlash = this.add.circle(whale.x, whale.y, 50, 0xff0000, 0.6); this.tweens.add({ targets: hitFlash, alpha: 0, scale: 1.5, duration: 300, onComplete: () => hitFlash.destroy() }); // Whale damaged animation (shake) this.tweens.add({ targets: whale, x: whale.x + 5, yoyo: true, repeat: 2, duration: 50 }); if (health <= 0) { // Whale is dead! this.killWhale(whale); } else { // Whale is still alive this.showMessage(`Hit! Whale health: ${health}/${maxHealth}`); } } killWhale(whale) { whale.setData('alive', false); // Check if enough fuel to process the whale if (this.inventory.fuel < 2) { // Not enough fuel to cook the oil! this.tweens.add({ targets: whale, alpha: 0, y: whale.y + 50, duration: 1000, onComplete: () => { whale.destroy(); this.currentWhale = null; // Spawn new whale this.time.delayedCall(1500, () => { this.spawnWhale(); }); } }); this.showMessage('Whale killed but no fuel to process it! Need 2 fuel to cook whale oil.'); return; } // Whale death animation this.tweens.add({ targets: whale, alpha: 0, y: whale.y + 50, angle: 90, duration: 1000, onComplete: () => { whale.destroy(); this.currentWhale = null; // Spawn new whale after delay this.time.delayedCall(1500, () => { this.spawnWhale(); }); } }); // Consume fuel for processing this.inventory.fuel -= 2; // Reward this.inventory.whaleOil += 1; this.whalesHunted++; this.updateStats(); // Show success message this.showMessage(`Whale killed! +1 Whale Oil, -2 Fuel (Oil: ${this.inventory.whaleOil}, Fuel: ${this.inventory.fuel})`); // Success flash (fixed to screen) const flash = this.add.rectangle(400, 300, 800, 600, 0xffffff, 0.3); flash.setScrollFactor(0); this.tweens.add({ targets: flash, alpha: 0, duration: 300, onComplete: () => flash.destroy() }); } setupOceanMovement() { // Create continuous camera sway to simulate ocean movement // This makes the whole scene move as if on a rocking ship // Initial camera position offset this.cameraTime = 0; // Store this in the update loop by creating a timer this.time.addEvent({ delay: 16, // ~60fps callback: () => { this.cameraTime += 0.1; // Faster oscillation (~1 second period) // Smooth sine wave movement for natural ocean sway const swayX = Math.sin(this.cameraTime) * 25; // 25 pixels horizontal sway const swayY = Math.cos(this.cameraTime * 0.6) * 18; // 18 pixels vertical sway // Apply to camera scroll this.cameras.main.scrollX = swayX; this.cameras.main.scrollY = swayY; }, loop: true }); } returnToMap() { // Restore default cursor before leaving this.input.setDefaultCursor('default'); this.scene.start('MapScene', { inventory: this.inventory }); } }