Sharpen upgrades, mark the convoy's destination, and give the world sound
#204 the Beam Collimator widens the beam instead of adding range to one that already crosses the screen; beam width becomes a per-shot vessel stat. #205 the Energy Propulser gains three upgrades - dash recharge, dash reach, and boosted-cruise drain - which turns three constants into vessel stats. #206 three further doctrines carry the fleet from ten to twenty. #207 the prologue's World Ship gets a marker, through the scanner's own guidance arrow rather than a second one. #208 #210 #213 #214 four new synthesised sounds: the breach opening, cargo reaching the hold, a rock breaking, and a gas bomb blooming. #215 building health bars are larger, plated and framed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
parent
2239235993
commit
78836882af
33 changed files with 716 additions and 52 deletions
31
audio/Sfx.gd
31
audio/Sfx.gd
|
|
@ -28,6 +28,11 @@ const ENEMY_DEATH: String = "enemy_death"
|
|||
const BUILDING_DEATH: String = "building_death"
|
||||
const DIALOG: String = "dialog"
|
||||
const BOSS_SCREAM: String = "boss_scream"
|
||||
# #210, #213, #214, #208.
|
||||
const PICKUP: String = "pickup"
|
||||
const ASTEROID_BREAK: String = "asteroid_break"
|
||||
const GAS_BLAST: String = "gas_blast"
|
||||
const BLACK_HOLE: String = "black_hole"
|
||||
|
||||
const SOUNDS: Dictionary = {
|
||||
SHOT: "res://audio/sfx_shot.wav",
|
||||
|
|
@ -36,6 +41,10 @@ const SOUNDS: Dictionary = {
|
|||
BUILDING_DEATH: "res://audio/sfx_building_death.wav",
|
||||
DIALOG: "res://audio/sfx_dialog.wav",
|
||||
BOSS_SCREAM: "res://audio/sfx_boss_scream.wav",
|
||||
PICKUP: "res://audio/sfx_pickup.wav",
|
||||
ASTEROID_BREAK: "res://audio/sfx_asteroid_break.wav",
|
||||
GAS_BLAST: "res://audio/sfx_gas_blast.wav",
|
||||
BLACK_HOLE: "res://audio/sfx_black_hole.wav",
|
||||
}
|
||||
|
||||
# Per-sound level. A shot is fired constantly and a death happens once, so
|
||||
|
|
@ -47,12 +56,29 @@ const LEVELS: Dictionary = {
|
|||
BUILDING_DEATH: -23.0,
|
||||
DIALOG: -19.0,
|
||||
BOSS_SCREAM: -13.0,
|
||||
# Quieter than everything else by a clear margin: it is the only sound
|
||||
# here that can happen several times a second for minutes at a time, and
|
||||
# a pickup blip at shot volume would be the loudest thing in the game.
|
||||
PICKUP: -32.0,
|
||||
ASTEROID_BREAK: -24.0,
|
||||
GAS_BLAST: -21.0,
|
||||
# The prologue's one irreversible moment, and nothing else is playing
|
||||
# under it.
|
||||
BLACK_HOLE: -11.0,
|
||||
}
|
||||
|
||||
# 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
|
||||
# explosion sounds broken rather than varied.
|
||||
const SHOT_PITCH_SPREAD: float = 0.12
|
||||
# Pickups repeat harder than shots do, so they vary too - but less, because a
|
||||
# pickup is a short bright tone and a wide spread on one reads as wrong notes
|
||||
# rather than as variety. Rocks also break in twos and threes.
|
||||
const PITCH_SPREAD: Dictionary = {
|
||||
SHOT: SHOT_PITCH_SPREAD,
|
||||
PICKUP: 0.07,
|
||||
ASTEROID_BREAK: 0.09,
|
||||
}
|
||||
|
||||
|
||||
static func _pool(tree: SceneTree) -> Node:
|
||||
|
|
@ -99,7 +125,6 @@ static func play(tree: SceneTree, id: String) -> void:
|
|||
return
|
||||
voice.stream = stream
|
||||
voice.volume_db = float(LEVELS.get(id, -10.0))
|
||||
voice.pitch_scale = 1.0
|
||||
if id == SHOT:
|
||||
voice.pitch_scale = 1.0 + randf_range(-SHOT_PITCH_SPREAD, SHOT_PITCH_SPREAD)
|
||||
var spread: float = float(PITCH_SPREAD.get(id, 0.0))
|
||||
voice.pitch_scale = 1.0 if spread <= 0.0 else 1.0 + randf_range(-spread, spread)
|
||||
voice.play()
|
||||
|
|
|
|||
|
|
@ -183,6 +183,139 @@ def explosion(seconds, sub_from, sub_to, roar_seed, debris_level, drive):
|
|||
return [_saturate(v, drive) * _attack(i, 0.001) for i, v in enumerate(mixed)]
|
||||
|
||||
|
||||
def pickup(seconds=0.10):
|
||||
"""A unit of cargo arriving in the hold (#210).
|
||||
|
||||
This one fires more often than everything else in the file put together
|
||||
- every rock a drill turns out, every load a transporter lands - so it
|
||||
is built to be missable. Short, quiet, and NARROW: two clean partials a
|
||||
fifth apart with a couple of milliseconds of tick in front. No noise bed
|
||||
and no sub, because those are what make a repeated sound tiring."""
|
||||
n = int(RATE * seconds)
|
||||
tick = [value * _decay(i, n, 22.0) * 0.30
|
||||
for i, value in enumerate(_noise(n, 61, lambda t: 0.9 - 0.4 * t))]
|
||||
tone = [0.0] * n
|
||||
for hz, level in ((1180.0, 0.55), (1770.0, 0.22)):
|
||||
phase = 0.0
|
||||
for i in range(n):
|
||||
phase += 2.0 * math.pi * hz / RATE
|
||||
# Rings up and straight back down: an arrival, not a note held.
|
||||
tone[i] += math.sin(phase) * level * _decay(i, n, 3.4)
|
||||
mixed = _mix(tick, tone)
|
||||
return [_saturate(value, 1.5) * _attack(i, 0.0012)
|
||||
for i, value in enumerate(mixed)]
|
||||
|
||||
|
||||
def asteroid_break(seconds=0.62):
|
||||
"""A rock coming apart (#213).
|
||||
|
||||
Deliberately NOT the ship explosion with different numbers. Stone has no
|
||||
fuel in it: there is a hard dry crack, a short gravel roar, and a fall of
|
||||
fragments, with only enough low end to give it mass. The sub that makes a
|
||||
vessel's death read as big is exactly what would make this read as an
|
||||
explosion rather than a rock."""
|
||||
n = int(RATE * seconds)
|
||||
|
||||
crack_n = int(n * 0.14)
|
||||
crack = [value * _decay(i, crack_n, 6.5) * 0.95
|
||||
for i, value in enumerate(_noise(crack_n, 71, lambda t: 1.0 - 0.35 * t))]
|
||||
crack += [0.0] * (n - crack_n)
|
||||
|
||||
# Higher and drier than an explosion's roar - grit, not fire.
|
||||
gravel = [value * _decay(i, n, 2.4) * 0.6
|
||||
for i, value in enumerate(_noise(n, 73, lambda t: 0.55 * (1.0 - t) ** 1.1 + 0.05))]
|
||||
|
||||
thud = [value * _decay(i, n, 2.6) * 0.45
|
||||
for i, value in enumerate(_sweep(n, 190.0, 70.0, 0.6))]
|
||||
|
||||
# Fragments, denser and shorter than an explosion's debris, and pitched
|
||||
# up where small hard things live.
|
||||
rng = random.Random(77)
|
||||
shards = [0.0] * n
|
||||
for _ in range(int(seconds * 90)):
|
||||
at = rng.randint(int(n * 0.10), n - 1)
|
||||
length = rng.randint(120, 620)
|
||||
pitch = rng.uniform(420.0, 2100.0)
|
||||
for k in range(min(length, n - at)):
|
||||
shards[at + k] += (math.sin(2.0 * math.pi * pitch * k / RATE)
|
||||
* (1.0 - k / length) ** 3.5
|
||||
* 0.16 * rng.uniform(0.4, 1.0))
|
||||
|
||||
mixed = _mix(crack, gravel, thud, shards)
|
||||
return [_saturate(value, 2.4) * _attack(i, 0.001)
|
||||
for i, value in enumerate(mixed)]
|
||||
|
||||
|
||||
def gas_blast(seconds=1.05):
|
||||
"""A gas bomb blooming (#214).
|
||||
|
||||
The opposite shape to a hull exploding: almost no crack, a soft low
|
||||
whoomph that swells rather than starting at full volume, and a long
|
||||
hiss that outlives it as the cloud spreads. The swell is the point - it
|
||||
is the sound of an area filling, and it matches a bloom the player
|
||||
watches expand rather than a flash they see once."""
|
||||
n = int(RATE * seconds)
|
||||
|
||||
# Rises over the first fifth, then falls away: a bloom, not a bang.
|
||||
def bloom(i):
|
||||
t = i / n
|
||||
return min(1.0, t / 0.18) * (1.0 - t) ** 1.3
|
||||
|
||||
whoomph = [value * bloom(i) * 1.1
|
||||
for i, value in enumerate(_sweep(n, 150.0, 46.0, 0.5))]
|
||||
|
||||
body = [value * bloom(i) * 0.85
|
||||
for i, value in enumerate(_noise(n, 83, lambda t: 0.22 * (1.0 - t) + 0.02))]
|
||||
|
||||
# The cloud itself: bright, thin, and still going after the low end has
|
||||
# gone. Cut off far later than the rest, which is what "it is still out
|
||||
# there" sounds like.
|
||||
hiss = [value * min(1.0, i / (RATE * 0.05)) * _decay(i, n, 1.05) * 0.42
|
||||
for i, value in enumerate(_noise(n, 89, lambda t: 0.75 - 0.25 * t))]
|
||||
|
||||
mixed = _mix(whoomph, body, hiss)
|
||||
return [_saturate(value, 2.2) * _attack(i, 0.004)
|
||||
for i, value in enumerate(mixed)]
|
||||
|
||||
|
||||
def black_hole(seconds=2.6):
|
||||
"""The vortex opening in the prologue (#208).
|
||||
|
||||
Something tearing rather than detonating. A pair of tones sliding in
|
||||
OPPOSITE directions - one climbing to a thin scream, one falling into
|
||||
the sub - so the sound pulls apart in the middle, plus a rising roar
|
||||
that swallows both. It has to hold under the beat's shake, so it swells
|
||||
across most of its length instead of decaying from the first sample."""
|
||||
n = int(RATE * seconds)
|
||||
|
||||
def swell(i, peak=0.72, power=1.6):
|
||||
t = i / n
|
||||
if t <= peak:
|
||||
return (t / peak) ** 1.25
|
||||
return ((1.0 - t) / (1.0 - peak)) ** power
|
||||
|
||||
# Up into a scream, and down into the floor. The gap between them opening
|
||||
# is the whole idea.
|
||||
tearing_up = [value * swell(i) * 0.34
|
||||
for i, value in enumerate(_sweep(n, 240.0, 1650.0, 1.35))]
|
||||
tearing_down = [value * swell(i) * 0.95
|
||||
for i, value in enumerate(_sweep(n, 190.0, 28.0, 0.85))]
|
||||
|
||||
# The roar closes over the top of them, brightening as it goes.
|
||||
roar = [value * swell(i, 0.8, 1.3) * 0.8
|
||||
for i, value in enumerate(_noise(n, 101, lambda t: 0.05 + 0.5 * t ** 1.5))]
|
||||
|
||||
# A slow beat in the low end, so it churns instead of humming.
|
||||
churn = [0.0] * n
|
||||
for i in range(n):
|
||||
churn[i] = math.sin(2.0 * math.pi * 37.0 * i / RATE) \
|
||||
* math.sin(2.0 * math.pi * 4.3 * i / RATE) * swell(i) * 0.5
|
||||
|
||||
mixed = _mix(tearing_up, tearing_down, roar, churn)
|
||||
return [_saturate(value, 2.8) * _attack(i, 0.02)
|
||||
for i, value in enumerate(mixed)]
|
||||
|
||||
|
||||
def boss_scream(seconds=1.65):
|
||||
"""A huge synthetic creature waking: a rough falling throat tone with
|
||||
an unstable upper rasp. Long enough to carry the reveal shake, without
|
||||
|
|
@ -213,3 +346,7 @@ if __name__ == "__main__":
|
|||
_write("sfx_ship_death", explosion(1.45, 260.0, 38.0, 5, 0.16, 2.6))
|
||||
# A structure: longer, deeper, and it keeps coming down afterwards.
|
||||
_write("sfx_building_death", explosion(2.3, 150.0, 24.0, 9, 0.24, 3.1))
|
||||
_write("sfx_pickup", pickup())
|
||||
_write("sfx_asteroid_break", asteroid_break())
|
||||
_write("sfx_gas_blast", gas_blast())
|
||||
_write("sfx_black_hole", black_hole())
|
||||
|
|
|
|||
BIN
audio/sfx_asteroid_break.wav
Normal file
BIN
audio/sfx_asteroid_break.wav
Normal file
Binary file not shown.
BIN
audio/sfx_black_hole.wav
Normal file
BIN
audio/sfx_black_hole.wav
Normal file
Binary file not shown.
BIN
audio/sfx_gas_blast.wav
Normal file
BIN
audio/sfx_gas_blast.wav
Normal file
Binary file not shown.
BIN
audio/sfx_pickup.wav
Normal file
BIN
audio/sfx_pickup.wav
Normal file
Binary file not shown.
Loading…
Reference in a new issue