diff --git a/server/main.go b/server/main.go index 23b5cf6..7e7ba26 100644 --- a/server/main.go +++ b/server/main.go @@ -22,9 +22,9 @@ import ( ) const ( - worldSize = 3600.0 - spawnX = 1200.0 - spawnY = 1550.0 + worldSize = 5400.0 + spawnX = 1800.0 + spawnY = 2325.0 playerMaxHull = 100 playerMaxSpeed = 180.0 playerReverseSpeed = -55.0 @@ -36,7 +36,7 @@ const ( shotSpeed = 520.0 shotLifetime = 900 * time.Millisecond shotDamage = 16 - shipHitRadius = 44.0 + shipHitRadius = 34.0 respawnDelay = 2200 * time.Millisecond simTick = time.Second / 30 snapshotTick = 50 * time.Millisecond @@ -229,8 +229,8 @@ func (h *hub) addFakeCaptains(count int) { id := "bot:" + strings.ToLower(strings.ReplaceAll(name, " ", "-")) state := newPlayerState(id, name, 0, 0) phase := math.Pi * 2 * float64(index) / float64(count) - state.X = clampFloat(spawnX+math.Cos(phase)*300, 90, worldSize-90) - state.Y = clampFloat(spawnY+math.Sin(phase)*300, 90, worldSize-90) + state.X = clampFloat(spawnX+math.Cos(phase)*650, 90, worldSize-90) + state.Y = clampFloat(spawnY+math.Sin(phase)*650, 90, worldSize-90) state.Angle = normalizeAngle(phase*180/math.Pi + 90) state.Bot = true h.clients[id] = &client{ @@ -329,8 +329,8 @@ func (h *hub) fireLocked(c *client, side string, now time.Time) bool { projectileID := c.state.ID + ":" + strconv.FormatUint(h.nextProjectile, 10) h.projectiles[projectileID] = projectileState{ ID: projectileID, OwnerID: c.state.ID, - X: c.state.X + math.Cos(base)*34, - Y: c.state.Y + math.Sin(base)*34, + X: c.state.X + math.Cos(base)*28, + Y: c.state.Y + math.Sin(base)*28, VX: math.Cos(angle)*shotSpeed + shipVX*0.25, VY: math.Sin(angle)*shotSpeed + shipVY*0.25, Damage: shotDamage, @@ -428,7 +428,7 @@ func (h *hub) fakeCaptainDecisionLocked(c *client, now time.Time) (inputState, s dx := candidate.state.X - c.state.X dy := candidate.state.Y - c.state.Y distance := math.Hypot(dx, dy) - if distance < bestRange { + if distance <= 1100 && distance < bestRange { target = candidate bestRange = distance } @@ -436,7 +436,7 @@ func (h *hub) fakeCaptainDecisionLocked(c *client, now time.Time) (inputState, s if target == nil { phase := float64(c.botIndex)*2.1 + float64(now.UnixMilli()%60000)/60000*math.Pi*2 - radius := 420.0 + float64(c.botIndex)*75 + radius := 720.0 + float64(c.botIndex)*110 targetX := spawnX + math.Cos(phase)*radius targetY := spawnY + math.Sin(phase)*radius bearing := bearingDegrees(c.state.X, c.state.Y, targetX, targetY) diff --git a/server/main_test.go b/server/main_test.go index f6c60fe..9432b57 100644 --- a/server/main_test.go +++ b/server/main_test.go @@ -25,7 +25,7 @@ func TestTransformSanitizers(t *testing.T) { if got := clampFloat(-5, 0, worldSize); got != 0 { t.Fatalf("clamp low=%v", got) } - if got := clampFloat(4000, 0, worldSize); got != worldSize { + if got := clampFloat(6000, 0, worldSize); got != worldSize { t.Fatalf("clamp high=%v", got) } if got := clampFloat(math.NaN(), 0, worldSize); got != 0 { @@ -190,3 +190,39 @@ func TestFakeCaptainEngagesNearbyHumanWithAuthoritativeBroadside(t *testing.T) { t.Fatal("fake captain input sequence did not advance") } } + + +func TestExpandedWorldAndSmallerShipCombatScale(t *testing.T) { + if worldSize != 5400 { + t.Fatalf("worldSize=%v", worldSize) + } + if shipHitRadius != 34 { + t.Fatalf("shipHitRadius=%v", shipHitRadius) + } +} + +func TestFakeCaptainDoesNotAggroHumanAcrossWholeWorld(t *testing.T) { + h := newHub("TEST", 1) + var bot *client + for _, candidate := range h.clients { + if candidate.state.Bot { + bot = candidate + break + } + } + human := &client{ + state: newPlayerState("human-far", "Human", 0, 0), + lastFire: map[string]time.Time{}, + } + bot.state.X, bot.state.Y = 500, 500 + human.state.X, human.state.Y = 5000, 5000 + h.clients[human.state.ID] = human + + input, fireSide := h.fakeCaptainDecisionLocked(bot, time.Unix(300, 0)) + if fireSide != "" { + t.Fatalf("far human should not trigger broadside: %s", fireSide) + } + if input.Throttle <= 0 { + t.Fatalf("bot should patrol when human is far: %+v", input) + } +} diff --git a/src/game-v4.js b/src/game-v4.js index 1ff5fae..a90ca90 100644 --- a/src/game-v4.js +++ b/src/game-v4.js @@ -71,7 +71,7 @@ import { merchantRouteEndpoint, } from './world/trade-traffic.js'; -const WORLD_SIZE = 3600; +const WORLD_SIZE = 5400; const PLAYER_MAX_SPEED = 180; const PLAYER_REVERSE_SPEED = -55; const PLAYER_ACCELERATION = 92; @@ -124,6 +124,8 @@ class GameScene extends Phaser.Scene { seed: querySeed || 'SKYWARD-001', width: WORLD_SIZE, height: WORLD_SIZE, + islandCount: 24, + portCount: 9, }); this.progression = createStarterProgression(); this.playerStats = deriveShipStats(this.progression.loadout, this.progression.installedConditions); @@ -217,9 +219,9 @@ class GameScene extends Phaser.Scene { this.player = this.physics.add.sprite(1200, 1550, 'player-airship', 0); this.player.setPosition(this.world.spawn.x, this.world.spawn.y); - this.player.setDepth(6).setCollideWorldBounds(true).setDisplaySize(66, 126); + this.player.setDepth(6).setCollideWorldBounds(true).setDisplaySize(52, 100); this.player.body.setAllowGravity(false); - this.player.body.setCircle(44, 20, 84); + this.player.body.setCircle(34, 30, 94); this.player.setFrame(0); this.createPlayerSmoke(); @@ -325,7 +327,7 @@ class GameScene extends Phaser.Scene { this.cameras.main.setBounds(0, 0, this.world.width, this.world.height); this.cameras.main.startFollow(this.player, true, 0.08, 0.08); - this.cameras.main.setZoom(1.12); + this.cameras.main.setZoom(1.0); this.createHud(); @@ -1606,13 +1608,13 @@ class GameScene extends Phaser.Scene { ); sprite .setDepth(5.5) - .setDisplaySize(60, 114) + .setDisplaySize(48, 92) .setTint(0x8cd3ff) .setAlpha(0.86); sprite.play('player-forward'); sprite.anims.timeScale = 0.72; - const label = this.add.text(sprite.x, sprite.y - 70, state.name || 'CAPTAIN', { + const label = this.add.text(sprite.x, sprite.y - 56, state.name || 'CAPTAIN', { fontFamily: 'monospace', fontSize: '10px', color: '#bfe6ff', @@ -1745,7 +1747,7 @@ class GameScene extends Phaser.Scene { sprite.x = Phaser.Math.Linear(sprite.x, remote.targetX, 0.32); sprite.y = Phaser.Math.Linear(sprite.y, remote.targetY, 0.32); sprite.angle += signedAngleDelta(remote.targetAngle, sprite.angle) * 0.32; - remote.label.setPosition(sprite.x, sprite.y - 70); + remote.label.setPosition(sprite.x, sprite.y - 56); remote.label.setVisible(window.airshipSettings?.get?.().showPlayerLabels !== false); } } diff --git a/src/world/world-generator.js b/src/world/world-generator.js index dfe8602..a089f7a 100644 --- a/src/world/world-generator.js +++ b/src/world/world-generator.js @@ -187,10 +187,10 @@ const candidatesLength = (islands, ports) => Math.max( export const generateWorld = ({ seed = 'SKYWARD-001', - width = 3600, - height = 3600, - islandCount = 16, - portCount = 7, + width = 5400, + height = 5400, + islandCount = 24, + portCount = 9, } = {}) => { const random = createSeededRandom(seed); const regions = createRegions(random, width, height); diff --git a/tests/wiring.test.mjs b/tests/wiring.test.mjs index 7804f25..aa11a40 100644 --- a/tests/wiring.test.mjs +++ b/tests/wiring.test.mjs @@ -277,8 +277,8 @@ test('player uses the four-frame north-facing sheet as a forward propulsion anim assert.match(source, /import playerAirshipUrl from '.\/assets\/player-airship-4frame\.png\?url'/); assert.match(source, /this\.load\.spritesheet\('player-airship', playerAirshipUrl, \{[\s\S]*?frameWidth: 128,[\s\S]*?frameHeight: 256/); assert.match(source, /this\.physics\.add\.sprite\(1200, 1550, 'player-airship', 0\)/); - assert.match(source, /setDisplaySize\(66, 126\)/); - assert.match(source, /this\.player\.body\.setCircle\(44, 20, 84\)/); + assert.match(source, /setDisplaySize\(52, 100\)/); + assert.match(source, /this\.player\.body\.setCircle\(34, 30, 94\)/); assert.match(source, /key: 'player-forward'/); assert.match(source, /generateFrameNumbers\('player-airship', \{ start: 0, end: 3 \}\)/); assert.match(source, /const forwardThrust = clamp\(this\.playerSpeed \/ PLAYER_MAX_SPEED, 0, 1\)/); @@ -329,6 +329,28 @@ test('environment physics adds wind, soft island collision, turbulence, and proj assert.match(source, /windSpeed: this\.environmentWind\?\.strength/); }); +test('larger shared world uses smaller player ships and wider fake-captain spacing', async () => { + const [game, world, server] = await Promise.all([ + read('src/game-v4.js'), + read('src/world/world-generator.js'), + read('server/main.go'), + ]); + + assert.match(game, /const WORLD_SIZE = 5400/); + assert.match(game, /islandCount: 24/); + assert.match(game, /portCount: 9/); + assert.match(game, /setDisplaySize\(52, 100\)/); + assert.match(game, /setCircle\(34, 30, 94\)/); + assert.match(game, /setDisplaySize\(48, 92\)/); + assert.match(game, /this\.cameras\.main\.setZoom\(1\.0\)/); + assert.match(world, /width = 5400/); + assert.match(world, /islandCount = 24/); + assert.match(server, /worldSize\s+= 5400\.0/); + assert.match(server, /shipHitRadius\s+= 34\.0/); + assert.match(server, /math\.Cos\(phase\)\*650/); + assert.match(server, /distance <= 1100/); +}); + test('authoritative multiplayer server seeds fake captains that move and fight like players', async () => { const server = await read('server/main.go');