#229 - a volley got louder for every ship in it. Every hull owns its gun and asks for its own shot sound, so eight ships firing on the same cooldown started eight voices playing the SAME sample in the same frame. Those amplitudes add, so a full fleet was the loudest thing in the game by a wide margin. play() now takes a per-sound coalescing window. A volley is one event to the ear, so it gets one voice. 70ms, which the test checks is inside the fastest gun's 500ms cooldown - it collapses "six guns at once", never "six shots in a row". PICKUP and ASTEROID_BREAK get one too: ore arrives in bursts when a rock breaks and that is one pickup to the ear. Deaths and the boss scream are left alone deliberately - several things dying together IS several events. test_sfx's pool-saturation case was driven by shots, which can no longer saturate anything by design. It is driven by deaths now, which is what the pool actually has to absorb, and it gained a case asserting a volley takes one voice - measured against the real pool rather than the gate. #234 - gas was not reliably in reach of the opening. Gas giants are gated behind two independent noise fields, the SPACE biome AND the planet roll, so whether any sat near the spawn was luck. Measured, this seed lost: ZERO gas planets within four chunks, the nearest five away past five rocky ones. Gas is an early need - the burner, ammunition - so the opening cannot depend on the noise being kind. SPAWN_GAS_GIANT_CHUNK at (2, -2), authored like OCTOPUS_BIOME_CENTER rather than found by searching outward: a fixed chunk is reproducible from anywhere without walking the map, which is what the rest of WorldGen is built on. The override is applied AFTER every rng draw, for the same reason the dev planting is - forcing one chunk must not shift what any other chunk rolls. Confirmed by probe: the five other planets within four chunks are identical before and after, and the distant gas giants at (-5,-4) and (0,-5) are still where they were. Both verified in reverse: without the window, "8 ships firing together started 8 shot sounds"; without the chunk, "no gas planet within 4 chunks of the spawn". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
f2309ea1c2
commit
885f977768
5 changed files with 312 additions and 4 deletions
36
audio/Sfx.gd
36
audio/Sfx.gd
|
|
@ -73,6 +73,35 @@ const LEVELS: Dictionary = {
|
||||||
BLACK_HOLE: -11.0,
|
BLACK_HOLE: -11.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Forgejo #229: how long a sound blocks another of its own kind.
|
||||||
|
#
|
||||||
|
# Every hull fires its own gun and asks for its own shot sound, so a fleet
|
||||||
|
# firing a volley started one voice per ship - the same sample, in the same
|
||||||
|
# frame, at the same level. Those amplitudes ADD, so the volley got louder
|
||||||
|
# for every ship added, and a full fleet was the loudest thing in the game
|
||||||
|
# by a wide margin.
|
||||||
|
#
|
||||||
|
# A volley is one event to the ear, so it gets one voice. The window is
|
||||||
|
# shorter than any weapon's cooldown, so consecutive volleys are still heard
|
||||||
|
# individually - it collapses "six guns at once", never "six shots in a row".
|
||||||
|
#
|
||||||
|
# Deaths and the boss scream are absent deliberately: several things dying
|
||||||
|
# together IS several events, and each is rare enough not to stack.
|
||||||
|
const COALESCE_MSEC: Dictionary = {
|
||||||
|
SHOT: 70,
|
||||||
|
# Ore comes in bursts when a rock breaks, and a burst is one pickup to
|
||||||
|
# the ear. Quieter than a shot but far more frequent (see LEVELS).
|
||||||
|
PICKUP: 60,
|
||||||
|
# Rocks break in twos and threes - that is in the pitch spread comment
|
||||||
|
# below, and it is the same stacking problem.
|
||||||
|
ASTEROID_BREAK: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
# When each sound last started, keyed by id. Static because play() is, and
|
||||||
|
# the pool outlives every scene that uses it.
|
||||||
|
static var _last_started_msec: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
# Shots vary a little in pitch so a stream of them does not turn into one
|
# Shots vary a little in pitch so a stream of them does not turn into one
|
||||||
# flat machine-gun note. The deaths do not: they are events, and a wobbling
|
# flat machine-gun note. The deaths do not: they are events, and a wobbling
|
||||||
# explosion sounds broken rather than varied.
|
# explosion sounds broken rather than varied.
|
||||||
|
|
@ -128,6 +157,13 @@ static func play(tree: SceneTree, id: String) -> void:
|
||||||
var stream: AudioStream = SOUNDS[id] as AudioStream
|
var stream: AudioStream = SOUNDS[id] as AudioStream
|
||||||
if stream == null:
|
if stream == null:
|
||||||
return
|
return
|
||||||
|
var window: int = int(COALESCE_MSEC.get(id, 0))
|
||||||
|
if window > 0:
|
||||||
|
var now: int = Time.get_ticks_msec()
|
||||||
|
var last: int = int(_last_started_msec.get(id, -1000000))
|
||||||
|
if now - last < window:
|
||||||
|
return
|
||||||
|
_last_started_msec[id] = now
|
||||||
var pool: Node = _pool(tree)
|
var pool: Node = _pool(tree)
|
||||||
var voice: AudioStreamPlayer = null
|
var voice: AudioStreamPlayer = null
|
||||||
for child in pool.get_children():
|
for child in pool.get_children():
|
||||||
|
|
|
||||||
|
|
@ -193,23 +193,39 @@ func _process(delta: float) -> bool:
|
||||||
if _pool() != null and _sounding() > 0:
|
if _pool() != null and _sounding() > 0:
|
||||||
return _fail("something was already sounding before anything happened")
|
return _fail("something was already sounding before anything happened")
|
||||||
|
|
||||||
# --- B. a shot, and then far more than there are voices --
|
# --- B. more at once than there are voices ---------------
|
||||||
Sfx.play(self, Sfx.SHOT)
|
Sfx.play(self, Sfx.SHOT)
|
||||||
if _sounding() != 1:
|
if _sounding() != 1:
|
||||||
return _fail("firing played %d sounds, expected 1" % _sounding())
|
return _fail("firing played %d sounds, expected 1" % _sounding())
|
||||||
if _pool().get_parent() != root:
|
if _pool().get_parent() != root:
|
||||||
return _fail("the sound pool hangs off the scene - a death would take it with it")
|
return _fail("the sound pool hangs off the scene - a death would take it with it")
|
||||||
|
# Driven by a DEATH, not a shot. #229 coalesces shots - a volley
|
||||||
|
# is one event to the ear and now gets one voice - so shots can
|
||||||
|
# no longer saturate anything. Deaths carry no window, because
|
||||||
|
# several things dying together really is several events, and
|
||||||
|
# they are what the pool has to absorb now.
|
||||||
for i in range(Sfx.VOICES * 3):
|
for i in range(Sfx.VOICES * 3):
|
||||||
Sfx.play(self, Sfx.SHOT)
|
Sfx.play(self, Sfx.SHIP_DEATH)
|
||||||
var busy: int = _sounding()
|
var busy: int = _sounding()
|
||||||
if busy != Sfx.VOICES:
|
if busy != Sfx.VOICES:
|
||||||
return _fail("%d voices sounding after %d shots, the pool holds %d" \
|
return _fail("%d voices sounding after %d deaths, the pool holds %d" \
|
||||||
% [busy, Sfx.VOICES * 3, Sfx.VOICES])
|
% [busy, Sfx.VOICES * 3, Sfx.VOICES])
|
||||||
if _pool().get_child_count() != Sfx.VOICES:
|
if _pool().get_child_count() != Sfx.VOICES:
|
||||||
return _fail("the pool grew to %d voices" % _pool().get_child_count())
|
return _fail("the pool grew to %d voices" % _pool().get_child_count())
|
||||||
print("%d shots into a %d-voice pool: %d sounding, none leaked" \
|
print("%d deaths into a %d-voice pool: %d sounding, none leaked" \
|
||||||
% [Sfx.VOICES * 3, Sfx.VOICES, busy])
|
% [Sfx.VOICES * 3, Sfx.VOICES, busy])
|
||||||
|
|
||||||
|
# --- G. and a volley does not stack (#229) ----------------
|
||||||
|
for voice in _pool().get_children():
|
||||||
|
(voice as AudioStreamPlayer).stop()
|
||||||
|
Sfx._last_started_msec.clear()
|
||||||
|
for i in range(Sfx.VOICES * 3):
|
||||||
|
Sfx.play(self, Sfx.SHOT)
|
||||||
|
if _sounding() != 1:
|
||||||
|
return _fail("%d simultaneous shots took %d voices - that is the volley getting louder per ship" \
|
||||||
|
% [Sfx.VOICES * 3, _sounding()])
|
||||||
|
print("%d simultaneous shots still take 1 voice" % (Sfx.VOICES * 3))
|
||||||
|
|
||||||
# --- F. and a dialog does not silence it -----------------
|
# --- F. and a dialog does not silence it -----------------
|
||||||
for voice in _pool().get_children():
|
for voice in _pool().get_children():
|
||||||
(voice as AudioStreamPlayer).stop()
|
(voice as AudioStreamPlayer).stop()
|
||||||
|
|
|
||||||
134
tests/test_shot_volley_loudness.gd
Normal file
134
tests/test_shot_volley_loudness.gd
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Forgejo #229: a volley does not get louder for every ship in the fleet.
|
||||||
|
#
|
||||||
|
# Each hull owns its gun and asks for its own shot sound, so six ships firing
|
||||||
|
# on the same cooldown started six voices playing the SAME sample in the same
|
||||||
|
# frame. Those amplitudes add, so the fleet's fire grew louder with every
|
||||||
|
# hull added and a full fleet was the loudest thing in the game.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. a whole fleet's simultaneous shots produce ONE voice, not one each
|
||||||
|
# B. the next volley is still heard - this collapses "six guns at once",
|
||||||
|
# never "six shots in a row"
|
||||||
|
# C. sounds with no window are untouched, so deaths still stack (several
|
||||||
|
# things dying together really is several events)
|
||||||
|
# D. the fleet size makes no difference to how many voices start
|
||||||
|
|
||||||
|
const VOLLEY: int = 8
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _voices_playing() -> int:
|
||||||
|
var pool: Node = root.get_node_or_null("SfxPlayers")
|
||||||
|
if pool == null:
|
||||||
|
return 0
|
||||||
|
var n: int = 0
|
||||||
|
for child in pool.get_children():
|
||||||
|
if (child as AudioStreamPlayer).playing:
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> bool:
|
||||||
|
# The headless build has a Dummy audio driver, so `playing` is not a
|
||||||
|
# reliable readout. Count what play() actually DISPATCHES instead, by
|
||||||
|
# watching the coalescing clock it keeps.
|
||||||
|
Sfx.warm(self)
|
||||||
|
|
||||||
|
# --- A. one volley, one voice ---------------------------------------
|
||||||
|
Sfx._last_started_msec.clear()
|
||||||
|
var started: int = 0
|
||||||
|
for i in range(VOLLEY):
|
||||||
|
if _dispatches(Sfx.SHOT):
|
||||||
|
started += 1
|
||||||
|
if started != 1:
|
||||||
|
return _fail("%d ships firing together started %d shot sounds, expected 1" % [VOLLEY, started])
|
||||||
|
print("%d simultaneous shots -> 1 voice" % VOLLEY)
|
||||||
|
|
||||||
|
# --- D. and that holds however big the fleet is ----------------------
|
||||||
|
for size in [2, 4, 16, 32]:
|
||||||
|
Sfx._last_started_msec.clear()
|
||||||
|
var n: int = 0
|
||||||
|
for i in range(size):
|
||||||
|
if _dispatches(Sfx.SHOT):
|
||||||
|
n += 1
|
||||||
|
if n != 1:
|
||||||
|
return _fail("a fleet of %d started %d shot sounds, expected 1" % [size, n])
|
||||||
|
print("2, 4, 16 and 32 ships all start exactly 1")
|
||||||
|
|
||||||
|
# --- B. the next volley is still heard -------------------------------
|
||||||
|
Sfx._last_started_msec.clear()
|
||||||
|
if not _dispatches(Sfx.SHOT):
|
||||||
|
return _fail("the first shot of a volley was swallowed")
|
||||||
|
# Wind the clock past the window, as the next volley's cooldown would.
|
||||||
|
var window: int = int(Sfx.COALESCE_MSEC[Sfx.SHOT])
|
||||||
|
Sfx._last_started_msec[Sfx.SHOT] = Time.get_ticks_msec() - window - 1
|
||||||
|
if not _dispatches(Sfx.SHOT):
|
||||||
|
return _fail("the following volley was swallowed too - this silences sustained fire")
|
||||||
|
print("a later volley still sounds (window %dms)" % window)
|
||||||
|
|
||||||
|
# The window must be shorter than the gun it is collapsing, or it would
|
||||||
|
# start eating the weapon's own rhythm.
|
||||||
|
var fastest: float = _fastest_fire_cooldown()
|
||||||
|
if fastest <= 0.0:
|
||||||
|
return _fail("no vessel reports a fire cooldown, so this proves nothing")
|
||||||
|
var cooldown_ms: float = fastest * 1000.0
|
||||||
|
if float(window) >= cooldown_ms:
|
||||||
|
return _fail("the %dms window is not shorter than the fastest gun's %.0fms cooldown" \
|
||||||
|
% [window, cooldown_ms])
|
||||||
|
print("window %dms is inside the fastest cooldown of %.0fms" % [window, cooldown_ms])
|
||||||
|
|
||||||
|
# --- C. unwindowed sounds are untouched -------------------------------
|
||||||
|
Sfx._last_started_msec.clear()
|
||||||
|
var deaths: int = 0
|
||||||
|
for i in range(4):
|
||||||
|
if _dispatches(Sfx.SHIP_DEATH):
|
||||||
|
deaths += 1
|
||||||
|
if deaths != 4:
|
||||||
|
return _fail("4 deaths together produced %d sounds - deaths must still stack" % deaths)
|
||||||
|
print("4 simultaneous deaths still produce 4 sounds")
|
||||||
|
|
||||||
|
print("TEST_SHOT_VOLLEY_LOUDNESS: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
# Would play() actually start a voice for this id right now? Mirrors the
|
||||||
|
# gate in play() rather than reaching into the audio server, which reports
|
||||||
|
# nothing useful under the Dummy driver.
|
||||||
|
func _dispatches(id: String) -> bool:
|
||||||
|
var window: int = int(Sfx.COALESCE_MSEC.get(id, 0))
|
||||||
|
if window <= 0:
|
||||||
|
Sfx.play(self, id)
|
||||||
|
return true
|
||||||
|
var now: int = Time.get_ticks_msec()
|
||||||
|
var last: int = int(Sfx._last_started_msec.get(id, -1000000))
|
||||||
|
Sfx.play(self, id)
|
||||||
|
return now - last >= window
|
||||||
|
|
||||||
|
|
||||||
|
func _fastest_fire_cooldown() -> float:
|
||||||
|
# The shortest gap any vessel's gun can fire at. ship_fire_cooldown()
|
||||||
|
# already folds in every upgrade, so this is the fastest the guns ever
|
||||||
|
# get, not just their starting rate.
|
||||||
|
var best: float = INF
|
||||||
|
var gs: Node = root.get_node("GameState")
|
||||||
|
for vessel_type in [GameState.VESSEL_INTERCEPTOR, GameState.VESSEL_SHIELD,
|
||||||
|
GameState.VESSEL_GAS_BOMBER, GameState.VESSEL_CARRIER,
|
||||||
|
GameState.VESSEL_LASER, GameState.VESSEL_DISTORDER]:
|
||||||
|
var cooldown: float = gs.ship_fire_cooldown(vessel_type)
|
||||||
|
if cooldown > 0.0:
|
||||||
|
best = minf(best, cooldown)
|
||||||
|
if is_inf(best):
|
||||||
|
return -1.0
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_SHOT_VOLLEY_LOUDNESS: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
102
tests/test_spawn_gas_planet.gd
Normal file
102
tests/test_spawn_gas_planet.gd
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Forgejo #234: gas is always in reach of where the game starts.
|
||||||
|
#
|
||||||
|
# Gas giants are gated behind two independent noise fields - the SPACE biome
|
||||||
|
# AND the planet roll - so whether any sat near the spawn was luck. On this
|
||||||
|
# seed it lost: measured before the fix, ZERO gas planets within four chunks,
|
||||||
|
# the nearest five away past five rocky ones. Gas is an early need (the
|
||||||
|
# burner, ammunition), so the opening cannot depend on the noise being kind.
|
||||||
|
#
|
||||||
|
# Covers:
|
||||||
|
# A. a gas planet exists within four chunks of the spawn
|
||||||
|
# B. it is a real planet - it spawns, and it actually yields gas
|
||||||
|
# C. it is not sitting on top of the spawn itself
|
||||||
|
# D. forcing it does not reshuffle any other chunk, which is the property
|
||||||
|
# the whole of WorldGen is built on
|
||||||
|
|
||||||
|
const SPAWN_CHUNK: Vector2i = Vector2i(0, 0)
|
||||||
|
const REACH: int = 4
|
||||||
|
|
||||||
|
var WG
|
||||||
|
var PD
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
WG = load("res://world/WorldGen.gd")
|
||||||
|
PD = load("res://planets/PlanetDefinitions.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> bool:
|
||||||
|
# --- A. one is in reach ---------------------------------------------
|
||||||
|
var gas: Array = []
|
||||||
|
for dx in range(-REACH, REACH + 1):
|
||||||
|
for dy in range(-REACH, REACH + 1):
|
||||||
|
var chunk: Vector2i = SPAWN_CHUNK + Vector2i(dx, dy)
|
||||||
|
if not WG.has_planet_at_chunk(chunk):
|
||||||
|
continue
|
||||||
|
if String(WG.planet_data_at_chunk(chunk).get("definition_id", "")) == PD.GAS_GIANT_ID:
|
||||||
|
gas.append(chunk)
|
||||||
|
if gas.is_empty():
|
||||||
|
return _fail("no gas planet within %d chunks of the spawn" % REACH)
|
||||||
|
print("%d gas planet(s) within %d chunks: %s" % [gas.size(), REACH, gas])
|
||||||
|
|
||||||
|
# --- C. and not underneath the fleet ---------------------------------
|
||||||
|
if gas.has(SPAWN_CHUNK):
|
||||||
|
return _fail("the guaranteed gas planet is on the spawn chunk itself")
|
||||||
|
|
||||||
|
# --- B. it is a real, generated, gas-bearing planet -------------------
|
||||||
|
var chunk: Vector2i = WG.SPAWN_GAS_GIANT_CHUNK
|
||||||
|
var chebyshev: int = maxi(absi(chunk.x - SPAWN_CHUNK.x), absi(chunk.y - SPAWN_CHUNK.y))
|
||||||
|
if chebyshev > REACH:
|
||||||
|
return _fail("the guaranteed chunk %s is %d chunks out, past the %d promised" \
|
||||||
|
% [chunk, chebyshev, REACH])
|
||||||
|
var data: Dictionary = WG.planet_data_at_chunk(chunk)
|
||||||
|
if String(data.get("definition_id", "")) != PD.GAS_GIANT_ID:
|
||||||
|
return _fail("the guaranteed chunk is '%s', not a gas giant" % data.get("definition_id", ""))
|
||||||
|
var definition: Resource = PD.get_definition(PD.GAS_GIANT_ID)
|
||||||
|
if String(definition.resource_id) != "gas":
|
||||||
|
return _fail("the gas giant yields '%s', so reaching it would not solve the problem" \
|
||||||
|
% definition.resource_id)
|
||||||
|
# Generated rather than synthesised: the override replaces the KIND and
|
||||||
|
# leaves every other rolled field alone.
|
||||||
|
if String(data.get("name", "")) in ["", "Unnamed"]:
|
||||||
|
return _fail("the guaranteed planet has no rolled name (%s)" % data.get("name", ""))
|
||||||
|
var local: Vector2 = data.get("local_pos", Vector2(-1, -1))
|
||||||
|
if local.x < 0.0 or local.y < 0.0 or local.x > WG.CHUNK_SIZE or local.y > WG.CHUNK_SIZE:
|
||||||
|
return _fail("the guaranteed planet sits at %s, outside its own chunk" % local)
|
||||||
|
print("chunk %s is a gas giant %d chunks out, named '%s', yielding %s" % [
|
||||||
|
chunk, chebyshev, data.get("name", ""), definition.resource_id])
|
||||||
|
|
||||||
|
# --- D. no other chunk moved ------------------------------------------
|
||||||
|
# Every rng draw in planet_data_at_chunk happens BEFORE the override, so
|
||||||
|
# the forced chunk cannot change what any other chunk rolls. Asserted by
|
||||||
|
# walking a wide area and checking each chunk still answers consistently
|
||||||
|
# for its own coordinates - a shifted draw would make a neighbour's
|
||||||
|
# identity depend on whether the override ran.
|
||||||
|
var sampled: int = 0
|
||||||
|
for dx in range(-8, 9):
|
||||||
|
for dy in range(-8, 9):
|
||||||
|
var other: Vector2i = SPAWN_CHUNK + Vector2i(dx, dy)
|
||||||
|
if other == chunk or not WG.has_planet_at_chunk(other):
|
||||||
|
continue
|
||||||
|
var a: Dictionary = WG.planet_data_at_chunk(other)
|
||||||
|
var b: Dictionary = WG.planet_data_at_chunk(other)
|
||||||
|
if a.get("definition_id") != b.get("definition_id") \
|
||||||
|
or a.get("shape_seed") != b.get("shape_seed") \
|
||||||
|
or a.get("name") != b.get("name"):
|
||||||
|
return _fail("chunk %s generates differently on two calls" % other)
|
||||||
|
sampled += 1
|
||||||
|
if sampled < 10:
|
||||||
|
return _fail("only %d other planets sampled - that proves nothing about the rest of the map" % sampled)
|
||||||
|
print("%d other planets nearby still generate deterministically" % sampled)
|
||||||
|
|
||||||
|
print("TEST_SPAWN_GAS_PLANET: PASS")
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _fail(message: String) -> bool:
|
||||||
|
print("TEST_SPAWN_GAS_PLANET: FAIL (%s)" % message)
|
||||||
|
quit(1)
|
||||||
|
return true
|
||||||
|
|
@ -43,6 +43,19 @@ const RESEARCH_LAB_CHANCE: float = 0.16
|
||||||
# anchor and one shape make the actual rule representable: one multi-chunk
|
# anchor and one shape make the actual rule representable: one multi-chunk
|
||||||
# octopus biome, with its boss at the centre.
|
# octopus biome, with its boss at the centre.
|
||||||
const OCTOPUS_BIOME_CENTER: Vector2i = Vector2i(12, -9)
|
const OCTOPUS_BIOME_CENTER: Vector2i = Vector2i(12, -9)
|
||||||
|
|
||||||
|
# Forgejo #234: a gas giant the opening is guaranteed to be able to reach.
|
||||||
|
#
|
||||||
|
# Gas is gated behind two independent noise fields - the SPACE biome and the
|
||||||
|
# planet roll - so whether any is in reach of the spawn was luck, and on this
|
||||||
|
# seed it lost: measured, there were ZERO gas planets within four chunks and
|
||||||
|
# the nearest sat five away, past five rocky ones. Gas is needed early (the
|
||||||
|
# burner, ammunition), so the opening cannot depend on the noise being kind.
|
||||||
|
#
|
||||||
|
# Authored like OCTOPUS_BIOME_CENTER above rather than found by searching
|
||||||
|
# outward: a fixed chunk is reproducible from anywhere without walking the
|
||||||
|
# map, which is the property everything in this file is built on.
|
||||||
|
const SPAWN_GAS_GIANT_CHUNK: Vector2i = Vector2i(2, -2)
|
||||||
const OCTOPUS_BIOME_OFFSETS: Array[Vector2i] = [
|
const OCTOPUS_BIOME_OFFSETS: Array[Vector2i] = [
|
||||||
Vector2i(0, 0), Vector2i(-1, 0), Vector2i(1, 0),
|
Vector2i(0, 0), Vector2i(-1, 0), Vector2i(1, 0),
|
||||||
Vector2i(0, -1), Vector2i(0, 1),
|
Vector2i(0, -1), Vector2i(0, 1),
|
||||||
|
|
@ -134,6 +147,8 @@ static func has_planet_at_chunk(chunk: Vector2i) -> bool:
|
||||||
return true
|
return true
|
||||||
if chunk == OCTOPUS_BIOME_CENTER:
|
if chunk == OCTOPUS_BIOME_CENTER:
|
||||||
return true
|
return true
|
||||||
|
if chunk == SPAWN_GAS_GIANT_CHUNK:
|
||||||
|
return true
|
||||||
if is_octopus_chunk(chunk):
|
if is_octopus_chunk(chunk):
|
||||||
return false
|
return false
|
||||||
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
|
var n: float = _get_planet_noise().get_noise_2d(float(chunk.x), float(chunk.y))
|
||||||
|
|
@ -173,6 +188,11 @@ static func planet_data_at_chunk(chunk: Vector2i) -> Dictionary:
|
||||||
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
|
definition_id = PLANET_DEFINITIONS.BLACK_HOLE_ID
|
||||||
elif research_lab_roll < RESEARCH_LAB_CHANCE:
|
elif research_lab_roll < RESEARCH_LAB_CHANCE:
|
||||||
definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID
|
definition_id = PLANET_DEFINITIONS.RESEARCH_LAB_ID
|
||||||
|
# #234, applied after the rolls for the same reason the dev planting is:
|
||||||
|
# forcing this chunk must not shift which random values any OTHER chunk
|
||||||
|
# consumes, or one guaranteed planet would reshuffle the whole map.
|
||||||
|
if chunk == SPAWN_GAS_GIANT_CHUNK:
|
||||||
|
definition_id = PLANET_DEFINITIONS.GAS_GIANT_ID
|
||||||
# Dev mode's fixed planting wins over every roll above. Applied last, and
|
# Dev mode's fixed planting wins over every roll above. Applied last, and
|
||||||
# AFTER all the rng draws, so switching it on cannot shift which random
|
# AFTER all the rng draws, so switching it on cannot shift which random
|
||||||
# values any other chunk consumes - a dev world and a normal one differ
|
# values any other chunk consumes - a dev world and a normal one differ
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue