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