365 lines
14 KiB
GDScript
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
|