Files
train-game/scripts/car.gd
2026-08-29 11:44:31 +02:00

365 lines
14 KiB
GDScript

extends Node3D
## Ein Fahrzeug des Zugverbands: Aussenhuelle, begehbarer Innenraum,
## Sitzplätze und Interaktionspunkte. Alles in lokalen Koordinaten,
## +Z zeigt in Fahrtrichtung.
const Mat = preload("res://scripts/mat.gd")
const Deco = preload("res://scripts/deco.gd")
const Defs = preload("res://scripts/car_defs.gd")
const OUTER_HALF := 1.5
const INNER_HALF := 1.36
const FLOOR_Y := 1.10
const CEIL_Y := 3.50
const WALL_T := 0.12
const GANGWAY_HALF := 0.48
const UNDER := Color("3a3a40")
const FLOOR_COL := Color("8a6b4a")
const CEIL_COL := Color("efe9dc")
const SEAT_COL := Color("356b8f")
const SEAT_COL2 := Color("8f3550")
const METAL := Color("55575c")
const CONSOLE := Color("2f3338")
var id := ""
var kind := 0
var length := 12.0
var index := 0 ## Position im Verband, 0 = Lok
var center_s := 0.0 ## Laengsposition der Waggonmitte im Zug
var inner_z0 := 0.0 ## begehbarer Bereich, lokal
var inner_z1 := 0.0
var blockers: Array[AABB] = [] ## Hindernisse im Innenraum
var seats: Array = [] ## [{ "pos": Vector3, "yaw": float, "occupant": null }]
var hotspots: Array = [] ## [{ "pos": Vector3, "kind": String, "seat": int }]
var capacity := 0
var _cargo_stack: Array[MeshInstance3D] = []
func build(car_id: String) -> void:
id = car_id
kind = Defs.kind_of(car_id)
length = Defs.car_length(car_id)
var d: Dictionary = Defs.LOCOS[car_id] if Defs.LOCOS.has(car_id) else Defs.CARS[car_id]
var body := Color(String(d["color"]))
var trim := Color(String(d["trim"]))
capacity = int(d.get("capacity", 0))
inner_z0 = -length * 0.5 + 0.16
inner_z1 = length * 0.5 - 0.16
if kind == Defs.Kind.LOCO:
inner_z1 = length * 0.5 - 4.6 # vorne sitzt der Maschinenraum
_chassis(trim)
_floor_and_ceiling()
if kind == Defs.Kind.LOCO:
_loco_shell(body, trim)
elif kind == Defs.Kind.FREIGHT:
_freight_shell(body, trim)
else:
_coach_shell(body, trim)
_lights()
match kind:
Defs.Kind.LOCO: _fit_loco()
Defs.Kind.PASSENGER: _fit_passenger(int(d["seats"]))
Defs.Kind.DINING: _fit_dining(int(d["seats"]))
Defs.Kind.FREIGHT: _fit_freight()
## Deckenlampen - ohne sie waere vor allem der Güterwagen stockdunkel.
func _lights() -> void:
var n := maxi(1, int((inner_z1 - inner_z0) / 5.0))
for i in n:
var t := (i + 0.5) / float(n)
var z := lerpf(inner_z0, inner_z1, t)
var l := OmniLight3D.new()
l.position = Vector3(0, CEIL_Y - 0.42, z)
l.light_color = Color("ffe8c8")
l.light_energy = 2.6
l.omni_range = 8.5
l.omni_attenuation = 1.3
l.shadow_enabled = false
add_child(l)
_box(Vector3(0, CEIL_Y - 0.16, z), Vector3(0.55, 0.09, 0.55), Color("fff5e0"))
# --- Bausteine ------------------------------------------------------------
func _box(center: Vector3, size: Vector3, color: Color, block := false) -> MeshInstance3D:
var m := MeshInstance3D.new()
m.mesh = Deco.box(size)
m.material_override = Mat.flat(color)
m.position = center
add_child(m)
if block:
blockers.append(AABB(center - size * 0.5, size))
return m
func _chassis(trim: Color) -> void:
_box(Vector3(0, 0.72, 0), Vector3(2.86, 0.42, length - 0.5), UNDER)
for sz: float in [-1.0, 1.0]:
_bogie(sz * length * 0.31)
_box(Vector3(0, 0.72, sz * (length * 0.5 - 0.1)), Vector3(0.34, 0.26, 0.5), METAL)
# Puffer
for sz: float in [-1.0, 1.0]:
for sx: float in [-1.0, 1.0]:
_box(Vector3(sx * 0.75, 0.8, sz * (length * 0.5 + 0.06)),
Vector3(0.28, 0.28, 0.16), trim)
func _bogie(z: float) -> void:
_box(Vector3(0, 0.55, z), Vector3(1.9, 0.34, 2.5), UNDER)
for az: float in [-0.9, 0.9]:
for sx: float in [-1.0, 1.0]:
var w := MeshInstance3D.new()
w.mesh = Deco.cyl(0.42, 0.18, 10)
w.material_override = Mat.flat(METAL)
w.position = Vector3(sx * 0.78, 0.42, z + az)
w.rotate_z(PI * 0.5)
add_child(w)
func _floor_and_ceiling() -> void:
var iz0 := -length * 0.5 + 0.06
var iz1 := length * 0.5 - 0.06
var l := iz1 - iz0
_box(Vector3(0, FLOOR_Y - 0.06, (iz0 + iz1) * 0.5), Vector3(2.78, 0.12, l), FLOOR_COL)
if kind != Defs.Kind.LOCO:
_box(Vector3(0, CEIL_Y - 0.05, 0), Vector3(2.78, 0.1, l), CEIL_COL)
else:
_box(Vector3(0, CEIL_Y - 0.05, (inner_z0 + inner_z1) * 0.5),
Vector3(2.78, 0.1, inner_z1 - inner_z0), CEIL_COL)
## Stirnwand mit Durchgang. open = Tuer vorhanden.
func _end_wall(z: float, color: Color, open: bool) -> void:
var t := 0.14
if not open:
_box(Vector3(0, (0.94 + CEIL_Y) * 0.5, z), Vector3(2.9, CEIL_Y - 0.94, t), color, true)
return
var door_h := FLOOR_Y + 2.05
for sx: float in [-1.0, 1.0]:
var w := (1.45 - 0.5)
_box(Vector3(sx * (0.5 + w * 0.5), (0.94 + CEIL_Y) * 0.5, z),
Vector3(w, CEIL_Y - 0.94, t), color, true)
_box(Vector3(0, (door_h + CEIL_Y) * 0.5, z), Vector3(1.0, CEIL_Y - door_h, t), color)
# Faltenbalg nach hinten
_box(Vector3(0, FLOOR_Y - 0.05, z + sign(z) * 0.2), Vector3(1.1, 0.1, 0.4), UNDER)
func _roof(color: Color) -> void:
_box(Vector3(0, CEIL_Y + 0.12, 0), Vector3(3.02, 0.16, length), color)
_box(Vector3(0, CEIL_Y + 0.24, 0), Vector3(2.3, 0.1, length - 0.6), color.darkened(0.12))
## Seitenwand mit Fensterband.
func _windowed_side(color: Color, trim: Color, z0: float, z1: float) -> void:
var win_b := FLOOR_Y + 0.78
var win_t := FLOOR_Y + 1.86
var seg := z1 - z0
var mid := (z0 + z1) * 0.5
for sx: float in [-1.0, 1.0]:
var x := sx * (OUTER_HALF - WALL_T * 0.5)
_box(Vector3(x, (0.94 + win_b) * 0.5, mid), Vector3(WALL_T, win_b - 0.94, seg), color, true)
_box(Vector3(x, (win_t + CEIL_Y) * 0.5, mid), Vector3(WALL_T, CEIL_Y - win_t, seg), color)
_box(Vector3(x, win_b + 0.03, mid), Vector3(WALL_T + 0.08, 0.08, seg), trim)
var n := maxi(2, int(seg / 2.3))
var cell := seg / float(n)
for i in n + 1:
var pz := z0 + i * cell
var w := 0.52
if i == 0:
pz += 0.3
w = 0.6
elif i == n:
pz -= 0.3
w = 0.6
_box(Vector3(x, (win_b + win_t) * 0.5, pz), Vector3(WALL_T, win_t - win_b, w), color)
# Unsichtbare Wand ueber die volle Hoehe haelt Spieler drinnen
blockers.append(AABB(
Vector3(x - 0.1 if sx > 0 else x - 0.02, FLOOR_Y, z0),
Vector3(0.12, 2.2, seg)))
func _solid_side(color: Color, trim: Color, z0: float, z1: float) -> void:
var seg := z1 - z0
var mid := (z0 + z1) * 0.5
for sx: float in [-1.0, 1.0]:
var x := sx * (OUTER_HALF - WALL_T * 0.5)
_box(Vector3(x, (0.94 + CEIL_Y) * 0.5, mid), Vector3(WALL_T, CEIL_Y - 0.94, seg), color, true)
# Schiebetuer als Farbakzent
_box(Vector3(x + sx * 0.02, FLOOR_Y + 1.0, mid), Vector3(0.06, 1.9, 2.4), trim)
func _coach_shell(body: Color, trim: Color) -> void:
_windowed_side(body, trim, -length * 0.5 + 0.06, length * 0.5 - 0.06)
_end_wall(length * 0.5 - 0.07, body.darkened(0.1), true)
_end_wall(-length * 0.5 + 0.07, body.darkened(0.1), true)
_roof(trim)
func _freight_shell(body: Color, trim: Color) -> void:
_solid_side(body, trim, -length * 0.5 + 0.06, length * 0.5 - 0.06)
# Helle Innenverkleidung, damit sich die Ladung abhebt
for sx: float in [-1.0, 1.0]:
_box(Vector3(sx * (OUTER_HALF - WALL_T - 0.03), (FLOOR_Y + CEIL_Y) * 0.5, 0),
Vector3(0.05, CEIL_Y - FLOOR_Y, length - 0.4), Color("d9d2c4"))
_end_wall(length * 0.5 - 0.07, body.darkened(0.1), true)
_end_wall(-length * 0.5 + 0.07, body.darkened(0.1), true)
_roof(trim)
func _loco_shell(body: Color, trim: Color) -> void:
var cab0 := inner_z0
var cab1 := inner_z1
# Fuehrerhaus
_windowed_side(body, trim, cab0 + 0.06, cab1)
_end_wall(cab0 + 0.07, body.darkened(0.1), true)
# Frontscheibe: Wand mit grossem Ausschnitt
_box(Vector3(0, CEIL_Y - 0.25, cab1), Vector3(2.9, 0.5, 0.14), body, true)
for sx: float in [-1.0, 1.0]:
_box(Vector3(sx * 1.2, FLOOR_Y + 1.35, cab1), Vector3(0.5, 2.2, 0.14), body, true)
_box(Vector3(0, FLOOR_Y + 0.42, cab1), Vector3(2.9, 0.85, 0.14), body, true)
_box(Vector3(0, CEIL_Y + 0.12, (cab0 + cab1) * 0.5),
Vector3(3.02, 0.16, cab1 - cab0 + 0.2), trim)
# Maschinenraum vorne
var nz0 := cab1
var nz1 := length * 0.5
var nl := nz1 - nz0
_box(Vector3(0, FLOOR_Y + 0.42, nz1 + 0.08), Vector3(2.8, 0.5, 0.3), Color("d8b24a"))
_box(Vector3(0, FLOOR_Y - 0.05, (nz0 + nz1) * 0.5), Vector3(2.5, 0.3, nl), UNDER)
if id == "steam":
# Runder Kessel statt Kasten
var boiler := MeshInstance3D.new()
boiler.mesh = Deco.cyl(0.92, nl - 0.2, 12)
boiler.material_override = Mat.flat(body)
boiler.position = Vector3(0, FLOOR_Y + 0.55, (nz0 + nz1) * 0.5)
boiler.rotate_x(PI * 0.5)
add_child(boiler)
var front := MeshInstance3D.new()
front.mesh = Deco.cyl(0.99, 0.3, 12)
front.material_override = Mat.flat(trim)
front.position = Vector3(0, FLOOR_Y + 0.55, nz1 - 0.15)
front.rotate_x(PI * 0.5)
add_child(front)
var stack := MeshInstance3D.new()
stack.mesh = Deco.cyl(0.27, 1.0, 8)
stack.material_override = Mat.flat(UNDER)
stack.position = Vector3(0, FLOOR_Y + 1.95, nz1 - 0.9)
add_child(stack)
_box(Vector3(0, FLOOR_Y + 1.62, nz1 - 2.6), Vector3(0.66, 0.6, 0.66), trim)
else:
_box(Vector3(0, FLOOR_Y + 0.55, (nz0 + nz1) * 0.5), Vector3(2.6, 1.3, nl), body)
_box(Vector3(0, FLOOR_Y + 1.25, (nz0 + nz1) * 0.5), Vector3(2.2, 0.18, nl), trim)
_box(Vector3(0, FLOOR_Y + 1.5, (nz0 + nz1) * 0.5), Vector3(1.4, 0.3, nl - 1.2), UNDER)
# --- Innenausbau ----------------------------------------------------------
func _seat(pos: Vector3, yaw: float, col: Color) -> void:
var s := Node3D.new()
s.position = pos
s.rotation.y = yaw
add_child(s)
var base := MeshInstance3D.new()
base.mesh = Deco.box(Vector3(0.92, 0.4, 0.56))
base.material_override = Mat.flat(col)
base.position = Vector3(0, 0.2, 0)
s.add_child(base)
var back := MeshInstance3D.new()
back.mesh = Deco.box(Vector3(0.92, 0.62, 0.14))
back.material_override = Mat.flat(col.darkened(0.12))
back.position = Vector3(0, 0.71, -0.21)
s.add_child(back)
blockers.append(AABB(pos + Vector3(-0.46, 0, -0.32), Vector3(0.92, 0.72, 0.64)))
func _fit_passenger(seat_count: int) -> void:
var rows := int(ceil(seat_count / 2.0))
var span := inner_z1 - inner_z0 - 1.6
var gap := span / float(maxi(1, rows - 1)) if rows > 1 else 0.0
var z0 := inner_z0 + 0.8
var placed := 0
for r in rows:
var z := z0 + gap * r
for sx: float in [-1.0, 1.0]:
if placed >= seat_count:
break
var pos := Vector3(sx * 0.92, FLOOR_Y, z)
_seat(pos, 0.0, SEAT_COL if sx < 0 else SEAT_COL2)
seats.append({"pos": pos, "yaw": 0.0, "occupant": null})
hotspots.append({"pos": pos + Vector3(0, 0.9, 0), "kind": "seat", "seat": seats.size() - 1})
placed += 1
# Tisch am Fenster
if r % 2 == 1:
for sx: float in [-1.0, 1.0]:
_box(Vector3(sx * 1.0, FLOOR_Y + 0.62, z + 0.55), Vector3(0.7, 0.06, 0.5), Color("caa87a"), true)
func _fit_dining(seat_count: int) -> void:
var z0 := inner_z0 + 0.6
var z1 := inner_z1 - 0.6
# Theke links
_box(Vector3(-0.85, FLOOR_Y + 0.52, (z0 + z1) * 0.5 - 1.0),
Vector3(0.9, 1.04, (z1 - z0) * 0.55), Color("7a4f2c"), true)
_box(Vector3(-0.85, FLOOR_Y + 1.06, (z0 + z1) * 0.5 - 1.0),
Vector3(1.0, 0.06, (z1 - z0) * 0.55), Color("caa87a"))
# Zapfstelle
var tap_pos := Vector3(-0.45, FLOOR_Y + 1.1, (z0 + z1) * 0.5 - 1.0)
_box(tap_pos, Vector3(0.3, 0.5, 0.3), Color("c8b56a"))
hotspots.append({"pos": tap_pos + Vector3(0.35, 0.1, 0), "kind": "tap", "seat": -1})
# Regal
_box(Vector3(-1.15, FLOOR_Y + 1.7, (z0 + z1) * 0.5 - 1.0),
Vector3(0.35, 0.7, (z1 - z0) * 0.4), Color("8f6a45"))
# Sitzecke rechts
var placed := 0
for i in int(ceil(seat_count / 2.0)):
var z := z1 - 1.2 - i * 2.2
for sz: float in [-1.0, 1.0]:
if placed >= seat_count:
break
var pos := Vector3(0.95, FLOOR_Y, z + sz * 0.65)
_seat(pos, 0.0 if sz < 0 else PI, Color("6b8f55"))
seats.append({"pos": pos, "yaw": 0.0 if sz < 0 else PI, "occupant": null})
hotspots.append({"pos": pos + Vector3(0, 0.9, 0), "kind": "seat", "seat": seats.size() - 1})
placed += 1
_box(Vector3(0.95, FLOOR_Y + 0.62, z), Vector3(0.8, 0.06, 0.7), Color("caa87a"), true)
func _fit_freight() -> void:
var console_pos := Vector3(1.05, FLOOR_Y + 1.0, inner_z1 - 1.0)
_box(console_pos, Vector3(0.4, 0.8, 0.7), CONSOLE, true)
_box(console_pos + Vector3(-0.22, 0.25, 0), Vector3(0.06, 0.3, 0.5), Color("6ad0a0"))
hotspots.append({"pos": console_pos + Vector3(-0.4, 0.1, 0), "kind": "load", "seat": -1})
# Ladeplaetze: Stapel werden je nach Füllstand sichtbar
var slots := capacity
var per_row := 2
var rows := int(ceil(float(slots) / per_row))
var z0 := inner_z0 + 1.0
var span := (inner_z1 - 2.4) - z0
for i in slots:
var r := i / per_row
var c := i % per_row
var z: float = z0 + (span * r / float(maxi(1, rows - 1)) if rows > 1 else 0.0)
var m := _box(Vector3((-0.6 if c == 0 else 0.6), FLOOR_Y + 0.35, z),
Vector3(0.85, 0.7, 0.85), Color.WHITE)
m.visible = false
_cargo_stack.append(m)
func _fit_loco() -> void:
var z := inner_z1 - 0.75
_box(Vector3(0, FLOOR_Y + 0.5, z), Vector3(1.9, 1.0, 0.5), CONSOLE, true)
_box(Vector3(0, FLOOR_Y + 1.02, z), Vector3(2.0, 0.08, 0.7), Color("4a4f55"))
for sx: float in [-1.0, 1.0]:
_box(Vector3(sx * 0.55, FLOOR_Y + 1.14, z), Vector3(0.16, 0.3, 0.16), Color("d8b24a"))
hotspots.append({"pos": Vector3(0, FLOOR_Y + 1.2, z - 0.55), "kind": "drive", "seat": -1})
# Hocker
_box(Vector3(-0.9, FLOOR_Y + 0.25, z - 1.1), Vector3(0.5, 0.5, 0.5), Color("6b4a33"), true)
# Depot-Terminal für den Laden
var term := Vector3(1.15, FLOOR_Y + 1.1, inner_z0 + 1.2)
_box(term, Vector3(0.3, 0.9, 0.8), CONSOLE, true)
_box(term + Vector3(-0.17, 0.2, 0), Vector3(0.06, 0.45, 0.6), Color("7ab7e0"))
hotspots.append({"pos": term + Vector3(-0.4, 0.0, 0), "kind": "shop", "seat": -1})
# --- Laufzeit -------------------------------------------------------------
func show_cargo(count: int, color: Color) -> void:
for i in _cargo_stack.size():
var m := _cargo_stack[i]
m.visible = i < count
if m.visible:
m.material_override = Mat.flat(color)
func free_seat() -> int:
for i in seats.size():
if seats[i]["occupant"] == null:
return i
return -1