Skip to content

Commit 942c430

Browse files
committed
explore upgrades
1 parent 562d6e3 commit 942c430

File tree

18 files changed

+918
-239
lines changed

18 files changed

+918
-239
lines changed

src/components/game/IslandExplorer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IslandIndicator } from './ui/IslandIndicator'
1414
import { Shop } from './ui/Shop'
1515
import { CompassIndicator } from './ui/CompassIndicator'
1616
import { GameOverOverlay } from './ui/GameOverOverlay'
17+
import { WinOverlay } from './ui/WinOverlay'
1718
import { StatsHUD } from './ui/StatsHUD'
1819

1920
function LoadingOverlay() {
@@ -57,6 +58,7 @@ export default function IslandExplorer() {
5758
<UpgradeOverlay />
5859
<CompleteOverlay />
5960
<GameOverOverlay />
61+
<WinOverlay />
6062
<GameHUD />
6163
<StatsHUD />
6264
<Minimap />

src/components/game/engine/GameEngine.ts

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class GameEngine {
7878
private islands: Islands
7979
private oceanRocks: OceanRocks
8080
private boundaryWalls: BoundaryWalls
81+
private directionalLight: THREE.DirectionalLight | null = null
8182
private aiShips: AIShips
8283
private cannonballs: Cannonballs
8384
private aiDebug: AIDebug
@@ -110,9 +111,16 @@ export class GameEngine {
110111
this.renderer.toneMapping = THREE.ACESFilmicToneMapping
111112
this.renderer.toneMappingExposure = 1.2
112113

114+
// PCSS disabled - was causing shader compilation errors
115+
// this.setupPCSS()
116+
113117
// Create scene
114118
this.scene = new THREE.Scene()
115119

120+
// Add atmospheric fog - matches horizon color for seamless blend
121+
// Extended far range so edges of 520-unit world aren't cut off
122+
this.scene.fog = new THREE.Fog('#B0E0E6', 200, 800)
123+
116124
// Create camera
117125
this.camera = new THREE.PerspectiveCamera(
118126
20, // Narrow FOV for isometric feel
@@ -176,12 +184,21 @@ export class GameEngine {
176184
this.initializeGameData()
177185

178186
// Add entities to scene
187+
// Render order: lower = renders first (back to front for transparency)
179188
this.scene.add(this.sky.mesh)
189+
190+
this.ocean.seaFloor.renderOrder = 0 // Sea floor first (deepest)
191+
this.scene.add(this.ocean.seaFloor)
192+
193+
this.islands.group.renderOrder = 1 // Islands above sea floor
194+
this.scene.add(this.islands.group)
195+
196+
this.ocean.mesh.renderOrder = 2 // Water surface on top
180197
this.scene.add(this.ocean.mesh)
198+
181199
this.scene.add(this.boat.group)
182200
this.scene.add(this.boat.getWakeMesh())
183201
this.scene.add(this.coins.group)
184-
this.scene.add(this.islands.group)
185202
this.scene.add(this.oceanRocks.group)
186203
this.scene.add(this.boundaryWalls.group)
187204
this.scene.add(this.aiShips.group)
@@ -208,15 +225,18 @@ export class GameEngine {
208225
const directional = new THREE.DirectionalLight('#FFF5E0', 1.3)
209226
directional.position.set(50, 100, 50)
210227
directional.castShadow = true
211-
directional.shadow.mapSize.set(2048, 2048)
212-
directional.shadow.camera.far = 250
213-
directional.shadow.camera.left = -100
214-
directional.shadow.camera.right = 100
215-
directional.shadow.camera.top = 100
216-
directional.shadow.camera.bottom = -100
217-
directional.shadow.bias = -0.0001
218-
directional.shadow.normalBias = 0.04
228+
directional.shadow.mapSize.set(4096, 4096)
229+
directional.shadow.camera.far = 150
230+
directional.shadow.camera.near = 20
231+
directional.shadow.camera.left = -40
232+
directional.shadow.camera.right = 40
233+
directional.shadow.camera.top = 40
234+
directional.shadow.camera.bottom = -40
235+
directional.shadow.bias = -0.0003
236+
directional.shadow.normalBias = 0.02
219237
this.scene.add(directional)
238+
this.scene.add(directional.target)
239+
this.directionalLight = directional
220240

221241
// Fill light
222242
const fill = new THREE.DirectionalLight('#87CEEB', 0.4)
@@ -228,6 +248,91 @@ export class GameEngine {
228248
this.scene.add(hemi)
229249
}
230250

251+
private setupPCSS(): void {
252+
// PCSS - Percentage Closer Soft Shadows
253+
// Shadows are sharp near caster, soft far from caster
254+
const pcssShaderChunk = /* glsl */ `
255+
#define LIGHT_WORLD_SIZE 0.05
256+
#define LIGHT_FRUSTUM_WIDTH 80.0
257+
#define LIGHT_SIZE_UV (LIGHT_WORLD_SIZE / LIGHT_FRUSTUM_WIDTH)
258+
#define NEAR_PLANE 20.0
259+
#define NUM_SAMPLES 17
260+
#define NUM_RINGS 11
261+
#define BLOCKER_SEARCH_NUM_SAMPLES NUM_SAMPLES
262+
263+
vec2 poissonDisk[NUM_SAMPLES];
264+
265+
void initPoissonSamples(const in vec2 randomSeed) {
266+
float ANGLE_STEP = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
267+
float INV_NUM_SAMPLES = 1.0 / float(NUM_SAMPLES);
268+
float angle = rand(randomSeed) * PI2;
269+
float radius = INV_NUM_SAMPLES;
270+
float radiusStep = radius;
271+
for (int i = 0; i < NUM_SAMPLES; i++) {
272+
poissonDisk[i] = vec2(cos(angle), sin(angle)) * pow(radius, 0.75);
273+
radius += radiusStep;
274+
angle += ANGLE_STEP;
275+
}
276+
}
277+
278+
float penumbraSize(const in float zReceiver, const in float zBlocker) {
279+
return (zReceiver - zBlocker) / zBlocker;
280+
}
281+
282+
float findBlocker(sampler2D shadowMap, const in vec2 uv, const in float zReceiver) {
283+
float searchRadius = LIGHT_SIZE_UV * (zReceiver - NEAR_PLANE) / zReceiver;
284+
float blockerDepthSum = 0.0;
285+
int numBlockers = 0;
286+
for (int i = 0; i < BLOCKER_SEARCH_NUM_SAMPLES; i++) {
287+
float shadowMapDepth = unpackRGBAToDepth(texture2D(shadowMap, uv + poissonDisk[i] * searchRadius));
288+
if (shadowMapDepth < zReceiver) {
289+
blockerDepthSum += shadowMapDepth;
290+
numBlockers++;
291+
}
292+
}
293+
if (numBlockers == 0) return -1.0;
294+
return blockerDepthSum / float(numBlockers);
295+
}
296+
297+
float PCF_Filter(sampler2D shadowMap, vec2 uv, float zReceiver, float filterRadius) {
298+
float sum = 0.0;
299+
for (int i = 0; i < NUM_SAMPLES; i++) {
300+
float depth = unpackRGBAToDepth(texture2D(shadowMap, uv + poissonDisk[i] * filterRadius));
301+
if (zReceiver <= depth) sum += 1.0;
302+
}
303+
for (int i = 0; i < NUM_SAMPLES; i++) {
304+
float depth = unpackRGBAToDepth(texture2D(shadowMap, uv + -poissonDisk[i].yx * filterRadius));
305+
if (zReceiver <= depth) sum += 1.0;
306+
}
307+
return sum / (2.0 * float(NUM_SAMPLES));
308+
}
309+
310+
float PCSS(sampler2D shadowMap, vec4 coords) {
311+
vec2 uv = coords.xy;
312+
float zReceiver = coords.z;
313+
initPoissonSamples(uv);
314+
float avgBlockerDepth = findBlocker(shadowMap, uv, zReceiver);
315+
if (avgBlockerDepth == -1.0) return 1.0;
316+
float penumbraRatio = penumbraSize(zReceiver, avgBlockerDepth);
317+
float filterRadius = penumbraRatio * LIGHT_SIZE_UV * NEAR_PLANE / zReceiver;
318+
return PCF_Filter(shadowMap, uv, zReceiver, filterRadius);
319+
}
320+
`
321+
322+
// Override Three.js shadow sampling
323+
let shader = THREE.ShaderChunk.shadowmap_pars_fragment
324+
shader = shader.replace(
325+
'#ifdef USE_SHADOWMAP',
326+
'#ifdef USE_SHADOWMAP\n' + pcssShaderChunk,
327+
)
328+
shader = shader.replace(
329+
'#if defined( SHADOWMAP_TYPE_PCF )',
330+
`return PCSS(shadowMap, shadowCoord);
331+
#if defined( SHADOWMAP_TYPE_PCF_DISABLED )`,
332+
)
333+
THREE.ShaderChunk.shadowmap_pars_fragment = shader
334+
}
335+
231336
private initializeGameData(): void {
232337
const state = useGameStore.getState()
233338

@@ -429,7 +534,7 @@ export class GameEngine {
429534
prevShowcaseUnlocked = true
430535

431536
// Expand world boundary for showcase zone
432-
state.setWorldBoundary(500)
537+
state.setWorldBoundary(520)
433538

434539
// Spawn tough outer rim AIs for the showcase zone
435540
this.aiSystem.spawn(8, true)
@@ -580,7 +685,8 @@ export class GameEngine {
580685
private update(delta: number, time: number): void {
581686
const state = useGameStore.getState()
582687

583-
// Always update ocean animation
688+
// Always update sky and ocean animation
689+
this.sky.update(time)
584690
this.ocean.update(time)
585691

586692
// Only update game systems when playing
@@ -616,6 +722,21 @@ export class GameEngine {
616722
this.boundaryWalls.update(delta)
617723
this.aiShips.update(time)
618724
this.cannonballs.update(delta)
725+
726+
// Update shadow camera to follow boat for better shadow quality
727+
if (this.directionalLight) {
728+
this.directionalLight.position.set(
729+
boatPosition[0] + 50,
730+
100,
731+
boatPosition[2] + 50,
732+
)
733+
this.directionalLight.target.position.set(
734+
boatPosition[0],
735+
0,
736+
boatPosition[2],
737+
)
738+
this.directionalLight.target.updateMatrixWorld()
739+
}
619740
}
620741
}
621742

src/components/game/engine/entities/AIDebug.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,24 @@ export class AIDebug {
2626
if (!this.group.visible || !this.aiSystem) return
2727

2828
const territories = this.aiSystem.getDebugTerritories()
29-
const leashRadius = this.aiSystem.getLeashRadius()
3029

3130
// Update or create territory circles
3231
for (const territory of territories) {
33-
const { id, homePosition, color } = territory
32+
const { id, homePosition, color, leashRadius } = territory
33+
34+
// Territory circle - recreate if radius changed or doesn't exist
35+
const existingCircle = this.circles.get(id)
36+
const needsNewCircle =
37+
!existingCircle || existingCircle.userData.leashRadius !== leashRadius
38+
39+
if (needsNewCircle) {
40+
// Remove old circle if exists
41+
if (existingCircle) {
42+
this.group.remove(existingCircle)
43+
existingCircle.geometry.dispose()
44+
;(existingCircle.material as THREE.Material).dispose()
45+
}
3446

35-
// Territory circle
36-
if (!this.circles.has(id)) {
3747
const geometry = new THREE.BufferGeometry()
3848
const segments = 64
3949
const positions = new Float32Array((segments + 1) * 3)
@@ -58,11 +68,11 @@ export class AIDebug {
5868

5969
const circle = new THREE.Line(geometry, material)
6070
circle.position.set(homePosition[0], 0, homePosition[1])
71+
circle.userData.leashRadius = leashRadius
6172
this.circles.set(id, circle)
6273
this.group.add(circle)
6374
} else {
64-
const circle = this.circles.get(id)!
65-
circle.position.set(homePosition[0], 0, homePosition[1])
75+
existingCircle.position.set(homePosition[0], 0, homePosition[1])
6676
}
6777

6878
// Home marker (small sphere)

0 commit comments

Comments
 (0)