Files
zeekayandHanzo Dev faac8377e7 feat: Godot 4 web export template with a playable 3D game
Orb Runner: a complete 3D arcade game in ~150 lines of GDScript, world built in
code so there are no binary scenes to merge-conflict on. Ships the non-threaded
export (runs everywhere incl. iOS Safari); flip thread_support in
export_presets.cfg for the threaded build, which hanzo.app already serves the
COOP/COEP for when the project declares framework=godot.

The export basename is fingerprinted (orb-<sha8>.html) because every sibling
asset inherits it — that is what makes index.js/.wasm/.pck cacheable forever
instead of stale-forever.

Why this exists: static-SPA builders cannot ship a game. Proving hanzo.app can
meant finding what actually stopped it, and three things did:

  1. POST /v1/projects/:slug/deploy carries the entire build in ONE request body and
     the public edge caps a body at 4 MiB (measured: 4194304 accepted, 4198400 gets
     the opaque fasthttp "Error when parsing request"). Godot emits a 43 MB engine
     wasm; Unity 22 MB + 5.7 MB of data. The artifact lane cannot express a game at
     all, so these ship through the release lane instead: stage to the object store,
     then POST /v1/sites/:slug/publish promotes it server-side with no body.

  2. Engine loaders emit NON-fingerprinted filenames (index.js, <name>.loader.js), and
     clients/sites.CacheControlFor only marks a content-hashed name immutable. A
     redeploy therefore leaves the CDN and the browser running the OLD loader against
     the NEW wasm. That is not a visible error: emscripten just prints "still waiting
     on run dependencies" forever and the canvas stays 300x150. Cost hours to find.
     Every template here fingerprints its own asset names so the case cannot recur.

  3. A cross-origin-isolated document (COEP: require-corp) BLOCKS a plain cross-origin
     script, so the shared a.hanzo.ai runtime needs crossorigin="anonymous" on exactly
     the pages that opt into isolation — game pages.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 23:09:51 -07:00

183 lines
5.1 KiB
GDScript

extends Node3D
## Orb Runner — the whole game, built in code so the repo has no binary scenes
## to merge-conflict on. Roll the sphere, bank the orbs, don't fall off the arena.
const ARENA := 22.0
const SPEED := 14.0
const JUMP := 9.0
const GRAVITY := 26.0
const ROUND_TIME := 60.0
var player: CharacterBody3D
var cam: Camera3D
var hud: Label
var orbs: Node3D
var score := 0
var best := 0
var clock := ROUND_TIME
func _ready() -> void:
_build_world()
_build_player()
orbs = Node3D.new()
add_child(orbs)
for i in 7:
_spawn_orb()
hud = Label.new()
hud.position = Vector2(24, 18)
hud.add_theme_font_size_override("font_size", 26)
hud.add_theme_color_override("font_color", Color(0.85, 0.95, 1.0))
var layer := CanvasLayer.new()
layer.add_child(hud)
add_child(layer)
func _build_world() -> void:
var env := Environment.new()
env.background_mode = Environment.BG_SKY
var sky := Sky.new()
var mat := ProceduralSkyMaterial.new()
mat.sky_top_color = Color(0.03, 0.04, 0.10)
mat.sky_horizon_color = Color(0.10, 0.06, 0.22)
mat.ground_bottom_color = Color(0.02, 0.02, 0.05)
mat.ground_horizon_color = Color(0.08, 0.05, 0.16)
sky.sky_material = mat
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.ambient_light_energy = 0.6
var we := WorldEnvironment.new()
we.environment = env
add_child(we)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-55, -35, 0)
sun.light_energy = 1.3
sun.light_color = Color(1.0, 0.93, 0.85)
add_child(sun)
var floor_body := StaticBody3D.new()
var box := BoxMesh.new()
box.size = Vector3(ARENA * 2, 1, ARENA * 2)
var mi := MeshInstance3D.new()
mi.mesh = box
mi.material_override = _mat(Color(0.09, 0.11, 0.18), Color(0.05, 0.12, 0.25), 0.35)
var cs := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = box.size
cs.shape = shape
floor_body.add_child(mi)
floor_body.add_child(cs)
floor_body.position.y = -0.5
add_child(floor_body)
# Grid of emissive pillars: cheap depth cue + a reason to steer.
for x in range(-2, 3):
for z in range(-2, 3):
if x == 0 and z == 0:
continue
var p := StaticBody3D.new()
var pm := BoxMesh.new()
pm.size = Vector3(1.2, 3.0, 1.2)
var pmi := MeshInstance3D.new()
pmi.mesh = pm
pmi.material_override = _mat(Color(0.15, 0.08, 0.3), Color(0.35, 0.1, 0.6), 0.8)
var pcs := CollisionShape3D.new()
var ps := BoxShape3D.new()
ps.size = pm.size
pcs.shape = ps
p.add_child(pmi)
p.add_child(pcs)
p.position = Vector3(x * 8.0, 1.5, z * 8.0)
add_child(p)
func _build_player() -> void:
player = CharacterBody3D.new()
var sm := SphereMesh.new()
sm.radius = 0.7
sm.height = 1.4
var mi := MeshInstance3D.new()
mi.mesh = sm
mi.material_override = _mat(Color(0.2, 0.9, 0.8), Color(0.1, 0.9, 0.8), 1.4)
var cs := CollisionShape3D.new()
var shape := SphereShape3D.new()
shape.radius = 0.7
cs.shape = shape
player.add_child(mi)
player.add_child(cs)
player.position = Vector3(0, 2, 0)
add_child(player)
cam = Camera3D.new()
cam.fov = 65.0
add_child(cam)
_track_camera(1.0)
func _mat(albedo: Color, emit: Color, energy: float) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = albedo
m.metallic = 0.2
m.roughness = 0.45
if energy > 0.0:
m.emission_enabled = true
m.emission = emit
m.emission_energy_multiplier = energy
return m
func _spawn_orb() -> void:
var a := Area3D.new()
var sm := SphereMesh.new()
sm.radius = 0.45
sm.height = 0.9
var mi := MeshInstance3D.new()
mi.mesh = sm
mi.material_override = _mat(Color(1.0, 0.8, 0.2), Color(1.0, 0.7, 0.1), 2.2)
var cs := CollisionShape3D.new()
var shape := SphereShape3D.new()
shape.radius = 0.9
cs.shape = shape
a.add_child(mi)
a.add_child(cs)
a.position = Vector3(randf_range(-ARENA + 2, ARENA - 2), 1.2, randf_range(-ARENA + 2, ARENA - 2))
a.body_entered.connect(func(b): _collect(a, b))
orbs.add_child(a)
func _collect(orb: Area3D, body: Node) -> void:
if body != player or not is_instance_valid(orb):
return
score += 1
best = maxi(best, score)
orb.queue_free()
_spawn_orb()
func _process(delta: float) -> void:
clock -= delta
if clock <= 0.0:
_reset()
orbs.rotate_y(delta * 0.9)
_track_camera(delta * 6.0)
hud.text = "ORBS %d BEST %d %0.1fs\nWASD / arrows to roll, SPACE to hop" % [score, best, clock]
func _track_camera(w: float) -> void:
var want := player.position + Vector3(0, 11, 15)
cam.position = cam.position.lerp(want, clampf(w, 0.0, 1.0))
cam.look_at(player.position + Vector3.UP * 1.5, Vector3.UP)
func _physics_process(delta: float) -> void:
var dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
var wish := Vector3(dir.x, 0, dir.y)
player.velocity.x = move_toward(player.velocity.x, wish.x * SPEED, SPEED * 6.0 * delta)
player.velocity.z = move_toward(player.velocity.z, wish.z * SPEED, SPEED * 6.0 * delta)
if player.is_on_floor():
if Input.is_action_pressed("jump"):
player.velocity.y = JUMP
else:
player.velocity.y -= GRAVITY * delta
player.move_and_slide()
if player.position.y < -12.0:
_reset()
func _reset() -> void:
score = 0
clock = ROUND_TIME
player.position = Vector3(0, 2, 0)
player.velocity = Vector3.ZERO