Initial commit: Whale hunting adventure game

- Set up Phaser 3 game framework with Vite
- Created ship deck scene with interactive objects
- Implemented navigation map with destination selection
- Added transition screens for travel between locations
- Inventory system for tracking fuel, whale oil, and penguins
- Three destination types: hunting grounds, Antarctic islands, and port

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Richter
2025-12-15 03:21:05 +01:00
commit 4190170f1f
9 changed files with 1788 additions and 0 deletions

26
src/main.js Normal file
View File

@@ -0,0 +1,26 @@
import Phaser from 'phaser';
import ShipDeckScene from './scenes/ShipDeckScene.js';
import MapScene from './scenes/MapScene.js';
import TransitionScene from './scenes/TransitionScene.js';
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'game-container',
backgroundColor: '#2d5f8e',
scene: [ShipDeckScene, MapScene, TransitionScene],
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: false
}
},
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
}
};
const game = new Phaser.Game(config);

198
src/scenes/MapScene.js Normal file
View File

@@ -0,0 +1,198 @@
import Phaser from 'phaser';
export default class MapScene extends Phaser.Scene {
constructor() {
super({ key: 'MapScene' });
}
init(data) {
// Receive inventory data from previous scene
this.inventory = data.inventory || { whaleOil: 0, fuel: 100, penguins: 0 };
}
create() {
// Ocean background
this.add.rectangle(400, 300, 800, 600, 0x1e5a8e);
// Title
this.add.text(400, 30, 'Navigation Map - Choose Your Destination', {
fontSize: '24px',
fill: '#fff',
fontStyle: 'bold'
}).setOrigin(0.5);
// Draw some decorative waves
this.drawWaves();
// Current position marker (The Ship)
this.createLocation(400, 300, 'SHIP', 0x8B4513,
'Your current position. Click to return to ship deck.',
() => this.returnToShip());
// Hunting Grounds
this.createLocation(250, 200, 'HUNTING\nGROUNDS', 0x2d5f8e,
'Rich waters where whales gather. Dangerous but profitable!',
() => this.goToHunting());
// Antarctic Island (for penguins)
this.createLocation(600, 450, 'ANTARCTIC\nISLAND', 0xE0E0E0,
'Cold, desolate islands inhabited by penguins...',
() => this.goToAntarctic());
// Port
this.createLocation(150, 500, 'PORT', 0x654321,
'Return to port to resupply fuel and sell whale oil.',
() => this.goToPort());
// Inventory display
this.createInventoryDisplay();
// Message box
this.createMessageBox();
this.showMessage('Select a destination. Watch your fuel levels!');
// Close map button
const closeButton = this.add.rectangle(750, 50, 80, 40, 0x8B0000);
closeButton.setInteractive({ useHandCursor: true });
closeButton.setStrokeStyle(2, 0xffffff);
this.add.text(750, 50, 'CLOSE', {
fontSize: '16px',
fill: '#fff'
}).setOrigin(0.5);
closeButton.on('pointerdown', () => {
this.returnToShip();
});
}
drawWaves() {
// Decorative wave lines
const graphics = this.add.graphics();
graphics.lineStyle(2, 0x4a90c4, 0.5);
for (let i = 0; i < 10; i++) {
const y = 100 + 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();
}
}
createLocation(x, y, name, color, description, onClick) {
// Location marker
const marker = this.add.circle(x, y, 30, color);
marker.setInteractive({ useHandCursor: true });
marker.setStrokeStyle(3, 0xffffff);
// Location name
const text = this.add.text(x, y, name, {
fontSize: '12px',
fill: '#fff',
fontStyle: 'bold',
align: 'center'
}).setOrigin(0.5);
// Hover effect
marker.on('pointerover', () => {
marker.setScale(1.1);
this.showMessage(description);
});
marker.on('pointerout', () => {
marker.setScale(1);
});
marker.on('pointerdown', onClick);
}
createInventoryDisplay() {
const panel = this.add.rectangle(80, 80, 140, 100, 0x000000, 0.8);
panel.setStrokeStyle(2, 0xffffff);
this.inventoryText = this.add.text(20, 40, '', {
fontSize: '14px',
fill: '#fff'
});
this.updateInventoryDisplay();
}
updateInventoryDisplay() {
this.inventoryText.setText([
'Inventory:',
`Fuel: ${this.inventory.fuel}`,
`Oil: ${this.inventory.whaleOil}`,
`Penguins: ${this.inventory.penguins}`
]);
}
createMessageBox() {
this.messageBox = this.add.rectangle(400, 560, 760, 60, 0x000000, 0.8);
this.messageBox.setStrokeStyle(2, 0xcccccc);
this.messageText = this.add.text(400, 560, '', {
fontSize: '16px',
fill: '#fff',
wordWrap: { width: 740 },
align: 'center'
}).setOrigin(0.5);
}
showMessage(message) {
this.messageText.setText(message);
}
returnToShip() {
this.scene.start('ShipDeckScene', { inventory: this.inventory });
}
goToHunting() {
if (this.inventory.fuel < 20) {
this.showMessage('Not enough fuel to reach the hunting grounds! You need at least 20 units.');
return;
}
// Go to transition scene, then to map (hunting scene not yet implemented)
this.scene.start('TransitionScene', {
inventory: this.inventory,
destination: 'hunting',
fuelCost: 20,
nextScene: 'MapScene' // Will change to 'HuntingScene' when implemented
});
}
goToAntarctic() {
if (this.inventory.fuel < 30) {
this.showMessage('Not enough fuel to reach Antarctic islands! You need at least 30 units.');
return;
}
// Go to transition scene, then to map (antarctic scene not yet implemented)
this.scene.start('TransitionScene', {
inventory: this.inventory,
destination: 'antarctic',
fuelCost: 30,
nextScene: 'MapScene' // Will change to 'AntarcticScene' when implemented
});
}
goToPort() {
if (this.inventory.fuel < 10) {
this.showMessage('Not enough fuel to return to port! You need at least 10 units. Consider... alternatives.');
return;
}
// Go to transition scene, then to map (port scene not yet implemented)
this.scene.start('TransitionScene', {
inventory: this.inventory,
destination: 'port',
fuelCost: 10,
nextScene: 'MapScene' // Will change to 'PortScene' when implemented
});
}
}

165
src/scenes/ShipDeckScene.js Normal file
View File

@@ -0,0 +1,165 @@
import Phaser from 'phaser';
export default class ShipDeckScene extends Phaser.Scene {
constructor() {
super({ key: 'ShipDeckScene' });
this.inventory = {
whaleOil: 0,
fuel: 100,
penguins: 0
};
}
init(data) {
// Receive inventory data when returning from other scenes
if (data && data.inventory) {
this.inventory = data.inventory;
}
}
create() {
// Background - ship deck
this.add.rectangle(400, 300, 800, 600, 0x8B4513);
this.add.text(400, 30, 'Ship Deck - The Whaling Vessel', {
fontSize: '24px',
fill: '#fff'
}).setOrigin(0.5);
// Create interactive areas
this.createDeck();
this.createBarrels();
this.createWheel();
this.createInventoryDisplay();
this.createMessageBox();
// Instructions
this.add.text(400, 560, 'Click on objects to interact', {
fontSize: '16px',
fill: '#ffff99'
}).setOrigin(0.5);
}
createDeck() {
// Ship deck planks
for (let i = 0; i < 8; i++) {
this.add.rectangle(400, 200 + i * 30, 700, 25, 0x654321)
.setStrokeStyle(2, 0x3d2817);
}
}
createBarrels() {
// Fuel barrel
const fuelBarrel = this.add.rectangle(150, 350, 60, 80, 0x8B0000);
fuelBarrel.setInteractive({ useHandCursor: true });
fuelBarrel.setStrokeStyle(3, 0x000000);
this.add.text(150, 350, 'FUEL', {
fontSize: '12px',
fill: '#fff'
}).setOrigin(0.5);
fuelBarrel.on('pointerdown', () => {
this.showMessage(`Fuel barrel: ${this.inventory.fuel} units remaining. This keeps the ship running.`);
});
// Whale oil barrel
const oilBarrel = this.add.rectangle(250, 350, 60, 80, 0x4B4B00);
oilBarrel.setInteractive({ useHandCursor: true });
oilBarrel.setStrokeStyle(3, 0x000000);
this.add.text(250, 350, 'OIL', {
fontSize: '12px',
fill: '#fff'
}).setOrigin(0.5);
oilBarrel.on('pointerdown', () => {
this.showMessage(`Whale oil: ${this.inventory.whaleOil} barrels. Your precious cargo!`);
});
// Penguin cage (dark humor ahead)
const penguinCage = this.add.rectangle(650, 350, 80, 80, 0x333333);
penguinCage.setInteractive({ useHandCursor: true });
penguinCage.setStrokeStyle(3, 0x000000);
this.add.text(650, 350, '🐧', {
fontSize: '32px'
}).setOrigin(0.5);
penguinCage.on('pointerdown', () => {
if (this.inventory.penguins > 0) {
this.showMessage(`${this.inventory.penguins} penguins. They're... emergency fuel. Dark times call for dark measures.`);
} else {
this.showMessage('Empty penguin cage. Should we catch some for... fuel?');
}
});
}
createWheel() {
// Ship's wheel
const wheel = this.add.circle(550, 200, 40, 0x8B4513);
wheel.setInteractive({ useHandCursor: true });
wheel.setStrokeStyle(4, 0x654321);
// Wheel spokes
for (let i = 0; i < 8; i++) {
const angle = (i * 45) * Math.PI / 180;
const x1 = 550 + Math.cos(angle) * 15;
const y1 = 200 + Math.sin(angle) * 15;
const x2 = 550 + Math.cos(angle) * 35;
const y2 = 200 + Math.sin(angle) * 35;
this.add.line(0, 0, x1, y1, x2, y2, 0x654321).setLineWidth(3);
}
wheel.on('pointerdown', () => {
// Open the map scene
this.scene.start('MapScene', { inventory: this.inventory });
});
}
createInventoryDisplay() {
// Inventory panel
const panel = this.add.rectangle(80, 80, 140, 100, 0x000000, 0.7);
panel.setStrokeStyle(2, 0xffffff);
this.inventoryText = this.add.text(20, 40, '', {
fontSize: '14px',
fill: '#fff'
});
this.updateInventoryDisplay();
}
updateInventoryDisplay() {
this.inventoryText.setText([
'Inventory:',
`Fuel: ${this.inventory.fuel}`,
`Oil: ${this.inventory.whaleOil}`,
`Penguins: ${this.inventory.penguins}`
]);
}
createMessageBox() {
// Message display area
this.messageBox = this.add.rectangle(400, 500, 760, 80, 0x000000, 0.8);
this.messageBox.setStrokeStyle(2, 0xcccccc);
this.messageText = this.add.text(400, 500, 'Welcome aboard! Click around to explore the ship.', {
fontSize: '16px',
fill: '#fff',
wordWrap: { width: 740 },
align: 'center'
}).setOrigin(0.5);
}
showMessage(message) {
this.messageText.setText(message);
}
// Helper method to modify inventory (for future use)
addToInventory(item, amount) {
if (this.inventory.hasOwnProperty(item)) {
this.inventory[item] += amount;
this.updateInventoryDisplay();
}
}
}

View File

@@ -0,0 +1,282 @@
import Phaser from 'phaser';
export default class TransitionScene extends Phaser.Scene {
constructor() {
super({ key: 'TransitionScene' });
}
init(data) {
// Receive data about the destination
this.inventory = data.inventory || { whaleOil: 0, fuel: 100, penguins: 0 };
this.destination = data.destination || 'unknown';
this.fuelCost = data.fuelCost || 0;
this.returnScene = data.returnScene || 'MapScene';
this.nextScene = data.nextScene || 'MapScene';
}
create() {
// Get destination-specific content
const content = this.getDestinationContent(this.destination);
// Background based on destination
this.add.rectangle(400, 300, 800, 600, content.backgroundColor);
// Add destination-specific visual elements
this.createDestinationVisuals(this.destination, content);
// Journey text container
const textBox = this.add.rectangle(400, 450, 700, 200, 0x000000, 0.85);
textBox.setStrokeStyle(3, 0xffffff);
// Destination title
this.add.text(400, 360, content.title, {
fontSize: '32px',
fill: '#ffffff',
fontStyle: 'bold',
stroke: '#000000',
strokeThickness: 4
}).setOrigin(0.5);
// Journey description
this.add.text(400, 430, content.description, {
fontSize: '18px',
fill: '#ffffff',
align: 'center',
wordWrap: { width: 660 }
}).setOrigin(0.5);
// Fuel cost display
this.add.text(400, 500, `Fuel consumed: ${this.fuelCost} units`, {
fontSize: '16px',
fill: '#ffff00'
}).setOrigin(0.5);
// Continue button
const continueBtn = this.add.rectangle(400, 540, 200, 50, 0x2d5f8e);
continueBtn.setInteractive({ useHandCursor: true });
continueBtn.setStrokeStyle(3, 0xffffff);
const btnText = this.add.text(400, 540, 'CONTINUE', {
fontSize: '20px',
fill: '#fff',
fontStyle: 'bold'
}).setOrigin(0.5);
// Hover effect
continueBtn.on('pointerover', () => {
continueBtn.setFillStyle(0x4a90c4);
});
continueBtn.on('pointerout', () => {
continueBtn.setFillStyle(0x2d5f8e);
});
continueBtn.on('pointerdown', () => {
// Deduct fuel
this.inventory.fuel -= this.fuelCost;
// Proceed to next scene
this.scene.start(this.nextScene, { inventory: this.inventory });
});
}
getDestinationContent(destination) {
const destinations = {
'hunting': {
title: 'Approaching the Hunting Grounds',
description: 'Your vessel cuts through the icy waters. The crew prepares the harpoons.\nIn the distance, spouts of water mark the presence of whales.\nThe hunt is about to begin...',
backgroundColor: 0x1e3a5f,
visualType: 'ocean_whales'
},
'antarctic': {
title: 'Arriving at Antarctic Island',
description: 'The ship navigates through floating ice. A desolate, frozen island emerges.\nThousands of penguins waddle along the rocky shores.\nYour crew exchanges uneasy glances. They know why you\'re here...',
backgroundColor: 0x4a5f7f,
visualType: 'ice_penguins'
},
'port': {
title: 'Returning to Port',
description: 'The familiar harbor comes into view. Seagulls circle overhead.\nYou can already smell the taverns and hear the merchants haggling.\nTime to resupply and sell your cargo.',
backgroundColor: 0x654321,
visualType: 'harbor'
}
};
return destinations[destination] || {
title: 'Traveling...',
description: 'Your journey continues across the vast ocean.',
backgroundColor: 0x2d5f8e,
visualType: 'ocean'
};
}
createDestinationVisuals(destination, content) {
switch (content.visualType) {
case 'ocean_whales':
this.createOceanWithWhales();
break;
case 'ice_penguins':
this.createIceWithPenguins();
break;
case 'harbor':
this.createHarbor();
break;
default:
this.createOcean();
break;
}
}
createOceanWithWhales() {
// Ocean waves
this.createOcean();
// Whale spouts in the distance
for (let i = 0; i < 3; i++) {
const x = 150 + i * 250;
const y = 150 + Math.random() * 100;
// Whale body (barely visible)
this.add.ellipse(x, y + 20, 60, 30, 0x2a2a2a, 0.6);
// Water spout
const spout = this.add.triangle(
x, y,
0, 0,
-8, -30,
8, -30,
0x87CEEB, 0.7
);
// Animate spout
this.tweens.add({
targets: spout,
alpha: { from: 0.7, to: 0.2 },
y: y - 10,
duration: 1000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
}
// Ship silhouette
this.add.rectangle(700, 280, 80, 40, 0x654321);
this.add.polygon(700, 260, [0, 0, -40, 40, 40, 40], 0x8B4513);
}
createIceWithPenguins() {
// Icy background effects
for (let i = 0; i < 20; i++) {
const x = Math.random() * 800;
const y = Math.random() * 300;
const size = 5 + Math.random() * 10;
this.add.circle(x, y, size, 0xE0E0E0, 0.3);
}
// Ice floes
this.add.ellipse(200, 250, 150, 80, 0xF0F0F0);
this.add.ellipse(500, 200, 120, 70, 0xE8E8E8);
this.add.ellipse(650, 270, 100, 60, 0xF5F5F5);
// Penguins on ice floes
const penguinPositions = [
{x: 180, y: 240}, {x: 200, y: 245}, {x: 220, y: 238},
{x: 490, y: 195}, {x: 510, y: 200},
{x: 645, y: 265}
];
penguinPositions.forEach(pos => {
// Penguin body
this.add.ellipse(pos.x, pos.y, 12, 18, 0x000000);
this.add.ellipse(pos.x, pos.y + 2, 8, 12, 0xFFFFFF);
// Penguin eyes
this.add.circle(pos.x - 2, pos.y - 4, 1.5, 0xFFFFFF);
this.add.circle(pos.x + 2, pos.y - 4, 1.5, 0xFFFFFF);
});
// Ship in the background
this.add.rectangle(100, 100, 60, 30, 0x654321, 0.7);
this.add.polygon(100, 85, [0, 0, -30, 30, 30, 30], 0x8B4513, 0.7);
}
createHarbor() {
// Harbor water
this.add.rectangle(400, 400, 800, 400, 0x2d5f8e);
// Dock
this.add.rectangle(400, 320, 700, 60, 0x654321);
for (let i = 0; i < 10; i++) {
this.add.rectangle(100 + i * 70, 320, 50, 55, 0x4a3621);
}
// Buildings in background
const buildings = [
{x: 100, y: 180, w: 80, h: 120, color: 0x8B4513},
{x: 220, y: 200, w: 100, h: 100, color: 0x654321},
{x: 360, y: 170, w: 90, h: 130, color: 0x8B6914},
{x: 490, y: 190, w: 110, h: 110, color: 0x654321},
{x: 640, y: 180, w: 95, h: 120, color: 0x8B4513}
];
buildings.forEach(b => {
this.add.rectangle(b.x, b.y, b.w, b.h, b.color);
this.add.rectangle(b.x, b.y, b.w, b.h).setStrokeStyle(2, 0x000000);
// Windows
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 2; j++) {
this.add.rectangle(
b.x - b.w/4 + j * b.w/2,
b.y - b.h/3 + i * b.h/3,
10, 12, 0xFFDD44
);
}
}
});
// Barrels on dock
for (let i = 0; i < 5; i++) {
this.add.circle(150 + i * 120, 310, 15, 0x8B0000);
}
// Seagulls
for (let i = 0; i < 3; i++) {
const x = 200 + i * 200;
const y = 80 + Math.random() * 40;
const bird = this.add.text(x, y, 'v', {
fontSize: '20px',
fill: '#ffffff'
});
this.tweens.add({
targets: bird,
x: x + 50,
y: y - 20,
duration: 2000 + Math.random() * 1000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
}
}
createOcean() {
// Simple ocean waves
const graphics = this.add.graphics();
graphics.lineStyle(3, 0x4a90c4, 0.7);
for (let i = 0; i < 8; i++) {
const y = 100 + i * 40;
graphics.beginPath();
for (let x = 0; x < 800; x += 20) {
const waveY = y + Math.sin((x + i * 30) * 0.03) * 15;
if (x === 0) {
graphics.moveTo(x, waveY);
} else {
graphics.lineTo(x, waveY);
}
}
graphics.strokePath();
}
}
}