Trailer tool (Movie Maker + ffmpeg) and a static download page built per release

This commit is contained in:
Paul Bailey
2026-11-05 19:31:33 +00:00
parent 608c66ea54
commit 1d0008d34b
5 changed files with 481 additions and 0 deletions

274
tools/trailer/trailer.gd Normal file
View File

@@ -0,0 +1,274 @@
extends SceneTree
## Temporary: a ~32 s test trailer, recorded with Movie Maker:
## godot --path . --fixed-fps 30 --write-movie out.avi --script tools/trailer/trailer.gd -- --no-creatures
## Six shots: dawn flyover + title, morning meadow, a hut at dusk, the wolf
## moon, a blizzard, and an end card.
const FPS := 30.0
const UITheme = preload("res://scripts/ui/theme.gd")
var world: Node3D
var cam: Camera3D
var dn: Node
var black: ColorRect
var big: Label
var sub: Label
var cap: Label
var hut_c := Vector3.ZERO
var fire_p := Vector3.ZERO
var yaw := 0.0
func _initialize() -> void:
_run.call_deferred()
func _h(x: float, z: float) -> float:
return world.terrain.sample_height(x, z)
func _g(p: Vector3, up: float = 0.0) -> Vector3:
return Vector3(p.x, _h(p.x, p.z) + up, p.z)
func _run() -> void:
var H: Node = root.get_node("Hints")
H.path = "user://trailer_hints.json" # (never your real tips)
H.load_prefs()
H.enabled = false
world = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
for _i in range(8):
await process_frame
var p: CharacterBody3D = world.player
p.set_physics_process(false)
p.survival.set_process(false)
if world.hud != null:
world.hud.visible = false
if world.has_node("HUD"):
world.get_node("HUD").visible = false
if world.wolves != null:
world.wolves.enabled = false
dn = world.day_night
dn.frozen = true
world.weather.set_process(false)
world.weather.force("clear")
cam = Camera3D.new()
cam.fov = 62
world.add_child(cam)
cam.current = true
_overlay()
await _stage()
await _shot_dawn()
await _shot_meadow()
await _shot_hut()
await _shot_wolves()
await _shot_blizzard()
await _shot_end()
DirAccess.remove_absolute(H.path)
quit(0)
# --- set dressing --------------------------------------------------------------------
func _stage() -> void:
var spawn: Vector3 = world.player.global_position
var site: Vector3 = world.build.debug_find_site(spawn + Vector3(10, 0, 6), 3.0)
yaw = 0.6
var hut: Dictionary = await world.build.debug_build_hut(site, yaw)
hut_c = site
if hut.has("pieces") and not hut.pieces.is_empty():
hut_c = hut.pieces[0].global_transform * Vector3(1, 0, 1)
var b := Basis(Vector3.UP, yaw)
fire_p = _g(hut_c + b * Vector3(0, 0, 5.5))
var fire: Node3D = world.spawn_piece(ItemDBRef().get_item("campfire"), Transform3D(Basis(), fire_p))
fire.fuel = fire.MAX_FUEL
for _i in range(5):
await physics_frame
func ItemDBRef() -> Node:
return root.get_node("ItemDB")
func _park_player(at: Vector3) -> void:
world.player.global_position = at # (the weather particles follow the player)
# --- overlay -------------------------------------------------------------------------
func _overlay() -> void:
var ui := CanvasLayer.new()
ui.layer = 50
root.add_child(ui)
var r := Control.new()
r.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
ui.add_child(r)
big = UITheme.label("", 84)
sub = UITheme.label("", 26, Color(0.95, 0.9, 0.8))
cap = UITheme.label("", 34)
for l in [big, sub, cap]:
l.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
l.add_theme_constant_override("outline_size", 6 if l == big else 3)
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.55))
l.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.5))
l.add_theme_constant_override("shadow_offset_x", 2)
l.add_theme_constant_override("shadow_offset_y", 2)
l.modulate.a = 0.0
r.add_child(l)
big.offset_top = -70
# The smaller lines sit on a soft dark box, so they read over snow and sun.
var box := StyleBoxFlat.new()
box.bg_color = Color(0.04, 0.03, 0.02, 0.55)
box.set_corner_radius_all(8)
box.content_margin_left = 22
box.content_margin_right = 22
box.content_margin_top = 8
box.content_margin_bottom = 10
for l in [sub, cap]:
l.add_theme_stylebox_override("normal", box)
l.grow_horizontal = Control.GROW_DIRECTION_BOTH
l.grow_vertical = Control.GROW_DIRECTION_BOTH
sub.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
sub.offset_top = 46
sub.offset_bottom = 46
cap.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
cap.grow_vertical = Control.GROW_DIRECTION_BEGIN
cap.offset_top = -70
cap.offset_bottom = -70
black = ColorRect.new()
black.color = Color.BLACK
black.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
r.add_child(black)
## 0..1 ramp: in over `fin` seconds from `a`, out over `fout` ending at `b`.
static func _env(t: float, a: float, b: float, fin: float = 0.6, fout: float = 0.6) -> float:
if t < a or t > b:
return 0.0
return clampf(minf((t - a) / fin, (b - t) / fout), 0.0, 1.0)
## Runs `secs` of a shot: `f(t, k)` each frame (t seconds, k = 0..1); fades
## from and to black at its ends.
func _shot(secs: float, f: Callable, caption: String = "", fade: float = 0.5) -> void:
var n := int(secs * FPS)
cap.text = caption
for i in range(n):
var t := i / FPS
f.call(t, t / secs)
black.modulate.a = 1.0 - _env(t, 0.0, secs, fade, fade)
cap.modulate.a = _env(t, 0.6, secs - 0.3, 0.5, 0.5) if not caption.is_empty() else 0.0
await process_frame
func _clock(hours: float) -> void:
dn.time = hours / 24.0
dn.apply(true)
# --- the shots -------------------------------------------------------------------------
func _shot_dawn() -> void:
big.text = "EMBERWILD"
sub.text = "Survive the island. Build a home."
var c := Vector3(0, 0, -10)
await _shot(7.5, func(t: float, k: float):
_clock(7.4 + k * 0.8)
var a := 2.2 + k * 0.55
cam.global_position = c + Vector3(cos(a), 0, sin(a)) * 115.0 + Vector3(0, 58.0 - k * 10.0, 0)
cam.look_at(c + Vector3(0, 6, 0))
_park_player(cam.global_position)
big.modulate.a = _env(t, 1.4, 6.6, 1.0, 0.8)
sub.modulate.a = _env(t, 2.4, 6.6, 1.0, 0.8), "", 0.9)
func _shot_meadow() -> void:
var from := _g(hut_c + Vector3(-30, 0, -22), 2.2)
var to := _g(hut_c + Vector3(-12, 0, -8), 1.8)
var cre: Node = world.creatures
var herd: Array = []
for i in range(3):
var at := _g(hut_c + Vector3(-14 + i * 2.5, 0, -2 + i * 1.5))
var d = cre.spawn("deer" if i < 2 else "rabbit", at)
if d != null:
herd.append(d)
await _shot(5.0, func(_t: float, k: float):
_clock(9.0)
var pos := from.lerp(to, k)
pos.y = maxf(pos.y, _h(pos.x, pos.z) + 1.8)
cam.global_position = pos
cam.look_at(_g(hut_c + Vector3(-14, 0, 0), 1.0))
_park_player(pos), "Gather. Hunt. Cook.")
for d in herd:
if is_instance_valid(d):
d.queue_free()
func _shot_hut() -> void:
var b := Basis(Vector3.UP, yaw)
var from := _g(hut_c + b * Vector3(9, 0, 13), 3.2)
var to := _g(hut_c + b * Vector3(4, 0, 9.5), 2.0)
await _shot(6.0, func(_t: float, k: float):
_clock(18.25 + k * 0.4)
var e := k * k * (3.0 - 2.0 * k)
cam.global_position = from.lerp(to, e)
cam.look_at((hut_c + fire_p) * 0.5 + Vector3(0, 1.3, 0))
_park_player(cam.global_position), "Build a home.")
func _shot_wolves() -> void:
dn.day = 7
# Three wolves, walked round the far side of the fire by hand (in the
# fire's light, in frame) rather than left to their own circling.
var pack: Array = []
for i in range(3):
var w: Node3D = load("res://scripts/creatures/tutorial_wolf.gd").new()
w.setup("wolf", world, _g(fire_p + Vector3(7, 0, 0)), 900 + i)
world.add_child(w)
w.set_physics_process(false)
w.set_process(false)
pack.append(w)
for _i in range(5):
await physics_frame
# A warm fill from the fire's side, so the circling pack reads at night.
var fill := OmniLight3D.new()
fill.light_color = Color(1.0, 0.62, 0.35)
fill.light_energy = 2.2
fill.omni_range = 18.0
fill.shadow_enabled = true
world.add_child(fill)
fill.global_position = fire_p + Vector3(0, 3.5, 0)
await _shot(5.5, func(_t: float, k: float):
_clock(21.6)
var a := yaw + 2.4 + k * 0.35
cam.global_position = _g(fire_p + Vector3(cos(a), 0, sin(a)) * 3.4, 1.7)
cam.look_at(_g(fire_p - Vector3(cos(a), 0, sin(a)) * 6.0, 0.4))
_park_player(cam.global_position)
var far := a + PI
for i in range(pack.size()):
var wa := far - 0.75 + i * 0.6 + k * 0.45
var r := 5.0 + i * 0.7
var wp := _g(fire_p + Vector3(cos(wa), 0, sin(wa)) * r)
var to := _g(fire_p + Vector3(cos(wa + 0.2), 0, sin(wa + 0.2)) * r) - wp
pack[i].global_position = wp
pack[i].rotation.y = atan2(-to.x, -to.z), "Hold out through the night.") # (creatures face -Z, as creature.gd)
fill.queue_free()
for w in pack:
if is_instance_valid(w):
w.queue_free()
dn.day = 3
func _shot_blizzard() -> void:
world.weather.force("blizzard")
var b := Basis(Vector3.UP, yaw)
for _i in range(60):
await process_frame # (let the snow build up)
await _shot(4.5, func(_t: float, k: float):
_clock(13.0)
var a := yaw - 0.4 - k * 0.25
cam.global_position = _g(hut_c + Vector3(cos(a), 0, sin(a)) * 14.0, 3.0)
cam.look_at(hut_c + Vector3(0, 1.5, 0))
_park_player(cam.global_position), "Weather the winter.")
world.weather.force("clear")
if world.weather_fx != null:
world.weather_fx.visible = false # (no leftover snow on the end card)
func _shot_end() -> void:
big.text = "EMBERWILD"
sub.text = "Self-hosted co-op survival · 0.1.0"
var b := Basis(Vector3.UP, yaw)
await _shot(6.0, func(t: float, k: float):
_clock(17.6 + k * 0.4)
cam.global_position = _g(hut_c + b * Vector3(10, 0, 9), 0.0) + Vector3(0, 6.0 + k * 10.0, 0) + b * Vector3(0, 0, k * 10.0)
cam.look_at(hut_c + Vector3(0, 1.0, 0))
_park_player(cam.global_position)
big.modulate.a = _env(t, 0.8, 5.6, 0.8, 0.6)
sub.modulate.a = _env(t, 1.6, 5.6, 0.8, 0.6), "", 0.7)