Compare commits
19 Commits
8c1a7f0079
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd3517da85 | ||
|
|
283c88134a | ||
|
|
077e58216c | ||
|
|
2ad205d495 | ||
|
|
c220ea4b53 | ||
|
|
3328269499 | ||
|
|
f2be8ca3d0 | ||
|
|
be41f912a2 | ||
|
|
1154a78908 | ||
|
|
b0fb15fe7b | ||
|
|
576799ae0e | ||
|
|
24a44583ef | ||
|
|
b52cdb0685 | ||
|
|
dc28c9f6eb | ||
|
|
89a47cd721 | ||
|
|
8de3f594f9 | ||
|
|
1e5f4f35cd | ||
|
|
e8f8a6a4ef | ||
|
|
67e3e924de |
134
.planning/codebase/ARCHITECTURE.md
Normal file
134
.planning/codebase/ARCHITECTURE.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Pattern Overview
|
||||||
|
|
||||||
|
**Overall:** Scene-based state machine with linear progression through game states.
|
||||||
|
|
||||||
|
**Key Characteristics:**
|
||||||
|
- Phaser 3 game framework with arcade physics
|
||||||
|
- Scene-based architecture where each major game location is a self-contained scene
|
||||||
|
- Inventory as shared game state passed between scenes via scene data
|
||||||
|
- Input-driven gameplay with dual control schemes (mouse/keyboard and touch)
|
||||||
|
- Responsive scaling for mobile and desktop viewports
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
**Presentation Layer (Scenes):**
|
||||||
|
- Purpose: Render game visuals and handle user interaction
|
||||||
|
- Location: `src/scenes/`
|
||||||
|
- Contains: Five scene classes extending Phaser.Scene
|
||||||
|
- Depends on: Phaser framework, Phaser graphics/text/physics utilities
|
||||||
|
- Used by: Main application entry point in `src/main.js`
|
||||||
|
|
||||||
|
**Game State Layer (Inventory):**
|
||||||
|
- Purpose: Track player progress across scenes (fuel, whale oil, penguins)
|
||||||
|
- Location: Passed as data object between scenes via `scene.start(nextScene, { inventory: ... })`
|
||||||
|
- Contains: Simple object with three numeric properties: `whaleOil`, `fuel`, `penguins`
|
||||||
|
- Depends on: Nothing
|
||||||
|
- Used by: All scenes read/modify inventory during gameplay
|
||||||
|
|
||||||
|
**Framework Configuration Layer:**
|
||||||
|
- Purpose: Initialize Phaser game with physics, scaling, scene list
|
||||||
|
- Location: `src/main.js`
|
||||||
|
- Contains: Phaser.Game configuration and setup
|
||||||
|
- Depends on: All scene classes
|
||||||
|
- Used by: Entry point for application startup
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
**Game Startup:**
|
||||||
|
1. Browser loads `index.html` which imports `src/main.js` as module
|
||||||
|
2. `src/main.js` creates Phaser.Game instance with config
|
||||||
|
3. IntroScene is the first scene in the scene list, auto-starts
|
||||||
|
4. Player interaction triggers scene transitions
|
||||||
|
|
||||||
|
**Between-Scene Transitions:**
|
||||||
|
1. Current scene calls `this.scene.start(nextSceneKey, { inventory: this.inventory })`
|
||||||
|
2. Phaser stops current scene, starts next scene
|
||||||
|
3. Next scene receives inventory data in `init(data)` lifecycle method
|
||||||
|
4. Scene reads `data.inventory` and stores it locally
|
||||||
|
5. Scene's `create()` method renders UI with current inventory state
|
||||||
|
|
||||||
|
**Inventory State:**
|
||||||
|
- IntroScene initializes: `{ whaleOil: 0, fuel: 100, penguins: 0 }`
|
||||||
|
- ShipDeckScene receives and passes forward
|
||||||
|
- MapScene receives, displays, and passes forward
|
||||||
|
- TransitionScene modifies (deducts fuel if applicable) and passes forward
|
||||||
|
- HuntingScene receives, displays, modifies (fuel cost, oil gain, penguin discovery) and returns to MapScene
|
||||||
|
- Penguin discovery (penguins > 0) unlocks penguin cage UI in ShipDeckScene
|
||||||
|
|
||||||
|
## Key Abstractions
|
||||||
|
|
||||||
|
**Scene Class Pattern:**
|
||||||
|
- Purpose: Encapsulate game logic and rendering for a specific game state
|
||||||
|
- Examples: `src/scenes/IntroScene.js`, `src/scenes/ShipDeckScene.js`, `src/scenes/MapScene.js`, `src/scenes/TransitionScene.js`, `src/scenes/HuntingScene.js`
|
||||||
|
- Pattern: Each scene extends `Phaser.Scene`, implements `create()` for setup and `update()` for per-frame logic
|
||||||
|
|
||||||
|
**Interactive UI Elements:**
|
||||||
|
- Purpose: Handle pointer (mouse/touch) input for clickable objects
|
||||||
|
- Pattern: Rectangle or circle objects with `.setInteractive()` and `.on('pointerdown')` or `.on('pointerup')` listeners
|
||||||
|
- Examples in ShipDeckScene: wheel click to open map, barrel zones, penguin cage
|
||||||
|
- Examples in MapScene: location markers with hover effects and click handlers
|
||||||
|
- Examples in HuntingScene: crosshair positioning and firing
|
||||||
|
|
||||||
|
**Inventory Display Pattern:**
|
||||||
|
- Purpose: Show player resources in consistent UI
|
||||||
|
- Pattern: Text panel with semi-transparent background showing formatted resource strings
|
||||||
|
- Used in: ShipDeckScene, MapScene, HuntingScene
|
||||||
|
- Format: Multi-line text with labels and current/max values
|
||||||
|
|
||||||
|
**Message System Pattern:**
|
||||||
|
- Purpose: Communicate game state changes and feedback to player
|
||||||
|
- Pattern: Semi-transparent rectangle with text object, updated via `showMessage(text)` method
|
||||||
|
- Used in: ShipDeckScene, MapScene, TransitionScene, HuntingScene
|
||||||
|
|
||||||
|
## Entry Points
|
||||||
|
|
||||||
|
**Application Entry:**
|
||||||
|
- Location: `index.html`
|
||||||
|
- Triggers: Page load
|
||||||
|
- Responsibilities: Load HTML structure, import main.js as module, render into game-container div
|
||||||
|
|
||||||
|
**Game Initialization:**
|
||||||
|
- Location: `src/main.js`
|
||||||
|
- Triggers: Module import by index.html
|
||||||
|
- Responsibilities: Configure Phaser game (physics, scaling, resolution), instantiate Phaser.Game, list all scenes
|
||||||
|
|
||||||
|
**First Scene:**
|
||||||
|
- Location: `src/scenes/IntroScene.js`
|
||||||
|
- Triggers: Phaser auto-starts first scene in config array
|
||||||
|
- Responsibilities: Display title, draw decorative elements, show SET SAIL button, initialize inventory with defaults
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Strategy:** Silent failures with user messaging; no error boundaries or try-catch blocks in current codebase.
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- Fuel depletion check before whale processing (HuntingScene.killWhale): if fuel < 2, whale is discarded without processing
|
||||||
|
- Whale only spawns if no alive whale exists: `if (this.currentWhale && this.currentWhale.getData('alive')) return;`
|
||||||
|
- Inventory bounds clamped by Math.min/Math.ceil to prevent negative values
|
||||||
|
- Crosshair position clamped to screen bounds: `Phaser.Math.Clamp(value, 0, max)`
|
||||||
|
|
||||||
|
## Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Logging:** None implemented. Debug would require browser console.
|
||||||
|
|
||||||
|
**Validation:** Minimal; mostly relies on initial state setup and boundary clamping.
|
||||||
|
- Fuel: clamped to [0, 100] via Math.min operations
|
||||||
|
- Whale oil: clamped to [0, 50] via Math.min operations
|
||||||
|
- Penguins: clamped to [0, 20] via Math.min operations
|
||||||
|
- Crosshair X: `Phaser.Math.Clamp(x, 0, 800)`
|
||||||
|
- Crosshair Y: `Phaser.Math.Clamp(y, 0, 600)`
|
||||||
|
|
||||||
|
**Authentication:** Not applicable; single-player game.
|
||||||
|
|
||||||
|
**Device Detection:** Mobile detection via user agent regex in HuntingScene.create():
|
||||||
|
```javascript
|
||||||
|
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Architecture analysis: 2026-02-04*
|
||||||
205
.planning/codebase/CONCERNS.md
Normal file
205
.planning/codebase/CONCERNS.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Codebase Concerns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Architecture & State Management
|
||||||
|
|
||||||
|
**Fragile Inventory Passing System:**
|
||||||
|
- Issue: Inventory object is manually passed between scenes via `data` parameter in every scene transition. No centralized state management.
|
||||||
|
- Files: `src/scenes/IntroScene.js`, `src/scenes/ShipDeckScene.js`, `src/scenes/MapScene.js`, `src/scenes/HuntingScene.js`, `src/scenes/TransitionScene.js`
|
||||||
|
- Impact: Each scene maintains its own copy of inventory. A player could lose progress if a scene fails to properly receive or pass inventory data. Adding new inventory items requires changes across all 5 scenes. Risk of inventory corruption during rapid scene transitions.
|
||||||
|
- Fix approach: Create a centralized GameState or InventoryManager singleton that all scenes access. Example structure:
|
||||||
|
```javascript
|
||||||
|
class GameState {
|
||||||
|
static instance = null;
|
||||||
|
static getInstance() {
|
||||||
|
if (!this.instance) this.instance = new GameState();
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
constructor() {
|
||||||
|
this.inventory = { whaleOil: 0, fuel: 100, penguins: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Then all scenes reference `GameState.getInstance().inventory` instead of managing copies.
|
||||||
|
|
||||||
|
## Testing Coverage Gaps
|
||||||
|
|
||||||
|
**Unit Tests Don't Test Actual Scene Logic:**
|
||||||
|
- What's not tested: Game logic tests in `tests/unit/game-logic.test.js` only test pure functions and simple calculations. They don't test the actual scene behavior, event handlers, or UI state changes.
|
||||||
|
- Files: `tests/unit/game-logic.test.js`, `src/scenes/*.js`
|
||||||
|
- Specific gaps:
|
||||||
|
- No tests for barrel creation/destruction logic in ShipDeckScene
|
||||||
|
- No tests for whale spawning, movement, health tracking
|
||||||
|
- No tests for harpoon firing and collision detection
|
||||||
|
- No tests for scene transition data passing
|
||||||
|
- No tests for inventory display updates
|
||||||
|
- Risk: Bugs in core game mechanics (whale health, fuel consumption, barrel positioning) can't be caught automatically. Changes to scene code could break functionality without detection.
|
||||||
|
- Priority: High - Core game loop (hunting scene) is untested
|
||||||
|
|
||||||
|
**E2E Tests Use Hard-coded Click Positions:**
|
||||||
|
- What's tested: `tests/e2e/game-flow.spec.js` relies on exact pixel coordinates for button clicks. Example: `canvas.click({ position: { x: 400, y: 400 } })`
|
||||||
|
- Problem: Any change to UI layout, button positioning, or screen resolution will break these tests without actually breaking the game
|
||||||
|
- Files: `tests/e2e/game-flow.spec.js` (lines 24, 40, 58, 66, 73, 95, 113-117, 125, 136)
|
||||||
|
- Risk: Tests become maintenance burden; false positives when layout changes
|
||||||
|
- Fix approach: Use Phaser's scene testing utilities or find elements by ID/class instead of pixel positions
|
||||||
|
|
||||||
|
**No Mobile Touch Testing:**
|
||||||
|
- What's tested: E2E includes mobile viewport test but only checks canvas visibility and scaling, not touch interaction functionality
|
||||||
|
- Files: `tests/e2e/game-flow.spec.js` (lines 144-173)
|
||||||
|
- Missing: Tests for touch event handlers, multi-touch scenarios, long-press interactions
|
||||||
|
- Risk: Touch controls may break without detection. Buttons may be non-responsive on actual devices.
|
||||||
|
|
||||||
|
## Performance & Complexity Issues
|
||||||
|
|
||||||
|
**HuntingScene is Large and Complex:**
|
||||||
|
- Issue: Single 637-line file handles whale AI, harpoon physics, collision detection, camera effects, input management, and rendering
|
||||||
|
- Files: `src/scenes/HuntingScene.js`
|
||||||
|
- Specific problem areas:
|
||||||
|
- Lines 316-398: Whale update logic duplicates movement calculation and bounds checking
|
||||||
|
- Lines 434-450: Harpoon update is simple loop-over-array that could become O(n²) with many harpoons
|
||||||
|
- Lines 453-489: Collision detection recalculates screen position every frame for every harpoon
|
||||||
|
- Lines 607-630: Camera sway uses a `time.addEvent()` with permanent loop instead of native `update()` method
|
||||||
|
- Impact: Difficult to debug, test, or modify. Camera sway timer creates memory leak potential (never cancelled). Whale movement becomes laggy with multiple whales.
|
||||||
|
- Fix approach: Extract whale logic to separate `Whale` class, harpoons to `Harpoon` class, collision logic to separate method, use `update()` for camera instead of addEvent()
|
||||||
|
|
||||||
|
**Barrel Recreation is Inefficient:**
|
||||||
|
- Issue: In `ShipDeckScene.js`, `updateInventoryDisplay()` calls `clearBarrels()` then recreates all barrels every time inventory changes (lines 294-297)
|
||||||
|
- Files: `src/scenes/ShipDeckScene.js` (lines 225-297)
|
||||||
|
- Impact: Visual flicker; hundreds of objects created/destroyed repeatedly. On low-end devices, could cause jank.
|
||||||
|
- Fix approach: Reuse barrel objects or update only visual properties (fill color, scale) instead of destroying/recreating
|
||||||
|
|
||||||
|
**Graphics Objects Never Destroyed:**
|
||||||
|
- Issue: Graphics objects created in multiple scenes are never destroyed - only drawn once
|
||||||
|
- Files:
|
||||||
|
- `src/scenes/IntroScene.js` (lines 82-98, 101-116): waves and ship drawn once
|
||||||
|
- `src/scenes/HuntingScene.js` (lines 144-170): crosshair graphics.generateTexture() leaves original graphics object
|
||||||
|
- `src/scenes/TransitionScene.js` (lines 286-301): ocean graphics never destroyed
|
||||||
|
- Impact: Memory leak. Small issue now but accumulates with multiple playthroughs. Old graphics objects persist in memory even after scene changes.
|
||||||
|
- Fix approach: Call `.destroy()` on graphics objects after `.generateTexture()` or scene cleanup
|
||||||
|
|
||||||
|
## Known Behavioral Issues
|
||||||
|
|
||||||
|
**Incomplete Features Referenced in Code:**
|
||||||
|
- Issue: Code contains references to unimplemented scenes
|
||||||
|
- Files: `src/scenes/MapScene.js` (lines 197, 207)
|
||||||
|
- Problem: Pressing buttons to go to Antarctic or Port just returns to MapScene. Code comments say "Will change to 'AntarcticScene' when implemented" and "PortScene when implemented"
|
||||||
|
- Impact: Two major gameplay areas not implemented. Players can't sell whale oil or gather penguins as intended
|
||||||
|
- No fix needed for this sprint; just document that these scenes don't exist
|
||||||
|
|
||||||
|
**Penguin Cage Visibility Logic is Opaque:**
|
||||||
|
- Issue: Penguin cage shown/hidden based on `inventory.penguins > 0` but there's no way in current code to acquire penguins
|
||||||
|
- Files: `src/scenes/ShipDeckScene.js` (lines 83-94, 281-291)
|
||||||
|
- Impact: Code path never executes. Feature is stub/placeholder.
|
||||||
|
- Severity: Low - intentional incomplete feature, but makes code harder to understand
|
||||||
|
|
||||||
|
**Whale Diving Mechanic is Incomplete:**
|
||||||
|
- Issue: Whales go transparent when diving but no actual "invulnerability" is enforced - collision detection still runs (line 461 checks `diving` but collision only prevented if whale is visible or alpha check)
|
||||||
|
- Files: `src/scenes/HuntingScene.js` (lines 304-306, 333-342, 394-410, 460-463)
|
||||||
|
- Risk: Harpoons might hit whale during dive in edge cases
|
||||||
|
- Fix approach: Ensure collision detection returns early if `whale.getData('diving')`
|
||||||
|
|
||||||
|
## Security & Validation Issues
|
||||||
|
|
||||||
|
**No Inventory Bounds Checking:**
|
||||||
|
- Issue: Inventory values can go negative or exceed max without validation
|
||||||
|
- Files: All scene files, particularly `src/scenes/HuntingScene.js` (lines 586, 589)
|
||||||
|
- Example problem: If fuel goes below 0, the display shows negative fuel and barrel count calculation breaks
|
||||||
|
- Current safeguard: Line 547 checks `fuel < 2` before consuming, but nothing prevents other code paths from setting fuel to -5
|
||||||
|
- Fix approach: Create Inventory class with setters that enforce bounds:
|
||||||
|
```javascript
|
||||||
|
class Inventory {
|
||||||
|
setFuel(value) { this.fuel = Math.max(0, Math.min(100, value)); }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hard-coded Game Constants Scattered Throughout:**
|
||||||
|
- Issue: Magic numbers for inventory limits, costs, and gameplay values are hard-coded in multiple files
|
||||||
|
- Files:
|
||||||
|
- Fuel max: `src/scenes/ShipDeckScene.js:276`, `src/scenes/MapScene.js:149`, `src/scenes/HuntingScene.js:234`
|
||||||
|
- Oil max: `src/scenes/ShipDeckScene.js:277`, `src/scenes/MapScene.js:150`, `src/scenes/HuntingScene.js:235`
|
||||||
|
- Whale health: `src/scenes/HuntingScene.js:292` (3 hits)
|
||||||
|
- Fuel cost to process: `src/scenes/HuntingScene.js:547, 586` (2 fuel)
|
||||||
|
- Barrel size calculations: `src/scenes/ShipDeckScene.js:105-165` (hardcoded ratios)
|
||||||
|
- Impact: Changing game balance requires finding and updating 10+ locations. High risk of inconsistency.
|
||||||
|
- Fix approach: Create `GameConstants.js` file:
|
||||||
|
```javascript
|
||||||
|
export const GAME_CONSTANTS = {
|
||||||
|
INVENTORY: { MAX_FUEL: 100, MAX_OIL: 50, MAX_PENGUINS: 20 },
|
||||||
|
WHALE: { HEALTH: 3, PROCESS_FUEL_COST: 2 },
|
||||||
|
BARRELS: { FUEL_UNITS_PER_BARREL: 10, OIL_UNITS_PER_BARREL: 10 }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Quality Issues
|
||||||
|
|
||||||
|
**Duplicate Inventory Initialization:**
|
||||||
|
- Issue: Default inventory object defined in 5 different places
|
||||||
|
- Files: `src/scenes/IntroScene.js:120-124`, `src/scenes/ShipDeckScene.js:6-10`, `src/scenes/HuntingScene.js:9`, `src/scenes/MapScene.js:10`, `src/scenes/TransitionScene.js:10`
|
||||||
|
- Risk: If inventory structure changes, must update in all 5 places
|
||||||
|
- Fix: One source of truth (GameState or constants)
|
||||||
|
|
||||||
|
**Inconsistent Data Passing Pattern:**
|
||||||
|
- Issue: Scene init() method doesn't always match how data is received
|
||||||
|
- Files: `src/scenes/TransitionScene.js` (line 10) uses OR operator for defaults but other scenes (ShipDeckScene line 15) use if check
|
||||||
|
- Impact: Minor style inconsistency, slightly reduces readability
|
||||||
|
|
||||||
|
**No Error Handling for Scene Transitions:**
|
||||||
|
- Issue: All `this.scene.start()` calls don't handle failures
|
||||||
|
- Files: All scene files
|
||||||
|
- Risk: If scene key is misspelled or doesn't exist, silently fails
|
||||||
|
- Fix: Add error handling or validation
|
||||||
|
|
||||||
|
## Missing Features Blocking Full Gameplay
|
||||||
|
|
||||||
|
**No Penguin Collection Mechanic:**
|
||||||
|
- What's missing: Antarctic Island scene doesn't exist, so penguins can't be collected
|
||||||
|
- Blocks: Complete game loop where fuel can be managed through penguin burning
|
||||||
|
- Files: Referenced in `src/scenes/MapScene.js:197` but scene doesn't exist
|
||||||
|
|
||||||
|
**No Port/Trading System:**
|
||||||
|
- What's missing: Port scene doesn't exist, can't sell whale oil
|
||||||
|
- Blocks: Economic gameplay loop and victory condition
|
||||||
|
- Files: Referenced in `src/scenes/MapScene.js:207` but scene doesn't exist
|
||||||
|
|
||||||
|
**No Game Over or Win Condition:**
|
||||||
|
- What's missing: No way to fail (fuel runs out) or win (enough whale oil collected)
|
||||||
|
- Impact: Game has no clear objective or end state
|
||||||
|
- Files: N/A - logic doesn't exist anywhere
|
||||||
|
|
||||||
|
**No Persistence:**
|
||||||
|
- What's missing: Game state lost on page reload
|
||||||
|
- Impact: Can't save/load progress
|
||||||
|
- Not critical for prototype but important for release
|
||||||
|
|
||||||
|
## Scaling & Device Issues
|
||||||
|
|
||||||
|
**Hardcoded Screen Dimensions:**
|
||||||
|
- Issue: Game assumes 800x600 canvas with many hardcoded position values
|
||||||
|
- Files: `src/main.js:10-11`, all scene files
|
||||||
|
- Example: `src/scenes/IntroScene.js:21` (text at x:400, y:150), `src/scenes/HuntingScene.js:139-140` (crosshair clamped to 0-800, 0-600)
|
||||||
|
- Risk: Resizing game window breaks positioning. Mobile scaling works via Phaser but coordinates are still hardcoded
|
||||||
|
- Fix: Use relative positioning or percentage-based coordinates
|
||||||
|
|
||||||
|
**Mobile Detection Uses navigator.userAgent:**
|
||||||
|
- Issue: `src/scenes/HuntingScene.js:23` uses userAgent string matching
|
||||||
|
- Risk: Can be spoofed, unreliable on some devices
|
||||||
|
- Better approach: Use Phaser's `this.sys.game.device` API or check touch availability
|
||||||
|
|
||||||
|
## Testing Infrastructure Concerns
|
||||||
|
|
||||||
|
**Vitest Config Exposes Dev Server Publicly:**
|
||||||
|
- Issue: `vitest.config.js:21-22` sets `host: '0.0.0.0'` allowing external connections
|
||||||
|
- Files: `vitest.config.js`
|
||||||
|
- Risk: In production, test server would be exposed to internet
|
||||||
|
- Fix: Only use in development (check NODE_ENV)
|
||||||
|
|
||||||
|
**E2E Tests Have Long Arbitrary Waits:**
|
||||||
|
- Issue: Tests use `waitForTimeout(1000)` and `waitForTimeout(2000)` instead of waiting for elements/state
|
||||||
|
- Files: `tests/e2e/game-flow.spec.js` (almost every test)
|
||||||
|
- Risk: Tests are slow and flaky; may timeout on slow devices but pass on fast ones
|
||||||
|
- Fix: Use `waitForSelector()`, `waitForFunction()`, or Phaser's scene ready events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Concerns audit: 2026-02-04*
|
||||||
139
.planning/codebase/CONVENTIONS.md
Normal file
139
.planning/codebase/CONVENTIONS.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Coding Conventions
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Naming Patterns
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- PascalCase for scene classes: `IntroScene.js`, `MapScene.js`, `HuntingScene.js`, `ShipDeckScene.js`, `TransitionScene.js`
|
||||||
|
- kebab-case for test files: `game-logic.test.js`, `game-flow.spec.js`
|
||||||
|
- camelCase for configuration files: `vitest.config.js`, `playwright.config.js`
|
||||||
|
|
||||||
|
**Functions:**
|
||||||
|
- camelCase for all methods: `drawWaves()`, `createLocation()`, `updateInventoryDisplay()`, `spawnWhale()`, `hitWhale()`
|
||||||
|
- Method names are descriptive and indicate action: `create*` for initialization, `update*` for state changes, `draw*` for graphics
|
||||||
|
- Private-like pattern: methods starting with event handlers (`on*` callbacks) or utility methods
|
||||||
|
|
||||||
|
**Variables:**
|
||||||
|
- camelCase for local variables and object properties: `whaleOil`, `currentWhale`, `inventory`, `crosshairX`, `swimSpeedX`
|
||||||
|
- PascalCase for class names: `IntroScene`, `MapScene`, `HuntingScene`
|
||||||
|
- SCREAMING_SNAKE_CASE for constants is not used; magic numbers are inline or stored as camelCase object properties
|
||||||
|
- Abbreviated names in loops: `i`, `x`, `y` for coordinates
|
||||||
|
|
||||||
|
**Types:**
|
||||||
|
- No TypeScript; vanilla JavaScript with JSDoc comments for complex logic
|
||||||
|
- Object literals for data structures (e.g., inventory: `{ whaleOil: 0, fuel: 100, penguins: 0 }`)
|
||||||
|
- No explicit type annotations; types inferred from usage context
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
**Formatting:**
|
||||||
|
- No explicit formatter configured (no .prettierrc or similar found)
|
||||||
|
- 4 spaces for indentation (observed consistently across all files)
|
||||||
|
- Single statements per line; no cramping of expressions
|
||||||
|
- Line length varies; some lines exceed 100 characters
|
||||||
|
|
||||||
|
**Linting:**
|
||||||
|
- No ESLint or similar linting tool configured
|
||||||
|
- No explicit style guide enforced; conventions followed naturally
|
||||||
|
|
||||||
|
## Import Organization
|
||||||
|
|
||||||
|
**Order:**
|
||||||
|
1. Phaser framework imports: `import Phaser from 'phaser'`
|
||||||
|
2. Scene exports: `export default class SceneName extends Phaser.Scene`
|
||||||
|
3. Test framework imports: `import { describe, it, expect } from 'vitest'` and `import { test, expect } from '@playwright/test'`
|
||||||
|
|
||||||
|
**Path Aliases:**
|
||||||
|
- No path aliases configured
|
||||||
|
- Relative imports with `.js` extension explicit: `import IntroScene from './scenes/IntroScene.js'`
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- Defensive checks before operations: `if (this.currentWhale && this.currentWhale.getData('alive'))`
|
||||||
|
- Boundary checking for game state: `if (this.inventory.fuel < 2)` before processing whale
|
||||||
|
- No explicit error throwing; invalid states default to safe fallbacks
|
||||||
|
- Scene transitions use `this.scene.start()` with null coalescing: `this.inventory = data.inventory || { whaleOil: 0, fuel: 100, penguins: 0 }`
|
||||||
|
- Array bounds checking: loop guards with `.length` checks, array splice after removal
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
**Framework:** console (implicit; not found in code)
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- No logging statements observed in source code
|
||||||
|
- Game messaging via `showMessage()` method: text display in game UI
|
||||||
|
- Debug output would likely use console.log if needed (not observed)
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
**When to Comment:**
|
||||||
|
- Method-level comments for non-obvious logic: `// Ocean blue background`, `// Draw decorative waves`
|
||||||
|
- Inline comments for complex calculations: `// Can't hit whale while diving`, `// Keep within bounds`
|
||||||
|
- Comment frequency is moderate; most code is self-documenting through naming
|
||||||
|
|
||||||
|
**JSDoc/TSDoc:**
|
||||||
|
- No JSDoc comments found
|
||||||
|
- Vanilla JavaScript with method names that describe intent
|
||||||
|
|
||||||
|
## Function Design
|
||||||
|
|
||||||
|
**Size:**
|
||||||
|
- Methods range from 10-70 lines
|
||||||
|
- Larger methods like `create()` (30+ lines) perform initialization with calls to smaller helper methods
|
||||||
|
- Complex game logic broken into focused methods: `updateWhale()`, `updateHarpoons()`, `checkCollisions()`
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- Methods accept 0-3 parameters typically
|
||||||
|
- Event handlers use single parameter (e.g., `pointerdown` callbacks)
|
||||||
|
- Data passed through scene initialization: `this.scene.start('NextScene', { inventory: data })`
|
||||||
|
|
||||||
|
**Return Values:**
|
||||||
|
- Mostly void/no return (Phaser scene lifecycle methods)
|
||||||
|
- Some utility functions return boolean: `hasOwnProperty()` checks
|
||||||
|
- Game state propagated through `this` context, not function returns
|
||||||
|
|
||||||
|
## Module Design
|
||||||
|
|
||||||
|
**Exports:**
|
||||||
|
- Each scene file exports default class: `export default class SceneName extends Phaser.Scene`
|
||||||
|
- All scenes imported into `src/main.js` as class references
|
||||||
|
- Scene registration in Phaser config array: `scene: [IntroScene, ShipDeckScene, MapScene, TransitionScene, HuntingScene]`
|
||||||
|
|
||||||
|
**Barrel Files:**
|
||||||
|
- No index.js or barrel files found
|
||||||
|
- Each scene is independent; imports are explicit by filename
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
**Object Literals:**
|
||||||
|
- Inventory object: `{ whaleOil: 0, fuel: 100, penguins: 0 }`
|
||||||
|
- Configuration objects: Phaser game config in `src/main.js`
|
||||||
|
- Destination mapping in `TransitionScene.js`: `destinations` object with title, description, backgroundColor, visualType
|
||||||
|
|
||||||
|
**Phaser-Specific Patterns:**
|
||||||
|
- `setData(key, value)` / `getData(key)` for storing state on game objects
|
||||||
|
- Container objects with `add([children])` for grouping (e.g., whale body with parts)
|
||||||
|
- Scene transitions with inventory passed as data parameter
|
||||||
|
|
||||||
|
## Common Patterns Observed
|
||||||
|
|
||||||
|
**Scene Lifecycle:**
|
||||||
|
1. `constructor()`: Set scene key
|
||||||
|
2. `init(data)`: Receive data from previous scene
|
||||||
|
3. `create()`: Initialize scene graphics and setup
|
||||||
|
4. `update()`: Game loop updates (only used in HuntingScene)
|
||||||
|
|
||||||
|
**UI Element Creation:**
|
||||||
|
- Rectangle/circle/text created with `.add.rectangle()`, `.add.circle()`, `.add.text()`
|
||||||
|
- Interactive zones created with `setInteractive()` and `.on('pointerX', callback)`
|
||||||
|
- Hover/click feedback with `setScale()`, `setFillStyle()` updates
|
||||||
|
|
||||||
|
**Animation:**
|
||||||
|
- Phaser tweens for smooth transitions: `this.tweens.add({ targets: obj, property: value, duration: ms })`
|
||||||
|
- Sine/cosine wave patterns for natural movement: `Math.sin()`, `Math.cos()` for bobbing/sway
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Convention analysis: 2026-02-04*
|
||||||
161
.planning/codebase/INTEGRATIONS.md
Normal file
161
.planning/codebase/INTEGRATIONS.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# External Integrations
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## APIs & External Services
|
||||||
|
|
||||||
|
**Not applicable** - No external APIs integrated. Game is fully self-contained.
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
**Databases:**
|
||||||
|
- Not applicable - No database required or used
|
||||||
|
- Game state is managed in-memory during gameplay
|
||||||
|
- State location: Passed via scene parameters in `src/main.js`
|
||||||
|
|
||||||
|
**File Storage:**
|
||||||
|
- Local filesystem only - Assets would be bundled with application
|
||||||
|
- No remote file storage configured
|
||||||
|
- Asset directories (planned): `assets/sprites/`, `assets/backgrounds/`
|
||||||
|
|
||||||
|
**Caching:**
|
||||||
|
- Browser caching only - Static assets cached by nginx (1 year TTL)
|
||||||
|
- No server-side caching layer
|
||||||
|
- Vite dev server handles hot module replacement during development
|
||||||
|
|
||||||
|
**State Management:**
|
||||||
|
- Phaser Scene-based state management
|
||||||
|
- Game state example from `src/scenes/IntroScene.js`:
|
||||||
|
```javascript
|
||||||
|
const inventory = {
|
||||||
|
whaleOil: 0,
|
||||||
|
fuel: 100,
|
||||||
|
penguins: 0
|
||||||
|
};
|
||||||
|
this.scene.start('ShipDeckScene', { inventory: inventory });
|
||||||
|
```
|
||||||
|
- State passed between scenes as parameters
|
||||||
|
- No persistence layer (state resets on page refresh)
|
||||||
|
|
||||||
|
## Authentication & Identity
|
||||||
|
|
||||||
|
**Not applicable** - No user authentication
|
||||||
|
- Game is anonymous and stateless
|
||||||
|
- No user accounts or login required
|
||||||
|
- All players access the same game instance
|
||||||
|
|
||||||
|
## Monitoring & Observability
|
||||||
|
|
||||||
|
**Error Tracking:**
|
||||||
|
- Not integrated
|
||||||
|
- Console errors only (browser developer tools)
|
||||||
|
- No remote error reporting configured
|
||||||
|
|
||||||
|
**Logs:**
|
||||||
|
- Browser console logging only
|
||||||
|
- Nginx access logs available in container at `/var/log/nginx/`
|
||||||
|
- Docker container logs accessible via: `docker-compose logs -f`
|
||||||
|
- Vite dev server logs to terminal
|
||||||
|
|
||||||
|
**Health Checks:**
|
||||||
|
- Docker health check: `wget --quiet --tries=1 --spider http://127.0.0.1/`
|
||||||
|
- Interval: 30 seconds
|
||||||
|
- Timeout: 3 seconds
|
||||||
|
- Retries: 3 before marking unhealthy
|
||||||
|
- Start period: 5 seconds
|
||||||
|
|
||||||
|
## CI/CD & Deployment
|
||||||
|
|
||||||
|
**Hosting:**
|
||||||
|
- Docker container on `node03.tricnet.de`
|
||||||
|
- Served via Nginx (Alpine Linux)
|
||||||
|
- Manual deployment via `deploy.sh`
|
||||||
|
|
||||||
|
**CI Pipeline:**
|
||||||
|
- Not detected - No automated CI/CD system configured
|
||||||
|
- Deployment process: Manual `./deploy.sh`
|
||||||
|
- Uses rsync to sync files to node03
|
||||||
|
- SSH to rebuild Docker container
|
||||||
|
- Rebuilds docker-compose services
|
||||||
|
|
||||||
|
**Deployment Method:**
|
||||||
|
- Deployment file: `deploy.sh`
|
||||||
|
- Sync target: `tho@node03.tricnet.de:/home/tho/whalehunting/`
|
||||||
|
- Excludes: `node_modules/`, `dist/`, `.git/`, `.DS_Store`
|
||||||
|
- Build command: `docker-compose down && docker-compose up -d --build`
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
**Required env vars:**
|
||||||
|
- None - Game requires no environment variables at runtime
|
||||||
|
- Docker container runs with default environment
|
||||||
|
|
||||||
|
**Secrets location:**
|
||||||
|
- Not applicable - No secrets, API keys, or credentials required
|
||||||
|
- SSH key required for deployment (stored on developer machine)
|
||||||
|
|
||||||
|
**Development Environment:**
|
||||||
|
- Game automatically runs on `http://localhost:5173` (Vite default)
|
||||||
|
- Playwright configured to use `http://localhost:5173` as baseURL
|
||||||
|
- No .env file needed for development
|
||||||
|
|
||||||
|
## Webhooks & Callbacks
|
||||||
|
|
||||||
|
**Incoming:**
|
||||||
|
- Not applicable - No webhooks received
|
||||||
|
|
||||||
|
**Outgoing:**
|
||||||
|
- Not applicable - No webhooks sent
|
||||||
|
- Game makes no HTTP requests
|
||||||
|
|
||||||
|
## Network Requirements
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- Must be able to access `localhost:5173` (Vite dev server)
|
||||||
|
- Must be able to access `localhost:5173` for Playwright E2E tests
|
||||||
|
- Vitest UI server binds to `0.0.0.0:51204` (all interfaces)
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
- Must be accessible on port 8880 (external Docker port)
|
||||||
|
- Health checks require outbound HTTP to `http://127.0.0.1/` (internal to container)
|
||||||
|
- No outbound internet access required from game runtime
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
**Client-Side Only:**
|
||||||
|
- All game logic runs in browser
|
||||||
|
- No data sent to any backend or external service
|
||||||
|
- Game state never persists (no save/load feature implemented)
|
||||||
|
- Player actions: Click/tap interactions → Phaser input handlers → Scene state changes
|
||||||
|
|
||||||
|
**Example State Flow** (from `src/scenes/IntroScene.js`):
|
||||||
|
1. User clicks "SET SAIL" button
|
||||||
|
2. Button click handler creates inventory object
|
||||||
|
3. Scene transitions: `this.scene.start('ShipDeckScene', { inventory })`
|
||||||
|
4. ShipDeckScene receives inventory via init parameter
|
||||||
|
5. Game continues with same inventory state
|
||||||
|
|
||||||
|
## Testing Infrastructure
|
||||||
|
|
||||||
|
**Unit Tests:**
|
||||||
|
- Framework: Vitest 4.0.16
|
||||||
|
- Environment: jsdom
|
||||||
|
- Test directory: `tests/unit/`
|
||||||
|
- Run: `npm run test` or `npm run test:ui` (with visual UI)
|
||||||
|
- Coverage: `npm run test:coverage`
|
||||||
|
|
||||||
|
**E2E Tests:**
|
||||||
|
- Framework: Playwright 1.57.0
|
||||||
|
- Test directory: `tests/e2e/`
|
||||||
|
- Browsers tested: Chromium, iPhone 12 (mobile)
|
||||||
|
- Run: `npm run test:e2e` or `npm run test:e2e:ui`
|
||||||
|
- Screenshots: Captured on failure only
|
||||||
|
- Traces: Captured on first retry
|
||||||
|
|
||||||
|
**Test Configuration Files:**
|
||||||
|
- `vitest.config.js` - Unit test setup
|
||||||
|
- `playwright.config.js` - E2E test setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Integration audit: 2026-02-04*
|
||||||
135
.planning/codebase/STACK.md
Normal file
135
.planning/codebase/STACK.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
# Technology Stack
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
**Primary:**
|
||||||
|
- JavaScript (ES6 modules) - All source code and configuration
|
||||||
|
|
||||||
|
## Runtime
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- Node.js 20 (Alpine) - Development and build time
|
||||||
|
- Browser (Chromium, Safari) - Runtime environment for game
|
||||||
|
|
||||||
|
**Package Manager:**
|
||||||
|
- npm - Dependency management
|
||||||
|
- Lockfile: `package-lock.json` (present)
|
||||||
|
|
||||||
|
## Frameworks
|
||||||
|
|
||||||
|
**Core:**
|
||||||
|
- Phaser 3.80.1 - 2D game framework and physics engine
|
||||||
|
- Used in: `src/main.js` and all scene files
|
||||||
|
- Provides game loop, rendering, input handling, physics (arcade)
|
||||||
|
|
||||||
|
**Build/Dev:**
|
||||||
|
- Vite 5.0.0 - Build tool and development server
|
||||||
|
- Config: Not detected (uses Vite defaults)
|
||||||
|
- Dev server: Port 5173 (standard)
|
||||||
|
- Output directory: `dist/`
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- Vitest 4.0.16 - Unit test framework
|
||||||
|
- Config: `vitest.config.js`
|
||||||
|
- Environment: jsdom (browser-like environment for testing)
|
||||||
|
- UI: @vitest/ui 4.0.16 for visual test runner
|
||||||
|
|
||||||
|
- Playwright 1.57.0 - E2E testing framework
|
||||||
|
- Config: `playwright.config.js`
|
||||||
|
- Test directory: `tests/e2e/`
|
||||||
|
- Browsers: Chromium, iPhone 12 (mobile simulation)
|
||||||
|
- Screenshots: Captured on test failure
|
||||||
|
|
||||||
|
## Key Dependencies
|
||||||
|
|
||||||
|
**Critical:**
|
||||||
|
- phaser (3.80.1) - Complete game development framework
|
||||||
|
- Why it matters: Core engine for all game logic, rendering, and physics
|
||||||
|
|
||||||
|
**Development Only:**
|
||||||
|
- @playwright/test (1.57.0) - E2E test runner with browser automation
|
||||||
|
- @vitest/ui (4.0.16) - Visual UI for viewing test results
|
||||||
|
- jsdom (27.3.0) - DOM implementation for testing in Node.js environment
|
||||||
|
- vite (5.0.0) - Lightning-fast build tool and dev server
|
||||||
|
- vitest (4.0.16) - Unit test framework built on Vite
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Environment:**
|
||||||
|
- No environment variables required for runtime
|
||||||
|
- Game runs entirely in browser with no backend dependencies
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
- `vite.config.js` - Not present (uses defaults)
|
||||||
|
- `playwright.config.js` - E2E test configuration
|
||||||
|
- `vitest.config.js` - Unit test configuration with jsdom environment
|
||||||
|
- Vite configured for module type: module (ES6)
|
||||||
|
|
||||||
|
## Platform Requirements
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
- Node.js 20+
|
||||||
|
- npm 8+ (included with Node 20)
|
||||||
|
- Git (for version control)
|
||||||
|
- Linux/macOS/Windows (any platform with Node.js)
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
- Nginx web server (Alpine Linux image)
|
||||||
|
- Docker (for containerized deployment)
|
||||||
|
- Static file hosting (game is fully client-side)
|
||||||
|
- No database required
|
||||||
|
- No backend API required
|
||||||
|
|
||||||
|
## Build & Deployment
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
```bash
|
||||||
|
npm install # Install dependencies
|
||||||
|
npm run dev # Start dev server on port 5173
|
||||||
|
npm run test # Run unit tests
|
||||||
|
npm run test:ui # Run tests with visual UI
|
||||||
|
npm run test:e2e # Run Playwright E2E tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
```bash
|
||||||
|
npm run build # Build to dist/ directory
|
||||||
|
docker build -t whalehunting-game . # Build Docker image
|
||||||
|
docker-compose up -d # Deploy with Docker Compose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Stack:**
|
||||||
|
- Base image: `nginx:alpine` (production serving)
|
||||||
|
- Build stage: `node:20-alpine` (build application)
|
||||||
|
- Port: 8880 (external), 80 (container)
|
||||||
|
- Health checks: wget ping every 30s
|
||||||
|
- Network: Docker bridge network `whalehunting-network`
|
||||||
|
|
||||||
|
**Deployment Target:**
|
||||||
|
- Docker container on node03.tricnet.de
|
||||||
|
- Deployed via rsync + SSH (see `deploy.sh`)
|
||||||
|
- Game served at: `http://node03.tricnet.de:8880`
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
- Vite provides fast module bundling
|
||||||
|
- Production build is minified and optimized
|
||||||
|
|
||||||
|
**Runtime:**
|
||||||
|
- Phaser uses WebGL/Canvas rendering (automatic selection via Phaser.AUTO)
|
||||||
|
- Game configured with fixed dimensions (800x600) with responsive scaling
|
||||||
|
- Responsive scaling mode: FIT with auto-centering
|
||||||
|
- Min viewport: 320x240 (mobile support)
|
||||||
|
- Arcade physics engine configured with no gravity (2D top-down view)
|
||||||
|
|
||||||
|
**Serving:**
|
||||||
|
- Nginx configured with gzip compression
|
||||||
|
- Static asset caching: 1 year (as noted in README)
|
||||||
|
- Health checks ensure container availability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Stack analysis: 2026-02-04*
|
||||||
177
.planning/codebase/STRUCTURE.md
Normal file
177
.planning/codebase/STRUCTURE.md
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
# Codebase Structure
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Directory Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
whalehunting/
|
||||||
|
├── src/ # Game source code
|
||||||
|
│ ├── main.js # Phaser game initialization and configuration
|
||||||
|
│ └── scenes/ # Game scene implementations
|
||||||
|
│ ├── IntroScene.js
|
||||||
|
│ ├── ShipDeckScene.js
|
||||||
|
│ ├── MapScene.js
|
||||||
|
│ ├── TransitionScene.js
|
||||||
|
│ └── HuntingScene.js
|
||||||
|
├── tests/ # Test suites
|
||||||
|
│ ├── unit/ # Unit tests (Vitest)
|
||||||
|
│ │ └── game-logic.test.js
|
||||||
|
│ └── e2e/ # End-to-end tests (Playwright)
|
||||||
|
│ └── game-flow.spec.js
|
||||||
|
├── .planning/ # GSD planning documents
|
||||||
|
│ └── codebase/ # Architecture analysis
|
||||||
|
├── index.html # HTML entry point
|
||||||
|
├── package.json # Dependencies and scripts
|
||||||
|
├── vitest.config.js # Unit test configuration
|
||||||
|
├── playwright.config.js # E2E test configuration
|
||||||
|
├── nginx.conf # Web server config for deployment
|
||||||
|
├── Dockerfile # Container image definition
|
||||||
|
├── docker-compose.yml # Multi-container orchestration
|
||||||
|
└── deploy.sh # Automated deployment script
|
||||||
|
```
|
||||||
|
|
||||||
|
## Directory Purposes
|
||||||
|
|
||||||
|
**src/:**
|
||||||
|
- Purpose: All game source code
|
||||||
|
- Contains: Main game initialization and scene implementations
|
||||||
|
- Key files: `main.js` is the entry point that configures Phaser
|
||||||
|
|
||||||
|
**src/scenes/:**
|
||||||
|
- Purpose: Game scene implementations
|
||||||
|
- Contains: Five scene classes, one per major game location/mode
|
||||||
|
- Key files: IntroScene starts the game, HuntingScene has most complex logic
|
||||||
|
|
||||||
|
**tests/:**
|
||||||
|
- Purpose: All test code separated from source
|
||||||
|
- Contains: Unit tests and E2E tests in separate subdirectories
|
||||||
|
- Key files: `game-logic.test.js` for game mechanics, `game-flow.spec.js` for user flows
|
||||||
|
|
||||||
|
**tests/unit/:**
|
||||||
|
- Purpose: Logic testing without Phaser
|
||||||
|
- Contains: Vitest tests for inventory calculations, fuel costs, barrel logic
|
||||||
|
- Key files: `game-logic.test.js`
|
||||||
|
|
||||||
|
**tests/e2e/:**
|
||||||
|
- Purpose: Full application flow testing
|
||||||
|
- Contains: Playwright tests simulating user interactions on actual game canvas
|
||||||
|
- Key files: `game-flow.spec.js`
|
||||||
|
|
||||||
|
**.planning/codebase/:**
|
||||||
|
- Purpose: Analysis documents for code navigation
|
||||||
|
- Contains: ARCHITECTURE.md, STRUCTURE.md, CONVENTIONS.md, TESTING.md, CONCERNS.md
|
||||||
|
- Generated by: GSD mapping processes
|
||||||
|
|
||||||
|
## Key File Locations
|
||||||
|
|
||||||
|
**Entry Points:**
|
||||||
|
- `index.html`: Browser loads this, contains game-container div and script import
|
||||||
|
- `src/main.js`: Phaser game configuration and scene list initialization
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
- `vitest.config.js`: Unit test runner with jsdom environment, port 51204 for UI server
|
||||||
|
- `playwright.config.js`: E2E test runner with Chromium and iPhone 12 profiles
|
||||||
|
- `package.json`: Dependencies (Phaser 3.80.1, Vite, Vitest, Playwright)
|
||||||
|
- `nginx.conf`: Web server routing (not actively used in dev)
|
||||||
|
- `docker-compose.yml`: Local dev: port 5173 for Vite server
|
||||||
|
- `Dockerfile`: Production build with multi-stage for size optimization
|
||||||
|
|
||||||
|
**Core Logic:**
|
||||||
|
- `src/scenes/IntroScene.js`: Game entry, starts with fuel=100, oil=0, penguins=0
|
||||||
|
- `src/scenes/ShipDeckScene.js`: Inventory display, barrel visualization, penguin cage reveal
|
||||||
|
- `src/scenes/MapScene.js`: Location selection (Hunting Grounds, Antarctic, Port)
|
||||||
|
- `src/scenes/TransitionScene.js`: Atmospheric journey scenes, destination-specific visuals
|
||||||
|
- `src/scenes/HuntingScene.js`: Core gameplay - whale spawning, harpoon mechanics, fuel consumption
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
- `tests/unit/game-logic.test.js`: Barrel calculations, inventory limits, fuel costs, whale health, crosshair clamping
|
||||||
|
- `tests/e2e/game-flow.spec.js`: Full flows (intro→ship→map→hunting), mobile viewport, desktop scaling
|
||||||
|
|
||||||
|
## Naming Conventions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- PascalCase + Scene suffix: `IntroScene.js`, `ShipDeckScene.js`
|
||||||
|
- camelCase for non-scene modules: Would apply if utilities existed
|
||||||
|
- Test files: Matched source with `.test.js` or `.spec.js` suffix
|
||||||
|
- Config files: kebab-case: `playwright.config.js`, `vitest.config.js`
|
||||||
|
|
||||||
|
**Directories:**
|
||||||
|
- lowercase plural: `src/scenes/`, `tests/unit/`, `tests/e2e/`
|
||||||
|
- camelCase for compound: Not currently used
|
||||||
|
|
||||||
|
**Class Names:**
|
||||||
|
- PascalCase: `IntroScene`, `ShipDeckScene`, `MapScene`, `TransitionScene`, `HuntingScene`
|
||||||
|
- Extend `Phaser.Scene` directly
|
||||||
|
|
||||||
|
**Methods:**
|
||||||
|
- camelCase: `create()`, `update()`, `init()`, `createDeck()`, `createBarrels()`, `updateInventoryDisplay()`
|
||||||
|
- Lifecycle methods (create, update) are Phaser conventions
|
||||||
|
- Helper methods prefix with action: `create*()`, `update*()`, `show*()`, `draw*()`
|
||||||
|
|
||||||
|
**Variables:**
|
||||||
|
- camelCase for instance: `this.inventory`, `this.currentWhale`, `this.harpoons`
|
||||||
|
- camelCase for local: `barrelCount`, `swimSpeedX`, `healthPercent`
|
||||||
|
- UPPERCASE for constants: `FUEL_COST = 2`, `MAX_HEALTH = 3`
|
||||||
|
|
||||||
|
**Game State Objects:**
|
||||||
|
- Inventory structure: `{ whaleOil: number, fuel: number, penguins: number }`
|
||||||
|
- Whale data stored via `setData()`: `health`, `alive`, `diving`, `swimSpeedX`, `swimSpeedY`, `direction`
|
||||||
|
|
||||||
|
## Where to Add New Code
|
||||||
|
|
||||||
|
**New Scene/Location:**
|
||||||
|
1. Create `src/scenes/NewScene.js` extending Phaser.Scene
|
||||||
|
2. Implement `init(data)` to receive inventory
|
||||||
|
3. Implement `create()` for rendering
|
||||||
|
4. Implement `update()` if frame-by-frame logic needed
|
||||||
|
5. Call `this.scene.start('MapScene', { inventory: this.inventory })` to transition
|
||||||
|
6. Add to scene list in `src/main.js` config array
|
||||||
|
7. Add location marker in `MapScene.js` that routes to new scene
|
||||||
|
|
||||||
|
**New Game Mechanic:**
|
||||||
|
- Hunting scene has most game logic: HuntingScene.js handles whale spawning, harpoon firing, collision detection, health tracking
|
||||||
|
- Add mechanics as methods in relevant scene class
|
||||||
|
- Store persistent state in `this.inventory` object
|
||||||
|
- Display UI changes via `updateInventoryDisplay()` pattern
|
||||||
|
|
||||||
|
**Utilities (if needed):**
|
||||||
|
- Create `src/utils/` directory for shared helpers
|
||||||
|
- Example: `src/utils/inventory.js` for max capacity functions
|
||||||
|
- Example: `src/utils/collision.js` for distance calculations
|
||||||
|
- Import in scenes: `import { calculateBarrels } from '../utils/inventory.js';`
|
||||||
|
|
||||||
|
**New UI Component Pattern:**
|
||||||
|
- Create methods in scene: `createInventoryDisplay()`, `createMessageBox()`
|
||||||
|
- Return reference to element for later updates
|
||||||
|
- Update via methods: `updateInventoryDisplay()`, `showMessage(text)`
|
||||||
|
- Use semi-transparent rectangles for panels with `.setFillStyle(0x000000, 0.7)`
|
||||||
|
|
||||||
|
## Special Directories
|
||||||
|
|
||||||
|
**node_modules/:**
|
||||||
|
- Purpose: Installed dependencies
|
||||||
|
- Generated: Yes (via npm install)
|
||||||
|
- Committed: No (.gitignore)
|
||||||
|
|
||||||
|
**dist/:**
|
||||||
|
- Purpose: Vite production build output
|
||||||
|
- Generated: Yes (via npm run build)
|
||||||
|
- Committed: No (.gitignore)
|
||||||
|
- Files: Optimized JS/CSS for deployment
|
||||||
|
|
||||||
|
**.git/:**
|
||||||
|
- Purpose: Git repository metadata
|
||||||
|
- Generated: Yes (git init)
|
||||||
|
- Committed: Meta repository
|
||||||
|
- Notable: Recent commits track feature additions and testing infrastructure
|
||||||
|
|
||||||
|
**.planning/:**
|
||||||
|
- Purpose: GSD orchestration files and analysis documents
|
||||||
|
- Generated: Yes (via /gsd commands)
|
||||||
|
- Committed: Yes (tracked in git)
|
||||||
|
- Structure: `codebase/` contains ARCHITECTURE.md, STRUCTURE.md, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Structure analysis: 2026-02-04*
|
||||||
314
.planning/codebase/TESTING.md
Normal file
314
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
# Testing Patterns
|
||||||
|
|
||||||
|
**Analysis Date:** 2026-02-04
|
||||||
|
|
||||||
|
## Test Framework
|
||||||
|
|
||||||
|
**Runner:**
|
||||||
|
- Vitest 4.0.16 - Unit test runner
|
||||||
|
- Playwright 1.57.0 - E2E test runner
|
||||||
|
|
||||||
|
**Config Files:**
|
||||||
|
- Unit tests: `vitest.config.js`
|
||||||
|
- E2E tests: `playwright.config.js`
|
||||||
|
|
||||||
|
**Run Commands:**
|
||||||
|
```bash
|
||||||
|
npm run test # Run unit tests (Vitest)
|
||||||
|
npm run test:ui # Run tests with Vitest UI
|
||||||
|
npm run test:coverage # Run tests with coverage report
|
||||||
|
npm run test:e2e # Run E2E tests (Playwright)
|
||||||
|
npm run test:e2e:ui # Run E2E tests with Playwright UI
|
||||||
|
npm run test:all # Run all tests (unit + E2E)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test File Organization
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- Unit tests: `tests/unit/` directory
|
||||||
|
- E2E tests: `tests/e2e/` directory
|
||||||
|
- Separate from source code (not co-located)
|
||||||
|
|
||||||
|
**Naming:**
|
||||||
|
- Unit tests: `.test.js` suffix: `game-logic.test.js`
|
||||||
|
- E2E tests: `.spec.js` suffix: `game-flow.spec.js`
|
||||||
|
|
||||||
|
**Structure:**
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/
|
||||||
|
│ └── game-logic.test.js
|
||||||
|
└── e2e/
|
||||||
|
└── game-flow.spec.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unit Test Structure
|
||||||
|
|
||||||
|
**Framework:** Vitest with globals enabled
|
||||||
|
|
||||||
|
**Pattern from `tests/unit/game-logic.test.js`:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('Game Logic - Inventory Management', () => {
|
||||||
|
describe('Barrel Calculations', () => {
|
||||||
|
it('should calculate correct fuel barrel count', () => {
|
||||||
|
const fuel = 100;
|
||||||
|
const barrelCount = Math.ceil(fuel / 10);
|
||||||
|
expect(barrelCount).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Organization:**
|
||||||
|
- Top-level `describe()` blocks group by feature/system (e.g., "Game Logic - Inventory Management")
|
||||||
|
- Nested `describe()` blocks organize related tests (e.g., "Barrel Calculations")
|
||||||
|
- Individual `it()` blocks test single logical assertions
|
||||||
|
- Test names are descriptive: "should [expected behavior]"
|
||||||
|
|
||||||
|
## E2E Test Structure
|
||||||
|
|
||||||
|
**Framework:** Playwright with page object patterns
|
||||||
|
|
||||||
|
**Pattern from `tests/e2e/game-flow.spec.js`:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Whale Hunting Game - Main Flow', () => {
|
||||||
|
test('should load the intro scene', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page).toHaveTitle(/Whale Hunting/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Organization:**
|
||||||
|
- `test.describe()` blocks group related scenarios
|
||||||
|
- Individual `test()` blocks test user flows
|
||||||
|
- Setup patterns: `test.beforeEach()` for common navigation
|
||||||
|
- Viewport configuration: `test.use()` for device-specific testing
|
||||||
|
- Async/await pattern for all page interactions
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
**Framework:** None configured or used
|
||||||
|
|
||||||
|
**Patterns:**
|
||||||
|
- No mocks found in unit tests
|
||||||
|
- Tests verify pure calculation logic (Math.ceil, Math.min)
|
||||||
|
- E2E tests work with actual running game instance
|
||||||
|
|
||||||
|
**What to Mock:**
|
||||||
|
- Not applicable; unit tests use pure functions without external dependencies
|
||||||
|
- Phaser framework is not mocked (games tested in browser context)
|
||||||
|
|
||||||
|
**What NOT to Mock:**
|
||||||
|
- Scene transitions (tested via canvas visibility)
|
||||||
|
- Phaser game instances (tested via E2E)
|
||||||
|
|
||||||
|
## Fixtures and Test Data
|
||||||
|
|
||||||
|
**Test Data:**
|
||||||
|
```javascript
|
||||||
|
// Direct inline values
|
||||||
|
it('should calculate correct fuel barrel count', () => {
|
||||||
|
const fuel = 100;
|
||||||
|
const barrelCount = Math.ceil(fuel / 10);
|
||||||
|
expect(barrelCount).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inventory object template
|
||||||
|
const inventory = { whaleOil: 0, fuel: 100, penguins: 0 };
|
||||||
|
```
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- No shared fixture files; test data created inline within test functions
|
||||||
|
- Inventory object structure repeated across multiple tests
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
```javascript
|
||||||
|
// vitest.config.js
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'html', 'lcov'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'dist/',
|
||||||
|
'*.config.js',
|
||||||
|
'tests/e2e/**'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**View Coverage:**
|
||||||
|
```bash
|
||||||
|
npm run test:coverage
|
||||||
|
# Creates HTML report in coverage/
|
||||||
|
# View: coverage/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
**Excluded from Coverage:**
|
||||||
|
- E2E tests (`tests/e2e/**`)
|
||||||
|
- Config files (`*.config.js`)
|
||||||
|
- Dependencies and build output
|
||||||
|
|
||||||
|
## Test Types
|
||||||
|
|
||||||
|
**Unit Tests:**
|
||||||
|
- Scope: Pure calculation logic (barrel counts, fuel consumption, bounds checking)
|
||||||
|
- Approach: Direct function calls with known inputs
|
||||||
|
- No DOM interaction; test game logic in isolation
|
||||||
|
- Coverage: `tests/unit/game-logic.test.js`
|
||||||
|
- Examples:
|
||||||
|
- Barrel calculation: `Math.ceil(fuel / 10)`
|
||||||
|
- Inventory limits: `Math.min(currentFuel + 10, maxFuel)`
|
||||||
|
- Whale health: Health deduction logic
|
||||||
|
- Mobile detection: User agent regex testing
|
||||||
|
- Crosshair bounds: Clamp function testing
|
||||||
|
|
||||||
|
**Integration Tests:**
|
||||||
|
- Not explicitly used
|
||||||
|
- Closest: E2E tests that verify scene transitions
|
||||||
|
|
||||||
|
**E2E Tests:**
|
||||||
|
- Scope: Complete user workflows (game startup to hunting)
|
||||||
|
- Approach: Browser automation via Playwright
|
||||||
|
- Tests: Canvas visibility, scene transitions, button clicks, viewport scaling
|
||||||
|
- Coverage: `tests/e2e/game-flow.spec.js`
|
||||||
|
- Desktop & mobile scenarios tested with viewport configuration
|
||||||
|
- Examples:
|
||||||
|
- "should load the intro scene"
|
||||||
|
- "should navigate to hunting grounds"
|
||||||
|
- "should work on mobile viewport"
|
||||||
|
- "should scale properly on desktop"
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
**Async Testing (E2E):**
|
||||||
|
```javascript
|
||||||
|
test('should start game from intro scene', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } });
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- `async ({ page })` destructures Playwright fixture
|
||||||
|
- `await` on page navigation and interactions
|
||||||
|
- `page.waitForTimeout()` for animation/transition delays
|
||||||
|
- `page.locator()` for element selection
|
||||||
|
|
||||||
|
**Error Testing (Unit):**
|
||||||
|
```javascript
|
||||||
|
it('should not process whale without enough fuel', () => {
|
||||||
|
let fuel = 1;
|
||||||
|
const requiredFuel = 2;
|
||||||
|
const canProcess = fuel >= requiredFuel;
|
||||||
|
expect(canProcess).toBe(false);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Boolean logic to test conditions
|
||||||
|
- No exception throwing; test state validation
|
||||||
|
|
||||||
|
**Mobile Testing (E2E):**
|
||||||
|
```javascript
|
||||||
|
test.describe('Mobile Compatibility', () => {
|
||||||
|
test.use({
|
||||||
|
viewport: { width: 375, height: 667 },
|
||||||
|
isMobile: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work on mobile viewport', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
const canvasBox = await canvas.boundingBox();
|
||||||
|
expect(canvasBox?.width).toBeLessThanOrEqual(375);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- `test.use()` sets viewport and device context
|
||||||
|
- Tap interactions: `canvas.tap()`
|
||||||
|
- BoundingBox assertions: verify canvas scaling
|
||||||
|
|
||||||
|
## Test Isolation
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
- E2E tests use `test.beforeEach()` for repeated navigation setup
|
||||||
|
- No shared state between tests
|
||||||
|
- Each test starts fresh from home page
|
||||||
|
|
||||||
|
**Teardown:**
|
||||||
|
- Playwright automatically closes browser context between tests
|
||||||
|
- No explicit cleanup required
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
**Vitest (`vitest.config.js`):**
|
||||||
|
- Environment: jsdom (browser-like DOM)
|
||||||
|
- Globals enabled: `describe`, `it`, `expect` available without imports
|
||||||
|
- Include pattern: `tests/unit/**/*.test.js`
|
||||||
|
- Server runs on port 51204 for Vitest UI
|
||||||
|
|
||||||
|
**Playwright (`playwright.config.js`):**
|
||||||
|
- Projects: Chromium and mobile (iPhone 12)
|
||||||
|
- Base URL: `http://localhost:5173` (dev server)
|
||||||
|
- Test directory: `tests/e2e/`
|
||||||
|
- Retries: 2 in CI, 0 locally
|
||||||
|
- Screenshots: Only on failure
|
||||||
|
- Trace: On first retry
|
||||||
|
- Server: Runs `npm run dev` before tests
|
||||||
|
|
||||||
|
## Test Execution Flow
|
||||||
|
|
||||||
|
1. **Unit Tests (Vitest):**
|
||||||
|
- Runs isolated game logic tests
|
||||||
|
- Uses jsdom environment
|
||||||
|
- No Phaser instance needed
|
||||||
|
- Fast execution
|
||||||
|
|
||||||
|
2. **E2E Tests (Playwright):**
|
||||||
|
- Starts dev server via `npm run dev`
|
||||||
|
- Launches Chromium and mobile browsers
|
||||||
|
- Navigates to `http://localhost:5173`
|
||||||
|
- Performs user interactions
|
||||||
|
- Verifies game state via canvas visibility
|
||||||
|
- Slower execution but tests full stack
|
||||||
|
|
||||||
|
## Coverage Gaps
|
||||||
|
|
||||||
|
**Not Tested (Unit):**
|
||||||
|
- Scene rendering and graphics creation
|
||||||
|
- Phaser animation timing and tweens
|
||||||
|
- Player input handling (mouse/touch)
|
||||||
|
- Collision detection logic
|
||||||
|
|
||||||
|
**Not Tested (E2E):**
|
||||||
|
- Detailed game mechanics (whale health, harpoon trajectory)
|
||||||
|
- Inventory state persistence
|
||||||
|
- Exact positioning of game elements
|
||||||
|
- Performance under load
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Testing analysis: 2026-02-04*
|
||||||
@@ -31,7 +31,7 @@ EXPOSE 80
|
|||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
|
CMD wget --quiet --tries=1 --spider http://127.0.0.1/ || exit 1
|
||||||
|
|
||||||
# Start nginx
|
# Start nginx
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
51
deploy-k8s.sh
Executable file
51
deploy-k8s.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Deploy whalehunting to Kubernetes via ArgoCD
|
||||||
|
# Usage: ./deploy-k8s.sh [tag]
|
||||||
|
# Example: ./deploy-k8s.sh v1.2.0
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GITEA_URL="git.kube2.tricnet.de"
|
||||||
|
REPO_NAME="whalehunting"
|
||||||
|
IMAGE_TAG="${1:-latest}"
|
||||||
|
|
||||||
|
echo "=== Whalehunting Kubernetes Deployment ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 1: Build Docker image
|
||||||
|
echo "1. Building Docker image..."
|
||||||
|
docker build -t ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG} .
|
||||||
|
|
||||||
|
# Step 2: Push image to registry
|
||||||
|
echo ""
|
||||||
|
echo "2. Pushing image to Gitea registry..."
|
||||||
|
docker push ${GITEA_URL}/admin/${REPO_NAME}:${IMAGE_TAG}
|
||||||
|
|
||||||
|
# Step 3: Update image tag in kustomization
|
||||||
|
echo ""
|
||||||
|
echo "3. Updating image tag in kustomization.yaml..."
|
||||||
|
sed -i "s/newTag: .*/newTag: ${IMAGE_TAG}/" k8s/kustomization.yaml
|
||||||
|
|
||||||
|
# Step 4: Commit and push to Gitea
|
||||||
|
echo ""
|
||||||
|
echo "4. Committing and pushing to Gitea..."
|
||||||
|
git add -A
|
||||||
|
git commit -m "deploy: update image to ${IMAGE_TAG}" || echo "No changes to commit"
|
||||||
|
git push origin master
|
||||||
|
|
||||||
|
# Step 5: Ensure ArgoCD application exists
|
||||||
|
echo ""
|
||||||
|
echo "5. Ensuring ArgoCD application exists..."
|
||||||
|
ssh root@kube2.tricnet.de "kubectl apply -f -" < k8s/argocd-application.yaml
|
||||||
|
|
||||||
|
# Step 6: Wait for sync and check status
|
||||||
|
echo ""
|
||||||
|
echo "6. Checking deployment status..."
|
||||||
|
sleep 5
|
||||||
|
ssh root@kube2.tricnet.de "kubectl get application whalehunting -n argocd"
|
||||||
|
ssh root@kube2.tricnet.de "kubectl get pods -n whalehunting"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Deployment complete ==="
|
||||||
|
echo "Game URL: https://whalehunting.kube2.tricnet.de"
|
||||||
23
deploy.sh
Executable file
23
deploy.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Deploy script for whale hunting game
|
||||||
|
# Syncs files to node03 and rebuilds the Docker container
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
echo "🚢 Syncing files to node03..."
|
||||||
|
rsync -avz --exclude 'node_modules' --exclude 'dist' --exclude '.git' --exclude '.DS_Store' \
|
||||||
|
./ tho@node03.tricnet.de:/home/tho/whalehunting/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🐳 Rebuilding and restarting Docker container..."
|
||||||
|
ssh tho@node03.tricnet.de "cd /home/tho/whalehunting && docker-compose down && docker-compose up -d --build"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo "🌐 Game available at: http://node03.tricnet.de:8880"
|
||||||
|
|
||||||
|
# Check container status
|
||||||
|
echo ""
|
||||||
|
echo "📊 Container status:"
|
||||||
|
ssh tho@node03.tricnet.de "cd /home/tho/whalehunting && docker-compose ps"
|
||||||
@@ -11,7 +11,7 @@ services:
|
|||||||
- "8880:80"
|
- "8880:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
17
index.html
17
index.html
@@ -2,9 +2,19 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-fullscreen">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<title>Whale Hunting - A Point & Click Adventure</title>
|
<title>Whale Hunting - A Point & Click Adventure</title>
|
||||||
<style>
|
<style>
|
||||||
|
* {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -14,9 +24,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
touch-action: none;
|
||||||
}
|
}
|
||||||
#game-container {
|
#game-container {
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
touch-action: none;
|
||||||
|
max-width: 100vw;
|
||||||
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
20
k8s/argocd-application.yaml
Normal file
20
k8s/argocd-application.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: whalehunting
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: http://gitea-http.gitea.svc.cluster.local:3000/admin/whalehunting.git
|
||||||
|
targetRevision: HEAD
|
||||||
|
path: k8s
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: whalehunting
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
45
k8s/deployment.yaml
Normal file
45
k8s/deployment.yaml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: whalehunting
|
||||||
|
namespace: whalehunting
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
|
app.kubernetes.io/component: web
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
|
app.kubernetes.io/component: web
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: whalehunting
|
||||||
|
image: git.kube2.tricnet.de/admin/whalehunting:latest
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
26
k8s/ingress.yaml
Normal file
26
k8s/ingress.yaml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: whalehunting
|
||||||
|
namespace: whalehunting
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: whalehunting.kube2.tricnet.de
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: whalehunting
|
||||||
|
port:
|
||||||
|
name: http
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- whalehunting.kube2.tricnet.de
|
||||||
|
secretName: whalehunting-tls
|
||||||
17
k8s/kustomization.yaml
Normal file
17
k8s/kustomization.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: whalehunting
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
|
||||||
|
commonLabels:
|
||||||
|
app.kubernetes.io/managed-by: argocd
|
||||||
|
|
||||||
|
images:
|
||||||
|
- name: git.kube2.tricnet.de/admin/whalehunting
|
||||||
|
newTag: latest
|
||||||
6
k8s/namespace.yaml
Normal file
6
k8s/namespace.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: whalehunting
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
16
k8s/service.yaml
Normal file
16
k8s/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: whalehunting
|
||||||
|
namespace: whalehunting
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: whalehunting
|
||||||
1723
package-lock.json
generated
1723
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
23
package.json
23
package.json
@@ -1,20 +1,35 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:coverage": "vitest --coverage",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:all": "npm run test && npm run test:e2e"
|
||||||
},
|
},
|
||||||
"keywords": ["game", "phaser", "adventure", "point-and-click"],
|
"keywords": [
|
||||||
|
"game",
|
||||||
|
"phaser",
|
||||||
|
"adventure",
|
||||||
|
"point-and-click"
|
||||||
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"phaser": "^3.80.1"
|
"phaser": "^3.80.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.0.0"
|
"@playwright/test": "^1.57.0",
|
||||||
|
"@vitest/ui": "^4.0.16",
|
||||||
|
"jsdom": "^27.3.0",
|
||||||
|
"vite": "^5.0.0",
|
||||||
|
"vitest": "^4.0.16"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
playwright.config.js
Normal file
32
playwright.config.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:5173',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mobile',
|
||||||
|
use: { ...devices['iPhone 12'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev',
|
||||||
|
url: 'http://localhost:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
10
src/main.js
10
src/main.js
@@ -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: {
|
||||||
@@ -21,7 +22,12 @@ const config = {
|
|||||||
},
|
},
|
||||||
scale: {
|
scale: {
|
||||||
mode: Phaser.Scale.FIT,
|
mode: Phaser.Scale.FIT,
|
||||||
autoCenter: Phaser.Scale.CENTER_BOTH
|
autoCenter: Phaser.Scale.CENTER_BOTH,
|
||||||
|
min: {
|
||||||
|
width: 320,
|
||||||
|
height: 240
|
||||||
|
},
|
||||||
|
fullscreenTarget: 'game-container'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
538
src/scenes/DeepSeaHuntingScene.js
Normal file
538
src/scenes/DeepSeaHuntingScene.js
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
@@ -19,6 +21,12 @@ export default class HuntingScene extends Phaser.Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
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
|
// Ocean background
|
||||||
this.add.rectangle(400, 300, 800, 600, 0x1e5a8e);
|
this.add.rectangle(400, 300, 800, 600, 0x1e5a8e);
|
||||||
this.createOceanWaves();
|
this.createOceanWaves();
|
||||||
@@ -38,7 +46,11 @@ export default class HuntingScene extends Phaser.Scene {
|
|||||||
this.setupOceanMovement();
|
this.setupOceanMovement();
|
||||||
|
|
||||||
// Message display
|
// Message display
|
||||||
this.showMessage('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);
|
||||||
|
|
||||||
|
// Fullscreen button
|
||||||
|
createFullscreenButton(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
@@ -89,19 +101,23 @@ export default class HuntingScene extends Phaser.Scene {
|
|||||||
this.shootHarpoon();
|
this.shootHarpoon();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tab to toggle control mode
|
// Tab to toggle control mode (desktop only)
|
||||||
this.tabKey.on('down', () => {
|
if (!this.isMobile) {
|
||||||
this.useKeyboard = !this.useKeyboard;
|
this.tabKey.on('down', () => {
|
||||||
this.controlModeText.setText(`Controls: ${this.useKeyboard ? 'KEYBOARD' : 'MOUSE'} (TAB to switch)`);
|
this.useKeyboard = !this.useKeyboard;
|
||||||
if (this.useKeyboard) {
|
this.controlModeText.setText(`Controls: ${this.useKeyboard ? 'KEYBOARD' : 'MOUSE'} (TAB to switch)`);
|
||||||
this.input.setDefaultCursor('default');
|
if (this.useKeyboard) {
|
||||||
} else {
|
this.input.setDefaultCursor('default');
|
||||||
this.input.setDefaultCursor('none');
|
} else {
|
||||||
}
|
this.input.setDefaultCursor('none');
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Start with mouse control (hide cursor)
|
// Start with mouse control (hide cursor on desktop only)
|
||||||
this.input.setDefaultCursor('none');
|
if (!this.isMobile) {
|
||||||
|
this.input.setDefaultCursor(this.useKeyboard ? 'default' : 'none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCrosshairMouse() {
|
updateCrosshairMouse() {
|
||||||
@@ -185,17 +201,19 @@ 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);
|
||||||
|
|
||||||
// Control mode indicator (fixed to screen)
|
// Control mode indicator (fixed to screen, desktop only)
|
||||||
this.controlModeText = this.add.text(400, 15, 'Controls: MOUSE (TAB to switch)', {
|
if (!this.isMobile) {
|
||||||
fontSize: '16px',
|
this.controlModeText = this.add.text(400, 15, 'Controls: MOUSE (TAB to switch)', {
|
||||||
fill: '#ffff00'
|
fontSize: fontSize(16),
|
||||||
}).setOrigin(0.5, 0);
|
fill: '#ffff00'
|
||||||
this.controlModeText.setScrollFactor(0);
|
}).setOrigin(0.5, 0);
|
||||||
|
this.controlModeText.setScrollFactor(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Return button (fixed to screen)
|
// Return button (fixed to screen)
|
||||||
const returnBtn = this.add.rectangle(750, 30, 80, 35, 0x8B0000);
|
const returnBtn = this.add.rectangle(750, 30, 80, 35, 0x8B0000);
|
||||||
@@ -204,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);
|
||||||
@@ -226,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 }
|
||||||
|
|||||||
@@ -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,12 +46,12 @@ 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);
|
||||||
|
|
||||||
// Button hover effects
|
// Button hover effects (desktop)
|
||||||
buttonBg.on('pointerover', () => {
|
buttonBg.on('pointerover', () => {
|
||||||
buttonBg.setFillStyle(0x4a90c4);
|
buttonBg.setFillStyle(0x4a90c4);
|
||||||
});
|
});
|
||||||
@@ -57,16 +60,35 @@ export default class IntroScene extends Phaser.Scene {
|
|||||||
buttonBg.setFillStyle(0x2d5f8e);
|
buttonBg.setFillStyle(0x2d5f8e);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Button click handler
|
// Touch feedback (works on both touch and mouse)
|
||||||
buttonBg.on('pointerdown', () => {
|
buttonBg.on('pointerdown', () => {
|
||||||
|
buttonBg.setFillStyle(0x4a90c4);
|
||||||
|
buttonBg.setScale(0.95);
|
||||||
|
buttonText.setScale(0.95);
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonBg.on('pointerup', () => {
|
||||||
|
buttonBg.setFillStyle(0x2d5f8e);
|
||||||
|
buttonBg.setScale(1.0);
|
||||||
|
buttonText.setScale(1.0);
|
||||||
this.startGame();
|
this.startGame();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -56,14 +63,25 @@ export default class MapScene extends Phaser.Scene {
|
|||||||
closeButton.setInteractive({ useHandCursor: true });
|
closeButton.setInteractive({ useHandCursor: true });
|
||||||
closeButton.setStrokeStyle(2, 0xffffff);
|
closeButton.setStrokeStyle(2, 0xffffff);
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
// Touch feedback (works on both touch and mouse)
|
||||||
closeButton.on('pointerdown', () => {
|
closeButton.on('pointerdown', () => {
|
||||||
|
closeButton.setScale(0.95);
|
||||||
|
closeText.setScale(0.95);
|
||||||
|
});
|
||||||
|
|
||||||
|
closeButton.on('pointerup', () => {
|
||||||
|
closeButton.setScale(1.0);
|
||||||
|
closeText.setScale(1.0);
|
||||||
this.returnToShip();
|
this.returnToShip();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fullscreen button
|
||||||
|
createFullscreenButton(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWaves() {
|
drawWaves() {
|
||||||
@@ -94,13 +112,13 @@ 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'
|
||||||
}).setOrigin(0.5);
|
}).setOrigin(0.5);
|
||||||
|
|
||||||
// Hover effect
|
// Hover effect (desktop)
|
||||||
marker.on('pointerover', () => {
|
marker.on('pointerover', () => {
|
||||||
marker.setScale(1.1);
|
marker.setScale(1.1);
|
||||||
this.showMessage(description);
|
this.showMessage(description);
|
||||||
@@ -110,7 +128,17 @@ export default class MapScene extends Phaser.Scene {
|
|||||||
marker.setScale(1);
|
marker.setScale(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
marker.on('pointerdown', onClick);
|
// Touch feedback (works on both touch and mouse)
|
||||||
|
marker.on('pointerdown', () => {
|
||||||
|
marker.setScale(0.9);
|
||||||
|
text.setScale(0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.on('pointerup', () => {
|
||||||
|
marker.setScale(1);
|
||||||
|
text.setScale(1);
|
||||||
|
onClick();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createInventoryDisplay() {
|
createInventoryDisplay() {
|
||||||
@@ -118,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'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,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'
|
||||||
@@ -189,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'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -239,8 +244,8 @@ export default class ShipDeckScene extends Phaser.Scene {
|
|||||||
wheel.setStrokeStyle(4, 0x654321);
|
wheel.setStrokeStyle(4, 0x654321);
|
||||||
|
|
||||||
// Wheel spokes
|
// Wheel spokes
|
||||||
const spokeDebug = this.add.graphics();
|
const spokes = this.add.graphics();
|
||||||
spokeDebug.lineStyle(2, 0x0000FF, 0.8);
|
spokes.lineStyle(3, 0x654321);
|
||||||
|
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
const angle = (i * 45) * Math.PI / 180;
|
const angle = (i * 45) * Math.PI / 180;
|
||||||
@@ -248,30 +253,13 @@ export default class ShipDeckScene extends Phaser.Scene {
|
|||||||
const y1 = 200 + Math.sin(angle) * 15;
|
const y1 = 200 + Math.sin(angle) * 15;
|
||||||
const x2 = 550 + Math.cos(angle) * 35;
|
const x2 = 550 + Math.cos(angle) * 35;
|
||||||
const y2 = 200 + Math.sin(angle) * 35;
|
const y2 = 200 + Math.sin(angle) * 35;
|
||||||
this.add.line(0, 0, x1, y1, x2, y2, 0x654321).setLineWidth(3);
|
spokes.lineBetween(x1, y1, x2, y2);
|
||||||
|
|
||||||
// Debug: Draw blue lines showing spoke endpoints
|
|
||||||
spokeDebug.lineBetween(x1, y1, x2, y2);
|
|
||||||
// Draw circles at endpoints
|
|
||||||
spokeDebug.strokeCircle(x1, y1, 3);
|
|
||||||
spokeDebug.strokeCircle(x2, y2, 3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wheel.on('pointerdown', () => {
|
wheel.on('pointerdown', () => {
|
||||||
// Open the map scene
|
// Open the map scene
|
||||||
this.scene.start('MapScene', { inventory: this.inventory });
|
this.scene.start('MapScene', { inventory: this.inventory });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug: Draw lines for wheel positioning
|
|
||||||
const debugLines = this.add.graphics();
|
|
||||||
debugLines.lineStyle(2, 0x00FF00, 0.8);
|
|
||||||
|
|
||||||
// Center crosshair
|
|
||||||
debugLines.lineBetween(550 - 50, 200, 550 + 50, 200); // Horizontal
|
|
||||||
debugLines.lineBetween(550, 200 - 50, 550, 200 + 50); // Vertical
|
|
||||||
|
|
||||||
// Bounding box (radius 40)
|
|
||||||
debugLines.strokeRect(550 - 40, 200 - 40, 80, 80);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createInventoryDisplay() {
|
createInventoryDisplay() {
|
||||||
@@ -280,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'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,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'
|
||||||
|
|||||||
@@ -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,12 +67,12 @@ 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);
|
||||||
|
|
||||||
// Hover effect
|
// Hover effect (desktop)
|
||||||
continueBtn.on('pointerover', () => {
|
continueBtn.on('pointerover', () => {
|
||||||
continueBtn.setFillStyle(0x4a90c4);
|
continueBtn.setFillStyle(0x4a90c4);
|
||||||
});
|
});
|
||||||
@@ -79,7 +81,18 @@ export default class TransitionScene extends Phaser.Scene {
|
|||||||
continueBtn.setFillStyle(0x2d5f8e);
|
continueBtn.setFillStyle(0x2d5f8e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Touch feedback (works on both touch and mouse)
|
||||||
continueBtn.on('pointerdown', () => {
|
continueBtn.on('pointerdown', () => {
|
||||||
|
continueBtn.setFillStyle(0x4a90c4);
|
||||||
|
continueBtn.setScale(0.95);
|
||||||
|
btnText.setScale(0.95);
|
||||||
|
});
|
||||||
|
|
||||||
|
continueBtn.on('pointerup', () => {
|
||||||
|
continueBtn.setFillStyle(0x2d5f8e);
|
||||||
|
continueBtn.setScale(1.0);
|
||||||
|
btnText.setScale(1.0);
|
||||||
|
|
||||||
// Deduct fuel (if any cost)
|
// Deduct fuel (if any cost)
|
||||||
if (this.fuelCost > 0) {
|
if (this.fuelCost > 0) {
|
||||||
this.inventory.fuel -= this.fuelCost;
|
this.inventory.fuel -= this.fuelCost;
|
||||||
@@ -88,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) {
|
||||||
@@ -109,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'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,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;
|
||||||
@@ -254,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'
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -289,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
75
src/utils/fullscreen.js
Normal 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
26
src/utils/responsive.js
Normal 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 };
|
||||||
193
tests/e2e/game-flow.spec.js
Normal file
193
tests/e2e/game-flow.spec.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('Whale Hunting Game - Main Flow', () => {
|
||||||
|
test('should load the intro scene', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Wait for Phaser to load
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Check that the game canvas exists
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
// Check page title
|
||||||
|
await expect(page).toHaveTitle(/Whale Hunting/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should start game from intro scene', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
// Click on the canvas where the "SET SAIL" button should be (center area)
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } });
|
||||||
|
|
||||||
|
// Wait for scene transition
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Game should still be running
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to map scene', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Click SET SAIL
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Click on ship's wheel (around x:550, y:200)
|
||||||
|
await canvas.click({ position: { x: 550, y: 200 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should now be in map scene
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to hunting grounds', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Start game
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Go to map
|
||||||
|
await canvas.click({ position: { x: 550, y: 200 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Click hunting grounds marker (around x:250, y:200)
|
||||||
|
await canvas.click({ position: { x: 250, y: 200 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should show transition scene
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
// Click continue button
|
||||||
|
await canvas.click({ position: { x: 400, y: 540 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should now be in hunting scene
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return to ship from map', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Start game
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Go to map
|
||||||
|
await canvas.click({ position: { x: 550, y: 200 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Click CLOSE button (top right)
|
||||||
|
await canvas.click({ position: { x: 750, y: 50 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should be back at ship deck
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Whale Hunting Scene', () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Navigate to hunting scene
|
||||||
|
await canvas.click({ position: { x: 400, y: 400 } }); // SET SAIL
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await canvas.click({ position: { x: 550, y: 200 } }); // Ship wheel
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await canvas.click({ position: { x: 250, y: 200 } }); // Hunting grounds
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
await canvas.click({ position: { x: 400, y: 540 } }); // Continue
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow shooting harpoons', async ({ page }) => {
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Click to shoot harpoon
|
||||||
|
await canvas.click({ position: { x: 400, y: 300 } });
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Game should still be running
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return to map from hunting scene', async ({ page }) => {
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Click RETURN button (top right area)
|
||||||
|
await canvas.click({ position: { x: 750, y: 30 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Should be back at map
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Mobile Compatibility', () => {
|
||||||
|
test.use({
|
||||||
|
viewport: { width: 375, height: 667 },
|
||||||
|
isMobile: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work on mobile viewport', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
// Should be scaled to fit
|
||||||
|
const canvasBox = await canvas.boundingBox();
|
||||||
|
expect(canvasBox?.width).toBeLessThanOrEqual(375);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle touch interactions', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
|
||||||
|
// Tap to start game
|
||||||
|
await canvas.tap({ position: { x: 200, y: 300 } });
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Desktop Scaling', () => {
|
||||||
|
test.use({
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should scale properly on desktop', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
const canvas = page.locator('canvas');
|
||||||
|
await expect(canvas).toBeVisible();
|
||||||
|
|
||||||
|
// Canvas should be visible and scaled
|
||||||
|
const canvasBox = await canvas.boundingBox();
|
||||||
|
expect(canvasBox).toBeTruthy();
|
||||||
|
expect(canvasBox?.width).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
179
tests/unit/game-logic.test.js
Normal file
179
tests/unit/game-logic.test.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
describe('Game Logic - Inventory Management', () => {
|
||||||
|
describe('Barrel Calculations', () => {
|
||||||
|
it('should calculate correct fuel barrel count', () => {
|
||||||
|
const fuel = 100;
|
||||||
|
const barrelCount = Math.ceil(fuel / 10);
|
||||||
|
expect(barrelCount).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate correct oil barrel count', () => {
|
||||||
|
const whaleOil = 25;
|
||||||
|
const barrelCount = Math.ceil(whaleOil / 10);
|
||||||
|
expect(barrelCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle zero barrels', () => {
|
||||||
|
expect(Math.ceil(0 / 10)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle partial barrels', () => {
|
||||||
|
expect(Math.ceil(15 / 10)).toBe(2);
|
||||||
|
expect(Math.ceil(1 / 10)).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Inventory Limits', () => {
|
||||||
|
it('should enforce fuel max capacity of 100', () => {
|
||||||
|
const maxFuel = 100;
|
||||||
|
let currentFuel = 100;
|
||||||
|
|
||||||
|
// Try to add more fuel
|
||||||
|
currentFuel = Math.min(currentFuel + 10, maxFuel);
|
||||||
|
expect(currentFuel).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enforce whale oil max capacity of 50', () => {
|
||||||
|
const maxOil = 50;
|
||||||
|
let currentOil = 50;
|
||||||
|
|
||||||
|
// Try to add more oil
|
||||||
|
currentOil = Math.min(currentOil + 5, maxOil);
|
||||||
|
expect(currentOil).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enforce penguin max capacity of 20', () => {
|
||||||
|
const maxPenguins = 20;
|
||||||
|
let currentPenguins = 20;
|
||||||
|
|
||||||
|
// Try to add more penguins
|
||||||
|
currentPenguins = Math.min(currentPenguins + 3, maxPenguins);
|
||||||
|
expect(currentPenguins).toBe(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Game Logic - Whale Hunting', () => {
|
||||||
|
describe('Fuel Consumption', () => {
|
||||||
|
it('should consume 2 fuel when processing a whale', () => {
|
||||||
|
let fuel = 100;
|
||||||
|
const fuelCost = 2;
|
||||||
|
fuel -= fuelCost;
|
||||||
|
expect(fuel).toBe(98);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not process whale without enough fuel', () => {
|
||||||
|
let fuel = 1;
|
||||||
|
const requiredFuel = 2;
|
||||||
|
const canProcess = fuel >= requiredFuel;
|
||||||
|
expect(canProcess).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process whale with exact fuel amount', () => {
|
||||||
|
let fuel = 2;
|
||||||
|
const requiredFuel = 2;
|
||||||
|
const canProcess = fuel >= requiredFuel;
|
||||||
|
expect(canProcess).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Whale Oil Rewards', () => {
|
||||||
|
it('should gain 1 oil per whale killed', () => {
|
||||||
|
let whaleOil = 0;
|
||||||
|
whaleOil += 1;
|
||||||
|
expect(whaleOil).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accumulate oil from multiple whales', () => {
|
||||||
|
let whaleOil = 5;
|
||||||
|
whaleOil += 1;
|
||||||
|
whaleOil += 1;
|
||||||
|
whaleOil += 1;
|
||||||
|
expect(whaleOil).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Whale Health System', () => {
|
||||||
|
it('should require 3 hits to kill a whale', () => {
|
||||||
|
let health = 3;
|
||||||
|
health -= 1; // Hit 1
|
||||||
|
expect(health).toBe(2);
|
||||||
|
health -= 1; // Hit 2
|
||||||
|
expect(health).toBe(1);
|
||||||
|
health -= 1; // Hit 3
|
||||||
|
expect(health).toBe(0);
|
||||||
|
expect(health <= 0).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if whale is alive', () => {
|
||||||
|
let health = 2;
|
||||||
|
expect(health > 0).toBe(true);
|
||||||
|
|
||||||
|
health = 0;
|
||||||
|
expect(health > 0).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Crosshair Bounds', () => {
|
||||||
|
it('should clamp crosshair X within 0-800', () => {
|
||||||
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||||
|
|
||||||
|
expect(clamp(-50, 0, 800)).toBe(0);
|
||||||
|
expect(clamp(900, 0, 800)).toBe(800);
|
||||||
|
expect(clamp(400, 0, 800)).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp crosshair Y within 0-600', () => {
|
||||||
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||||
|
|
||||||
|
expect(clamp(-100, 0, 600)).toBe(0);
|
||||||
|
expect(clamp(700, 0, 600)).toBe(600);
|
||||||
|
expect(clamp(300, 0, 600)).toBe(300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Game Logic - Mobile Detection', () => {
|
||||||
|
it('should detect iPhone user agent', () => {
|
||||||
|
const mobileUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)';
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(mobileUA);
|
||||||
|
expect(isMobile).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect Android user agent', () => {
|
||||||
|
const mobileUA = 'Mozilla/5.0 (Linux; Android 10; SM-G973F)';
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(mobileUA);
|
||||||
|
expect(isMobile).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not detect desktop as mobile', () => {
|
||||||
|
const desktopUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0';
|
||||||
|
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(desktopUA);
|
||||||
|
expect(isMobile).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Game Logic - Scene Transitions', () => {
|
||||||
|
it('should calculate fuel cost for destinations', () => {
|
||||||
|
const destinations = {
|
||||||
|
hunting: 0,
|
||||||
|
antarctic: 0,
|
||||||
|
port: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(destinations.hunting).toBe(0);
|
||||||
|
expect(destinations.antarctic).toBe(0);
|
||||||
|
expect(destinations.port).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate penguin discovery state', () => {
|
||||||
|
let penguins = 0;
|
||||||
|
let discovered = penguins > 0;
|
||||||
|
expect(discovered).toBe(false);
|
||||||
|
|
||||||
|
penguins = 5;
|
||||||
|
discovered = penguins > 0;
|
||||||
|
expect(discovered).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
24
vitest.config.js
Normal file
24
vitest.config.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['tests/unit/**/*.test.js'],
|
||||||
|
exclude: ['tests/e2e/**'],
|
||||||
|
coverage: {
|
||||||
|
provider: 'v8',
|
||||||
|
reporter: ['text', 'html', 'lcov'],
|
||||||
|
exclude: [
|
||||||
|
'node_modules/',
|
||||||
|
'dist/',
|
||||||
|
'*.config.js',
|
||||||
|
'tests/e2e/**'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 51204
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user