Compare commits

..

9 Commits

Author SHA1 Message Date
Thomas Richter
fd3517da85 deploy: update image to latest 2026-02-05 23:31:23 +01:00
Thomas Richter
283c88134a deploy: update image to latest 2026-02-05 19:46:22 +01:00
Thomas Richter
077e58216c deploy: update image to latest 2026-02-05 19:33:30 +01:00
Thomas Richter
2ad205d495 fix: whale stays longer (15s) and no pointer cursor
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 01:43:56 +01:00
Thomas Richter
c220ea4b53 feat: add swimming whale to Deep Sea scene
- Whale enters from random edge, exits to different edge
- 5 second smooth swim with Sine.inOut easing
- 2 second respawn delay
- Bioluminescent eye glow effect
- Subtle bobbing animation during movement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:46:30 +01:00
Thomas Richter
3328269499 feat: add Deep Sea alternative hunting area
- Add Deep Sea location to navigation map
- Create DeepSeaHuntingScene with dark atmosphere
- Add deep ocean transition visuals
- Bioluminescent particles and giant shadows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:29:32 +01:00
Thomas Richter
f2be8ca3d0 style: move fullscreen button to top-left corner 2026-02-05 00:10:16 +01:00
Thomas Richter
be41f912a2 feat: add fullscreen mode toggle button
- Add src/utils/fullscreen.js with createFullscreenButton() helper
- Fullscreen button appears in bottom-right corner of all scenes
- Click to toggle fullscreen mode
- Uses Phaser's built-in scale manager

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 00:05:03 +01:00
Thomas Richter
1154a78908 feat: add responsive font sizing for mobile
- Add src/utils/responsive.js with fontSize() helper
- Mobile fonts scale 1.4x for better readability
- Update all scenes to use responsive font sizes
- Update deploy-k8s.sh with full deployment steps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 23:50:29 +01:00
11 changed files with 810 additions and 40 deletions

View File

@@ -1,10 +1,8 @@
#!/bin/bash #!/bin/bash
# Deploy whalehunting to Kubernetes via ArgoCD # Deploy whalehunting to Kubernetes via ArgoCD
# Prerequisites: # Usage: ./deploy-k8s.sh [tag]
# 1. Create repo in Gitea: git.kube2.tricnet.de/admin/whalehunting # Example: ./deploy-k8s.sh v1.2.0
# 2. Push this repo to Gitea
# 3. Run this script to create the ArgoCD application
set -e set -e
@@ -15,34 +13,39 @@ IMAGE_TAG="${1:-latest}"
echo "=== Whalehunting Kubernetes Deployment ===" echo "=== Whalehunting Kubernetes Deployment ==="
echo "" echo ""
# Step 1: Build and push Docker image to Gitea registry # Step 1: Build Docker image
echo "1. Building Docker image..." echo "1. Building Docker image..."
docker build -t ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG} . docker build -t ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG} .
# Step 2: Push image to registry
echo "" echo ""
echo "2. Pushing image to Gitea registry..." echo "2. Pushing image to Gitea registry..."
echo " (You may need to: docker login ${GITEA_URL})"
docker push ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG} docker push ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG}
# Step 3: Update image tag in kustomization
echo "" echo ""
echo "3. Updating image tag in kustomization.yaml..." echo "3. Updating image tag in kustomization.yaml..."
sed -i "s/newTag: .*/newTag: ${IMAGE_TAG}/" k8s/kustomization.yaml sed -i "s/newTag: .*/newTag: ${IMAGE_TAG}/" k8s/kustomization.yaml
# Step 4: Commit and push to Gitea
echo "" echo ""
echo "4. Committing and pushing to Gitea..." echo "4. Committing and pushing to Gitea..."
git add -A git add -A
git commit -m "deploy: update image to ${IMAGE_TAG}" || echo "No changes to commit" git commit -m "deploy: update image to ${IMAGE_TAG}" || echo "No changes to commit"
git push origin master git push origin master
# Step 5: Ensure ArgoCD application exists
echo "" echo ""
echo "5. Creating/updating ArgoCD application..." echo "5. Ensuring ArgoCD application exists..."
ssh root@kube2.tricnet.de "kubectl apply -f -" < k8s/argocd-application.yaml ssh root@kube2.tricnet.de "kubectl apply -f -" < k8s/argocd-application.yaml
# Step 6: Wait for sync and check status
echo "" echo ""
echo "=== Deployment initiated ===" echo "6. Checking deployment status..."
echo "ArgoCD will sync automatically." sleep 5
echo "" ssh root@kube2.tricnet.de "kubectl get application whalehunting -n argocd"
echo "Check status:" ssh root@kube2.tricnet.de "kubectl get pods -n whalehunting"
echo " ssh root@kube2.tricnet.de 'kubectl get application whalehunting -n argocd'"
echo "" echo ""
echo "=== Deployment complete ==="
echo "Game URL: https://whalehunting.kube2.tricnet.de" echo "Game URL: https://whalehunting.kube2.tricnet.de"

View File

@@ -1,6 +1,6 @@
{ {
"name": "whalehunting-game", "name": "whalehunting-game",
"version": "1.0.0", "version": "1.2.0",
"description": "A Monkey Island-style point-and-click adventure game about 18th century whaling", "description": "A Monkey Island-style point-and-click adventure game about 18th century whaling",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {

View File

@@ -4,6 +4,7 @@ import ShipDeckScene from './scenes/ShipDeckScene.js';
import MapScene from './scenes/MapScene.js'; import MapScene from './scenes/MapScene.js';
import TransitionScene from './scenes/TransitionScene.js'; import TransitionScene from './scenes/TransitionScene.js';
import HuntingScene from './scenes/HuntingScene.js'; import HuntingScene from './scenes/HuntingScene.js';
import DeepSeaHuntingScene from './scenes/DeepSeaHuntingScene.js';
const config = { const config = {
type: Phaser.AUTO, type: Phaser.AUTO,
@@ -11,7 +12,7 @@ const config = {
height: 600, height: 600,
parent: 'game-container', parent: 'game-container',
backgroundColor: '#2d5f8e', backgroundColor: '#2d5f8e',
scene: [IntroScene, ShipDeckScene, MapScene, TransitionScene, HuntingScene], scene: [IntroScene, ShipDeckScene, MapScene, TransitionScene, HuntingScene, DeepSeaHuntingScene],
physics: { physics: {
default: 'arcade', default: 'arcade',
arcade: { arcade: {

View File

@@ -0,0 +1,538 @@
import Phaser from 'phaser';
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class DeepSeaHuntingScene extends Phaser.Scene {
constructor() {
super({ key: 'DeepSeaHuntingScene' });
}
init(data) {
this.inventory = data.inventory || { whaleOil: 0, fuel: 100, penguins: 0 };
this.whalesHunted = 0;
this.currentWhale = null;
this.harpoons = [];
this.crosshairX = 400;
this.crosshairY = 300;
}
create() {
// Dark deep ocean background
this.add.rectangle(400, 300, 800, 600, 0x0a1a2a);
// Create atmospheric effects
this.createDeepOceanAtmosphere();
// UI elements
this.createHUD();
this.createInstructions();
// Fullscreen button
createFullscreenButton(this);
// Create crosshair
this.createCrosshair();
// Setup input
this.setupInput();
// Spawn first whale
this.spawnWhale();
// Detect mobile
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Show initial message
const shootMessage = this.isMobile ? 'The leviathan approaches... Tap to harpoon!' : 'The leviathan approaches... Click to harpoon!';
this.showMessage(shootMessage);
}
update() {
// Update crosshair position
this.crosshairX = this.input.x;
this.crosshairY = this.input.y;
this.crosshair.setPosition(this.crosshairX, this.crosshairY);
// Update harpoons
this.updateHarpoons();
// Check collisions
this.checkCollisions();
}
createCrosshair() {
const graphics = this.add.graphics();
graphics.lineStyle(3, 0x4a9fff, 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, 0x4a9fff, 1);
graphics.strokeCircle(0, 0, 3);
// Convert to texture and create sprite
graphics.generateTexture('crosshair_deep', 40, 40);
graphics.destroy();
this.crosshair = this.add.sprite(400, 300, 'crosshair_deep');
this.crosshair.setDepth(100);
}
setupInput() {
// Mouse/touch click to shoot
this.input.on('pointerdown', () => {
this.shootHarpoon();
});
// Space to shoot
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
this.spaceKey.on('down', () => {
this.shootHarpoon();
});
// Hide cursor on desktop
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
this.input.setDefaultCursor('none');
}
}
shootHarpoon() {
// Create harpoon at crosshair position
const harpoon = this.add.container(this.crosshairX, this.crosshairY);
// Harpoon shaft with glow effect
const glow = this.add.rectangle(0, 0, 8, 30, 0x4a9fff, 0.3);
const shaft = this.add.rectangle(0, 0, 4, 25, 0x6abfff);
const tip = this.add.triangle(0, -15, 0, -8, -5, 0, 5, 0, 0x4a9fff);
harpoon.add([glow, shaft, tip]);
harpoon.setData('active', true);
harpoon.setDepth(50);
this.harpoons.push(harpoon);
// Flash effect
const flash = this.add.circle(this.crosshairX, this.crosshairY, 15, 0x4a9fff, 0.6);
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 -= 10;
// Remove if off screen
if (harpoon.y < -30) {
harpoon.destroy();
this.harpoons.splice(i, 1);
}
}
}
checkCollisions() {
if (!this.currentWhale || !this.currentWhale.getData('alive')) {
return;
}
const whale = this.currentWhale;
for (let i = this.harpoons.length - 1; i >= 0; i--) {
const harpoon = this.harpoons[i];
if (!harpoon.getData('active')) {
continue;
}
const distance = Phaser.Math.Distance.Between(
harpoon.x, harpoon.y,
whale.x, whale.y
);
if (distance < 90) {
this.hitWhale(whale, harpoon, i);
break;
}
}
}
hitWhale(whale, harpoon, harpoonIndex) {
harpoon.setData('active', false);
harpoon.destroy();
this.harpoons.splice(harpoonIndex, 1);
// Reduce 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, 10);
// Change color based on health
if (healthPercent > 0.6) {
healthBar.setFillStyle(0x00ff00);
} else if (healthPercent > 0.3) {
healthBar.setFillStyle(0xffff00);
} else {
healthBar.setFillStyle(0xff0000);
}
// Hit flash
const hitFlash = this.add.circle(whale.x, whale.y, 60, 0x4a9fff, 0.5);
this.tweens.add({
targets: hitFlash,
alpha: 0,
scale: 1.5,
duration: 300,
onComplete: () => hitFlash.destroy()
});
if (health <= 0) {
this.killWhale(whale);
} else {
this.showMessage(`Hit! Leviathan health: ${health}/${maxHealth}`);
}
}
killWhale(whale) {
whale.setData('alive', false);
// Stop the path tween
this.tweens.killTweensOf(whale.getData('pathData'));
// Check fuel
if (this.inventory.fuel < 3) {
this.tweens.add({
targets: whale,
alpha: 0,
y: whale.y + 100,
duration: 1500,
onComplete: () => {
whale.destroy();
this.currentWhale = null;
this.time.delayedCall(2000, () => this.spawnWhale());
}
});
this.showMessage('Leviathan slain but no fuel to process! Need 3 fuel.');
return;
}
// Death animation
this.tweens.add({
targets: whale,
alpha: 0,
y: whale.y + 100,
angle: whale.rotation * (180 / Math.PI) + 90,
duration: 1500,
onComplete: () => {
whale.destroy();
this.currentWhale = null;
this.time.delayedCall(2000, () => this.spawnWhale());
}
});
// Rewards - leviathans give more oil
this.inventory.fuel -= 3;
this.inventory.whaleOil += 3;
this.whalesHunted++;
this.updateStats();
this.showMessage(`Leviathan slain! +3 Oil, -3 Fuel`);
// Success flash
const flash = this.add.rectangle(400, 300, 800, 600, 0x4a9fff, 0.3);
this.tweens.add({
targets: flash,
alpha: 0,
duration: 500,
onComplete: () => flash.destroy()
});
}
createDeepOceanAtmosphere() {
// Gradient darkness
for (let i = 0; i < 6; i++) {
const alpha = 0.05 + i * 0.03;
this.add.rectangle(400, 500 - i * 80, 800, 100, 0x000000, alpha);
}
// Subtle waves
const graphics = this.add.graphics();
graphics.lineStyle(2, 0x1a3a5a, 0.3);
for (let i = 0; i < 8; i++) {
const y = 80 + i * 70;
graphics.beginPath();
for (let x = 0; x < 800; x += 30) {
const waveY = y + Math.sin((x + i * 50) * 0.012) * 8;
if (x === 0) {
graphics.moveTo(x, waveY);
} else {
graphics.lineTo(x, waveY);
}
}
graphics.strokePath();
}
// Bioluminescent particles
for (let i = 0; i < 20; i++) {
const x = Math.random() * 800;
const y = 100 + Math.random() * 400;
const particle = this.add.circle(x, y, 2 + Math.random() * 2, 0x4a9fff, 0.5);
this.tweens.add({
targets: particle,
alpha: { from: 0.5, to: 0.1 },
y: y - 30,
duration: 3000 + Math.random() * 2000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
}
}
spawnWhale() {
// Get random entry and exit points on different edges
const edges = ['top', 'bottom', 'left', 'right'];
const entryEdge = edges[Math.floor(Math.random() * edges.length)];
// Pick a different edge for exit
const exitEdges = edges.filter(e => e !== entryEdge);
const exitEdge = exitEdges[Math.floor(Math.random() * exitEdges.length)];
const entry = this.getEdgePoint(entryEdge);
const exit = this.getEdgePoint(exitEdge);
// Calculate angle for whale rotation
const angle = Phaser.Math.Angle.Between(entry.x, entry.y, exit.x, exit.y);
// Create whale container
const whale = this.add.container(entry.x, entry.y);
// Whale body (large deep sea whale)
const body = this.add.ellipse(0, 0, 180, 70, 0x1a2a3a);
body.setStrokeStyle(2, 0x2a4a6a);
body.disableInteractive();
// Whale underbelly
const belly = this.add.ellipse(0, 10, 140, 40, 0x2a3a4a);
// Tail
const tail = this.add.triangle(-100, 0, 0, -35, 0, 35, -50, 0, 0x1a2a3a);
// Dorsal fin
const dorsalFin = this.add.triangle(20, -25, 0, 0, 30, 0, 15, -30, 0x1a2a3a);
// Eye (bioluminescent glow)
const eyeGlow = this.add.circle(60, -10, 12, 0x4a9fff, 0.3);
const eye = this.add.circle(60, -10, 6, 0x6abfff);
// Add glow effect to eye
this.tweens.add({
targets: eyeGlow,
alpha: { from: 0.3, to: 0.6 },
scale: { from: 1, to: 1.3 },
duration: 1000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
// Health bar background
const healthBg = this.add.rectangle(0, -55, 120, 10, 0x000000);
healthBg.setStrokeStyle(1, 0x4a9fff);
// Health bar
const healthBar = this.add.rectangle(0, -55, 120, 10, 0x00ff00);
whale.add([tail, body, belly, dorsalFin, eyeGlow, eye, healthBg, healthBar]);
whale.setRotation(angle);
whale.setDepth(10);
// Set whale data
whale.setData('alive', true);
whale.setData('health', 5); // Leviathans are tougher - 5 hits
whale.setData('maxHealth', 5);
whale.setData('healthBar', healthBar);
this.currentWhale = whale;
// Subtle bobbing animation during movement
this.tweens.add({
targets: whale,
scaleY: { from: 1, to: 1.05 },
duration: 800,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
// Create curved path from entry to exit
const path = new Phaser.Curves.Path(entry.x, entry.y);
// Calculate midpoint and perpendicular offset for curve
const midX = (entry.x + exit.x) / 2;
const midY = (entry.y + exit.y) / 2;
// Create perpendicular offset for natural swimming curve
const dx = exit.x - entry.x;
const dy = exit.y - entry.y;
const dist = Math.sqrt(dx * dx + dy * dy);
// Perpendicular direction (normalized)
const perpX = -dy / dist;
const perpY = dx / dist;
// Random curve amplitude (positive or negative for variety)
const curveAmount = (Math.random() > 0.5 ? 1 : -1) * (80 + Math.random() * 120);
// Control points for smooth S-curve
const ctrl1X = entry.x + dx * 0.25 + perpX * curveAmount;
const ctrl1Y = entry.y + dy * 0.25 + perpY * curveAmount;
const ctrl2X = entry.x + dx * 0.75 - perpX * curveAmount * 0.7;
const ctrl2Y = entry.y + dy * 0.75 - perpY * curveAmount * 0.7;
// Add cubic bezier curve
path.cubicBezierTo(exit.x, exit.y, ctrl1X, ctrl1Y, ctrl2X, ctrl2Y);
// Store path progress
const pathData = { t: 0 };
whale.setData('pathData', pathData);
// Animate along curved path
this.tweens.add({
targets: pathData,
t: 1,
duration: 15000,
ease: 'Sine.inOut',
onUpdate: () => {
const point = path.getPoint(pathData.t);
const tangent = path.getTangent(pathData.t);
whale.setPosition(point.x, point.y);
whale.setRotation(Math.atan2(tangent.y, tangent.x));
},
onComplete: () => {
this.onWhaleExit(whale);
}
});
}
getEdgePoint(edge) {
const margin = 100; // How far off-screen to spawn
const padding = 100; // Padding from corners
switch (edge) {
case 'top':
return {
x: padding + Math.random() * (800 - padding * 2),
y: -margin
};
case 'bottom':
return {
x: padding + Math.random() * (800 - padding * 2),
y: 600 + margin
};
case 'left':
return {
x: -margin,
y: padding + Math.random() * (600 - padding * 2)
};
case 'right':
return {
x: 800 + margin,
y: padding + Math.random() * (600 - padding * 2)
};
}
}
onWhaleExit(whale) {
// Destroy the whale
whale.destroy();
this.currentWhale = null;
// Respawn after 2 seconds
this.time.delayedCall(2000, () => {
this.spawnWhale();
});
}
createHUD() {
// HUD background
const hudBg = this.add.rectangle(400, 30, 780, 50, 0x000000, 0.7);
hudBg.setStrokeStyle(2, 0x4a9fff);
// Stats
this.statsText = this.add.text(20, 15, '', {
fontSize: fontSize(16),
fill: '#4a9fff'
});
// Return button
const returnBtn = this.add.rectangle(750, 30, 80, 35, 0x1a3a5a);
returnBtn.setInteractive({ useHandCursor: true });
returnBtn.setStrokeStyle(2, 0x4a9fff);
const returnText = this.add.text(750, 30, 'RETURN', {
fontSize: fontSize(14),
fill: '#4a9fff'
}).setOrigin(0.5);
returnBtn.on('pointerdown', () => {
this.returnToMap();
});
this.updateStats();
}
updateStats() {
this.statsText.setText([
`Fuel: ${this.inventory.fuel}/100`,
`Oil: ${this.inventory.whaleOil}/50`,
`Leviathans: ${this.whalesHunted}`
]);
}
createInstructions() {
this.messageText = this.add.text(400, 570, '', {
fontSize: fontSize(16),
fill: '#4a9fff',
backgroundColor: '#000000',
padding: { x: 10, y: 5 }
}).setOrigin(0.5);
}
showMessage(text) {
this.messageText.setText(text);
}
returnToMap() {
this.input.setDefaultCursor('default');
this.scene.start('MapScene', { inventory: this.inventory });
}
}

View File

@@ -1,4 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class HuntingScene extends Phaser.Scene { export default class HuntingScene extends Phaser.Scene {
constructor() { constructor() {
@@ -46,6 +48,9 @@ export default class HuntingScene extends Phaser.Scene {
// Message display // Message display
const shootMessage = this.isMobile ? 'Hunt whales! Tap to shoot harpoon.' : 'Hunt whales! Click or press SPACE to shoot harpoon.'; const shootMessage = this.isMobile ? 'Hunt whales! Tap to shoot harpoon.' : 'Hunt whales! Click or press SPACE to shoot harpoon.';
this.showMessage(shootMessage); this.showMessage(shootMessage);
// Fullscreen button
createFullscreenButton(this);
} }
update() { update() {
@@ -196,7 +201,7 @@ export default class HuntingScene extends Phaser.Scene {
// Stats (fixed to screen) // Stats (fixed to screen)
this.statsText = this.add.text(20, 15, '', { this.statsText = this.add.text(20, 15, '', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#fff' fill: '#fff'
}); });
this.statsText.setScrollFactor(0); this.statsText.setScrollFactor(0);
@@ -204,7 +209,7 @@ export default class HuntingScene extends Phaser.Scene {
// Control mode indicator (fixed to screen, desktop only) // Control mode indicator (fixed to screen, desktop only)
if (!this.isMobile) { if (!this.isMobile) {
this.controlModeText = this.add.text(400, 15, 'Controls: MOUSE (TAB to switch)', { this.controlModeText = this.add.text(400, 15, 'Controls: MOUSE (TAB to switch)', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#ffff00' fill: '#ffff00'
}).setOrigin(0.5, 0); }).setOrigin(0.5, 0);
this.controlModeText.setScrollFactor(0); this.controlModeText.setScrollFactor(0);
@@ -217,7 +222,7 @@ export default class HuntingScene extends Phaser.Scene {
returnBtn.setScrollFactor(0); returnBtn.setScrollFactor(0);
const returnText = this.add.text(750, 30, 'RETURN', { const returnText = this.add.text(750, 30, 'RETURN', {
fontSize: '14px', fontSize: fontSize(14),
fill: '#fff' fill: '#fff'
}).setOrigin(0.5); }).setOrigin(0.5);
returnText.setScrollFactor(0); returnText.setScrollFactor(0);
@@ -239,7 +244,7 @@ export default class HuntingScene extends Phaser.Scene {
createInstructions() { createInstructions() {
this.messageText = this.add.text(400, 570, '', { this.messageText = this.add.text(400, 570, '', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#fff', fill: '#fff',
backgroundColor: '#000000', backgroundColor: '#000000',
padding: { x: 10, y: 5 } padding: { x: 10, y: 5 }

View File

@@ -1,3 +1,6 @@
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class IntroScene extends Phaser.Scene { export default class IntroScene extends Phaser.Scene {
constructor() { constructor() {
super({ key: 'IntroScene' }); super({ key: 'IntroScene' });
@@ -19,7 +22,7 @@ export default class IntroScene extends Phaser.Scene {
// Game title // Game title
const title = this.add.text(400, 150, 'WHALE HUNTING', { const title = this.add.text(400, 150, 'WHALE HUNTING', {
fontSize: '56px', fontSize: fontSize(56),
fill: '#ffffff', fill: '#ffffff',
fontStyle: 'bold' fontStyle: 'bold'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -27,7 +30,7 @@ export default class IntroScene extends Phaser.Scene {
// Subtitle // Subtitle
this.add.text(400, 210, 'A Whaling Adventure on the High Seas', { this.add.text(400, 210, 'A Whaling Adventure on the High Seas', {
fontSize: '22px', fontSize: fontSize(22),
fill: '#cccccc', fill: '#cccccc',
fontStyle: 'italic' fontStyle: 'italic'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -43,7 +46,7 @@ export default class IntroScene extends Phaser.Scene {
buttonBg.setInteractive({ useHandCursor: true }); buttonBg.setInteractive({ useHandCursor: true });
const buttonText = this.add.text(400, 400, 'SET SAIL', { const buttonText = this.add.text(400, 400, 'SET SAIL', {
fontSize: '28px', fontSize: fontSize(28),
fill: '#ffffff', fill: '#ffffff',
fontStyle: 'bold' fontStyle: 'bold'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -73,9 +76,19 @@ export default class IntroScene extends Phaser.Scene {
// Instructions text // Instructions text
this.add.text(400, 530, 'Click to cast off and seek yer fortune!', { this.add.text(400, 530, 'Click to cast off and seek yer fortune!', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#ffff99' fill: '#ffff99'
}).setOrigin(0.5); }).setOrigin(0.5);
// Fullscreen button
createFullscreenButton(this);
// Version display
this.add.text(790, 590, 'v1.2.0', {
fontSize: fontSize(12),
fill: '#ffffff',
alpha: 0.5
}).setOrigin(1, 1);
} }
drawWaves() { drawWaves() {

View File

@@ -1,4 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class MapScene extends Phaser.Scene { export default class MapScene extends Phaser.Scene {
constructor() { constructor() {
@@ -16,7 +18,7 @@ export default class MapScene extends Phaser.Scene {
// Title // Title
this.add.text(400, 30, 'Navigation Map - Choose Your Destination', { this.add.text(400, 30, 'Navigation Map - Choose Your Destination', {
fontSize: '24px', fontSize: fontSize(24),
fill: '#fff', fill: '#fff',
fontStyle: 'bold' fontStyle: 'bold'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -44,6 +46,11 @@ export default class MapScene extends Phaser.Scene {
'Return to port to resupply fuel and sell whale oil.', 'Return to port to resupply fuel and sell whale oil.',
() => this.goToPort()); () => this.goToPort());
// Deep Sea - Alternative hunting area
this.createLocation(550, 150, 'DEEP\nSEA', 0x0a2a4a,
'Treacherous deep waters. Giant whales lurk in the abyss...',
() => this.goToDeepSea());
// Inventory display // Inventory display
this.createInventoryDisplay(); this.createInventoryDisplay();
@@ -57,7 +64,7 @@ export default class MapScene extends Phaser.Scene {
closeButton.setStrokeStyle(2, 0xffffff); closeButton.setStrokeStyle(2, 0xffffff);
const closeText = this.add.text(750, 50, 'CLOSE', { const closeText = this.add.text(750, 50, 'CLOSE', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#fff' fill: '#fff'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -72,6 +79,9 @@ export default class MapScene extends Phaser.Scene {
closeText.setScale(1.0); closeText.setScale(1.0);
this.returnToShip(); this.returnToShip();
}); });
// Fullscreen button
createFullscreenButton(this);
} }
drawWaves() { drawWaves() {
@@ -102,7 +112,7 @@ export default class MapScene extends Phaser.Scene {
// Location name // Location name
const text = this.add.text(x, y, name, { const text = this.add.text(x, y, name, {
fontSize: '12px', fontSize: fontSize(12),
fill: '#fff', fill: '#fff',
fontStyle: 'bold', fontStyle: 'bold',
align: 'center' align: 'center'
@@ -136,7 +146,7 @@ export default class MapScene extends Phaser.Scene {
panel.setStrokeStyle(2, 0xffffff); panel.setStrokeStyle(2, 0xffffff);
this.inventoryText = this.add.text(20, 40, '', { this.inventoryText = this.add.text(20, 40, '', {
fontSize: '14px', fontSize: fontSize(14),
fill: '#fff' fill: '#fff'
}); });
@@ -163,7 +173,7 @@ export default class MapScene extends Phaser.Scene {
this.messageBox.setStrokeStyle(2, 0xcccccc); this.messageBox.setStrokeStyle(2, 0xcccccc);
this.messageText = this.add.text(400, 560, '', { this.messageText = this.add.text(400, 560, '', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#fff', fill: '#fff',
wordWrap: { width: 740 }, wordWrap: { width: 740 },
align: 'center' align: 'center'
@@ -207,4 +217,14 @@ export default class MapScene extends Phaser.Scene {
nextScene: 'MapScene' // Will change to 'PortScene' when implemented nextScene: 'MapScene' // Will change to 'PortScene' when implemented
}); });
} }
goToDeepSea() {
// Sailing is free - fuel is only for cooking
this.scene.start('TransitionScene', {
inventory: this.inventory,
destination: 'deepsea',
fuelCost: 0,
nextScene: 'DeepSeaHuntingScene'
});
}
} }

View File

@@ -1,4 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class ShipDeckScene extends Phaser.Scene { export default class ShipDeckScene extends Phaser.Scene {
constructor() { constructor() {
@@ -21,7 +23,7 @@ export default class ShipDeckScene extends Phaser.Scene {
// Background - ship deck // Background - ship deck
this.add.rectangle(400, 300, 800, 600, 0x8B4513); this.add.rectangle(400, 300, 800, 600, 0x8B4513);
this.add.text(400, 30, 'The Deck - Yer Whaling Vessel', { this.add.text(400, 30, 'The Deck - Yer Whaling Vessel', {
fontSize: '24px', fontSize: fontSize(24),
fill: '#fff' fill: '#fff'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -34,9 +36,12 @@ export default class ShipDeckScene extends Phaser.Scene {
// Instructions // Instructions
this.add.text(400, 560, 'Click on things to have a look, ye scurvy dog', { this.add.text(400, 560, 'Click on things to have a look, ye scurvy dog', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#ffff99' fill: '#ffff99'
}).setOrigin(0.5); }).setOrigin(0.5);
// Fullscreen button
createFullscreenButton(this);
} }
createDeck() { createDeck() {
@@ -85,7 +90,7 @@ export default class ShipDeckScene extends Phaser.Scene {
this.penguinCage.setStrokeStyle(3, 0x000000); this.penguinCage.setStrokeStyle(3, 0x000000);
this.penguinEmoji = this.add.text(650, 350, '🐧', { this.penguinEmoji = this.add.text(650, 350, '🐧', {
fontSize: '32px' fontSize: fontSize(32)
}).setOrigin(0.5); }).setOrigin(0.5);
// Hide penguin elements if not yet discovered // Hide penguin elements if not yet discovered
@@ -143,7 +148,7 @@ export default class ShipDeckScene extends Phaser.Scene {
// Add label only on first barrel // Add label only on first barrel
if (i === 0) { if (i === 0) {
const label = this.add.text(x, y, 'FUEL', { const label = this.add.text(x, y, 'FUEL', {
fontSize: '12px', fontSize: fontSize(12),
fill: '#fff' fill: '#fff'
}).setOrigin(0.5); }).setOrigin(0.5);
this.fuelBarrels.push(label); this.fuelBarrels.push(label);
@@ -201,7 +206,7 @@ export default class ShipDeckScene extends Phaser.Scene {
// Add label only on first barrel // Add label only on first barrel
if (i === 0) { if (i === 0) {
const label = this.add.text(x, y, 'OIL', { const label = this.add.text(x, y, 'OIL', {
fontSize: '12px', fontSize: fontSize(12),
fill: '#fff' fill: '#fff'
}).setOrigin(0.5); }).setOrigin(0.5);
this.oilBarrels.push(label); this.oilBarrels.push(label);
@@ -263,7 +268,7 @@ export default class ShipDeckScene extends Phaser.Scene {
panel.setStrokeStyle(2, 0xffffff); panel.setStrokeStyle(2, 0xffffff);
this.inventoryText = this.add.text(20, 40, '', { this.inventoryText = this.add.text(20, 40, '', {
fontSize: '14px', fontSize: fontSize(14),
fill: '#fff' fill: '#fff'
}); });
@@ -303,7 +308,7 @@ export default class ShipDeckScene extends Phaser.Scene {
this.messageBox.setStrokeStyle(2, 0xcccccc); this.messageBox.setStrokeStyle(2, 0xcccccc);
this.messageText = this.add.text(400, 500, 'Ahoy, matey! Welcome aboard. Have a look around the vessel.', { this.messageText = this.add.text(400, 500, 'Ahoy, matey! Welcome aboard. Have a look around the vessel.', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#fff', fill: '#fff',
wordWrap: { width: 740 }, wordWrap: { width: 740 },
align: 'center' align: 'center'

View File

@@ -1,4 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import { fontSize } from '../utils/responsive.js';
import { createFullscreenButton } from '../utils/fullscreen.js';
export default class TransitionScene extends Phaser.Scene { export default class TransitionScene extends Phaser.Scene {
constructor() { constructor() {
@@ -30,7 +32,7 @@ export default class TransitionScene extends Phaser.Scene {
// Destination title // Destination title
this.add.text(400, 360, content.title, { this.add.text(400, 360, content.title, {
fontSize: '32px', fontSize: fontSize(32),
fill: '#ffffff', fill: '#ffffff',
fontStyle: 'bold', fontStyle: 'bold',
stroke: '#000000', stroke: '#000000',
@@ -39,7 +41,7 @@ export default class TransitionScene extends Phaser.Scene {
// Journey description // Journey description
this.add.text(400, 430, content.description, { this.add.text(400, 430, content.description, {
fontSize: '18px', fontSize: fontSize(18),
fill: '#ffffff', fill: '#ffffff',
align: 'center', align: 'center',
wordWrap: { width: 660 } wordWrap: { width: 660 }
@@ -48,12 +50,12 @@ export default class TransitionScene extends Phaser.Scene {
// Fuel cost display (only if there's a cost) // Fuel cost display (only if there's a cost)
if (this.fuelCost > 0) { if (this.fuelCost > 0) {
this.add.text(400, 500, `Fuel consumed: ${this.fuelCost} units`, { this.add.text(400, 500, `Fuel consumed: ${this.fuelCost} units`, {
fontSize: '16px', fontSize: fontSize(16),
fill: '#ffff00' fill: '#ffff00'
}).setOrigin(0.5); }).setOrigin(0.5);
} else { } else {
this.add.text(400, 500, 'The wind carries your sails...', { this.add.text(400, 500, 'The wind carries your sails...', {
fontSize: '16px', fontSize: fontSize(16),
fill: '#cccccc', fill: '#cccccc',
fontStyle: 'italic' fontStyle: 'italic'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -65,7 +67,7 @@ export default class TransitionScene extends Phaser.Scene {
continueBtn.setStrokeStyle(3, 0xffffff); continueBtn.setStrokeStyle(3, 0xffffff);
const btnText = this.add.text(400, 540, 'CONTINUE', { const btnText = this.add.text(400, 540, 'CONTINUE', {
fontSize: '20px', fontSize: fontSize(20),
fill: '#fff', fill: '#fff',
fontStyle: 'bold' fontStyle: 'bold'
}).setOrigin(0.5); }).setOrigin(0.5);
@@ -99,6 +101,9 @@ export default class TransitionScene extends Phaser.Scene {
// Proceed to next scene // Proceed to next scene
this.scene.start(this.nextScene, { inventory: this.inventory }); this.scene.start(this.nextScene, { inventory: this.inventory });
}); });
// Fullscreen button
createFullscreenButton(this);
} }
getDestinationContent(destination) { getDestinationContent(destination) {
@@ -120,6 +125,12 @@ export default class TransitionScene extends Phaser.Scene {
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.', 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, backgroundColor: 0x654321,
visualType: 'harbor' visualType: 'harbor'
},
'deepsea': {
title: 'Descending into the Deep Sea',
description: 'The waters grow dark and cold. Your ship ventures into uncharted depths.\nMassive shadows move beneath the surface. The crew whispers of leviathans.\nOnly the bravest—or most foolish—hunt here...',
backgroundColor: 0x0a1a2a,
visualType: 'deep_ocean'
} }
}; };
@@ -142,6 +153,9 @@ export default class TransitionScene extends Phaser.Scene {
case 'harbor': case 'harbor':
this.createHarbor(); this.createHarbor();
break; break;
case 'deep_ocean':
this.createDeepOcean();
break;
default: default:
this.createOcean(); this.createOcean();
break; break;
@@ -265,7 +279,7 @@ export default class TransitionScene extends Phaser.Scene {
const x = 200 + i * 200; const x = 200 + i * 200;
const y = 80 + Math.random() * 40; const y = 80 + Math.random() * 40;
const bird = this.add.text(x, y, 'v', { const bird = this.add.text(x, y, 'v', {
fontSize: '20px', fontSize: fontSize(20),
fill: '#ffffff' fill: '#ffffff'
}); });
@@ -300,4 +314,74 @@ export default class TransitionScene extends Phaser.Scene {
graphics.strokePath(); graphics.strokePath();
} }
} }
createDeepOcean() {
// Dark, eerie deep ocean atmosphere
// Gradient effect with darker rectangles
for (let i = 0; i < 6; i++) {
const alpha = 0.1 + i * 0.05;
this.add.rectangle(400, 50 + i * 50, 800, 50, 0x000000, alpha);
}
// Subtle dark waves
const graphics = this.add.graphics();
graphics.lineStyle(2, 0x1a3a5a, 0.4);
for (let i = 0; i < 6; i++) {
const y = 80 + i * 50;
graphics.beginPath();
for (let x = 0; x < 800; x += 25) {
const waveY = y + Math.sin((x + i * 40) * 0.015) * 12;
if (x === 0) {
graphics.moveTo(x, waveY);
} else {
graphics.lineTo(x, waveY);
}
}
graphics.strokePath();
}
// Massive shadows lurking below
for (let i = 0; i < 2; i++) {
const x = 200 + i * 400;
const y = 200 + Math.random() * 80;
// Giant whale shadow
const shadow = this.add.ellipse(x, y, 200, 80, 0x000000, 0.3);
// Slow, ominous movement
this.tweens.add({
targets: shadow,
x: x + 30,
y: y + 15,
scaleX: 1.1,
duration: 4000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
}
// Bioluminescent particles
for (let i = 0; i < 15; i++) {
const x = Math.random() * 800;
const y = Math.random() * 300;
const particle = this.add.circle(x, y, 2, 0x4a9fff, 0.6);
this.tweens.add({
targets: particle,
alpha: { from: 0.6, to: 0.1 },
y: y - 20,
duration: 2000 + Math.random() * 2000,
yoyo: true,
repeat: -1,
ease: 'Sine.inOut'
});
}
// Ship silhouette at top
this.add.rectangle(100, 60, 70, 35, 0x1a1a1a, 0.8);
this.add.polygon(100, 45, [0, 0, -35, 30, 35, 30], 0x2a2a2a, 0.8);
}
} }

75
src/utils/fullscreen.js Normal file
View File

@@ -0,0 +1,75 @@
// Fullscreen toggle utility for Phaser scenes
import { fontSize } from './responsive.js';
/**
* Create a fullscreen toggle button in the corner of the screen
* @param {Phaser.Scene} scene - The Phaser scene to add the button to
* @param {object} options - Optional configuration
* @param {number} options.x - X position (default: 30)
* @param {number} options.y - Y position (default: 20)
*/
export function createFullscreenButton(scene, options = {}) {
const x = options.x || 30;
const y = options.y || 20;
// Button background
const btn = scene.add.rectangle(x, y, 50, 30, 0x000000, 0.6);
btn.setStrokeStyle(2, 0xffffff);
btn.setInteractive({ useHandCursor: true });
btn.setScrollFactor(0);
btn.setDepth(1000);
// Button icon/text
const icon = scene.add.text(x, y, '⛶', {
fontSize: fontSize(18),
fill: '#ffffff'
}).setOrigin(0.5);
icon.setScrollFactor(0);
icon.setDepth(1001);
// Update icon based on fullscreen state
const updateIcon = () => {
if (scene.scale.isFullscreen) {
icon.setText('⛶');
} else {
icon.setText('⛶');
}
};
// Toggle fullscreen on click
btn.on('pointerdown', () => {
btn.setFillStyle(0x333333, 0.8);
btn.setScale(0.95);
icon.setScale(0.95);
});
btn.on('pointerup', () => {
btn.setFillStyle(0x000000, 0.6);
btn.setScale(1);
icon.setScale(1);
if (scene.scale.isFullscreen) {
scene.scale.stopFullscreen();
} else {
scene.scale.startFullscreen();
}
updateIcon();
});
btn.on('pointerover', () => {
btn.setFillStyle(0x333333, 0.8);
});
btn.on('pointerout', () => {
btn.setFillStyle(0x000000, 0.6);
});
// Listen for fullscreen changes
scene.scale.on('enterfullscreen', updateIcon);
scene.scale.on('leavefullscreen', updateIcon);
return { btn, icon };
}
export default { createFullscreenButton };

26
src/utils/responsive.js Normal file
View File

@@ -0,0 +1,26 @@
// Responsive utilities for mobile-friendly text sizing
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Font size multiplier for mobile devices
const MOBILE_SCALE = 1.4;
/**
* Get responsive font size - larger on mobile for better readability
* @param {number} baseSize - Base font size in pixels
* @returns {string} Font size string with 'px' suffix
*/
export function fontSize(baseSize) {
const size = isMobile ? Math.round(baseSize * MOBILE_SCALE) : baseSize;
return `${size}px`;
}
/**
* Check if running on mobile device
* @returns {boolean}
*/
export function checkMobile() {
return isMobile;
}
export default { fontSize, checkMobile, isMobile };