extends Node3D const Voxels = preload("res://scripts/voxel_mesh.gd") var kind := "laser" var weapon_name := "" var body: Node3D var emitter: MeshInstance3D var charge_strip: MeshInstance3D var charge_fill: MeshInstance3D var charge_pips: Array[MeshInstance3D] = [] var charge_level := 0 var charge_max := 1 var live_signature := "" var muzzle_color := Color.WHITE var charge := 0.0 var powered := false var recoil := 0.0 var last_firing := false func build(data: Dictionary) -> void: for child in get_children(): remove_child(child) child.queue_free() charge_pips.clear() live_signature = "" last_firing = false recoil = 0.0 kind = canonical_kind(str(data.get("kind", "laser"))) weapon_name = str(data.get("name", "")) body = Node3D.new() add_child(body) var steel := Color("778e9b") var dark := Color("293944") var black := Color("141f28") var rim := Color("c2ccd0") var blocks := [Voxels.block(Vector3(0, 0.0, 0), Vector3(0.12, 0.035, 0.12), dark), Voxels.block(Vector3(0, -0.065, 0), Vector3(0.06, 0.115, 0.06), steel), Voxels.block(Vector3(0, -0.13, 0), Vector3(0.14, 0.015, 0.12), dark), Voxels.block(Vector3(-0.047, 0.007, 0), Vector3(0.013, 0.048, 0.086), rim)] var glow := Color("ef4940") var emitter_position := Vector3(0.171, 0.047, 0) var emitter_size := Vector3(0.008, 0.023, 0.025) var emitter_blocks: Array = [] match kind: "missile": blocks.append(Voxels.block(Vector3(0.01, 0.048, 0), Vector3(0.17, 0.075, 0.118), steel)) blocks.append(Voxels.block(Vector3(-0.06, 0.052, 0), Vector3(0.026, 0.099, 0.135), dark)) for z in [-0.028, 0.028]: blocks.append(Voxels.block(Vector3(0.098, 0.05, z), Vector3(0.025, 0.050, 0.049), rim)) blocks.append(Voxels.block(Vector3(0.112, 0.05, z), Vector3(0.006, 0.035, 0.033), black)) blocks.append(Voxels.block(Vector3(-0.04, 0.091, z), Vector3(0.10, 0.013, 0.015), Color("e3ae68"))) emitter_blocks.append(Voxels.block(Vector3(0.117, 0.05, z), Vector3(0.008, 0.027, 0.027), Color("ffbf5c"))) emitter_position = Vector3(0.117, 0.05, 0) emitter_size = Vector3(0.008, 0.011, 0.077) glow = Color("ffbf5c") "beam": blocks.append(Voxels.block(Vector3(0.0, 0.044, 0), Vector3(0.12, 0.065, 0.08), steel)) blocks.append(Voxels.block(Vector3(0.075, 0.064, 0), Vector3(0.15, 0.028, 0.10), dark)) for z in [-0.045, 0.045]: blocks.append(Voxels.block(Vector3(0.13, 0.054, z), Vector3(0.12, 0.045, 0.018), rim)) blocks.append(Voxels.block(Vector3(0.152, 0.056, z), Vector3(0.017, 0.061, 0.028), Color("a79566"))) blocks.append(Voxels.block(Vector3(-0.032, 0.066, 0), Vector3(0.038, 0.024, 0.10), rim)) emitter_position = Vector3(0.17, 0.055, 0) emitter_size = Vector3(0.011, 0.025, 0.062) glow = Color("ffdd75") "bomb": blocks.append(Voxels.block(Vector3(0, 0.06, 0), Vector3(0.12, 0.10, 0.10), steel)) blocks.append(Voxels.block(Vector3(0.075, 0.06, 0), Vector3(0.055, 0.07, 0.075), dark)) for z in [-0.06, 0.06]: blocks.append(Voxels.block(Vector3(0, 0.06, z), Vector3(0.065, 0.07, 0.018), Color("b591b7"))) blocks.append(Voxels.block(Vector3(0.039, 0.073, 0), Vector3(0.014, 0.087, 0.098), rim)) blocks.append(Voxels.block(Vector3(0.008, 0.106, 0), Vector3(0.049, 0.016, 0.049), dark)) emitter_position = Vector3(0.109, 0.062, 0) emitter_size = Vector3(0.011, 0.039, 0.040) glow = Color("cf83ff") "ion": blocks.append(Voxels.block(Vector3(0, 0.04, 0), Vector3(0.115, 0.06, 0.09), steel)) for x in [0.045, 0.075, 0.105]: blocks.append(Voxels.block(Vector3(x, 0.044, 0), Vector3(0.018, 0.075, 0.075), Color("548dba"))) blocks.append(Voxels.block(Vector3(x, 0.084, 0), Vector3(0.016, 0.006, 0.059), rim)) blocks.append(Voxels.block(Vector3(0.083, 0.044, 0), Vector3(0.12, 0.025, 0.025), dark)) blocks.append(Voxels.block(Vector3(0.146, 0.044, 0), Vector3(0.016, 0.045, 0.045), dark)) emitter_position = Vector3(0.155, 0.044, 0) emitter_size = Vector3(0.006, 0.028, 0.028) glow = Color("6ae0ff") "flak": blocks.append(Voxels.block(Vector3(0, 0.048, 0), Vector3(0.12, 0.075, 0.115), steel)) for z in [-0.038, 0.0, 0.038]: blocks.append(Voxels.block(Vector3(0.098, 0.05, z), Vector3(0.145, 0.027, 0.027), dark)) blocks.append(Voxels.block(Vector3(0.157, 0.05, z), Vector3(0.022, 0.035, 0.035), rim)) emitter_blocks.append(Voxels.block(Vector3(0.170, 0.05, z), Vector3(0.006, 0.018, 0.018), Color("f9a451"))) for z in [-0.070, 0.070]: blocks.append(Voxels.block(Vector3(-0.024, 0.041, z), Vector3(0.082, 0.057, 0.028), Color("988e72"))) emitter_position = Vector3(0.170, 0.05, 0) emitter_size = Vector3(0.006, 0.018, 0.090) glow = Color("f9a451") _: blocks.append(Voxels.block(Vector3(0, 0.041, 0), Vector3(0.13, 0.065, 0.085), steel)) var barrels := 2 if weapon_name.contains("BURST") or weapon_name.contains("DUAL") else 1 for i in range(barrels): var z := (i - (barrels - 1) * 0.5) * 0.043 blocks.append(Voxels.block(Vector3(0.10, 0.047, z), Vector3(0.14, 0.033, 0.026), dark)) blocks.append(Voxels.block(Vector3(0.165, 0.047, z), Vector3(0.02, 0.046, 0.04), rim)) emitter_blocks.append(Voxels.block(Vector3(0.176, 0.047, z), Vector3(0.007, 0.025, 0.026), glow)) blocks.append(Voxels.block(Vector3(-0.025, 0.079, 0), Vector3(0.07, 0.013, 0.055), Color("a06155"))) blocks.append(Voxels.block(Vector3(0.074, 0.046, 0), Vector3(0.019, 0.055, 0.094), steel)) if weapon_name.contains("HEAVY"): blocks.append(Voxels.block(Vector3(0.122, 0.047, 0), Vector3(0.086, 0.057, 0.071), steel)) # Cooling fins and a green charge strip on the upper armor remain readable # from a tabletop angle, rather than a subtle glow hidden inside a barrel. for x in [-0.052, -0.035, -0.018]: for z in [-0.052, 0.052]: blocks.append(Voxels.block(Vector3(x, 0.037, z), Vector3(0.008, 0.055, 0.012), rim)) for x in [-0.046, 0.049]: blocks.append(Voxels.block(Vector3(x, 0.101, 0), Vector3(0.014, 0.043, 0.034), steel)) body.add_child(Voxels.make(blocks)) muzzle_color = glow if emitter_blocks.is_empty(): emitter_blocks.append(Voxels.block(emitter_position, emitter_size, glow)) emitter = Voxels.make(emitter_blocks) var material: StandardMaterial3D = emitter.material_override material.emission_enabled = true material.emission = glow material.emission_energy_multiplier = 0.0 body.add_child(emitter) charge_strip = Voxels.make([Voxels.block(Vector3(0.004, 0.122, 0), Vector3(0.125, 0.012, 0.047), dark), Voxels.block(Vector3(0.004, 0.129, -0.024), Vector3(0.127, 0.005, 0.006), rim), Voxels.block(Vector3(0.004, 0.129, 0.024), Vector3(0.127, 0.005, 0.006), rim)]) body.add_child(charge_strip) charge_fill = _indicator(Vector3.ZERO, Vector3(1.0, 0.006, 0.033)) charge_fill.position.y = 0.131 body.add_child(charge_fill) charge_max = clampi(int(data.get("charge_max", 1)), 1, 8) if charge_max > 1: for i in range(charge_max): var pip := _indicator(Vector3.ZERO, Vector3(0.014, 0.007, 0.013)) pip.position = Vector3(-0.046 + i * 0.018, 0.134, -0.037) body.add_child(pip) charge_pips.append(pip) set_live(data) func _indicator(center: Vector3, size: Vector3) -> MeshInstance3D: var indicator: MeshInstance3D = Voxels.make([Voxels.block(center, size, Color.WHITE)]) var material: StandardMaterial3D = indicator.material_override material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED material.emission_enabled = true material.emission = Color("84dc7c") material.emission_energy_multiplier = 0.3 return indicator func set_live(data: Dictionary) -> void: powered = bool(data.get("powered", false)) charge = clampf(float(data.get("charge_fraction", float(data.get("charge", 0)) / maxf(float(data.get("cooldown", 1)), 0.001))), 0.0, 1.0) charge_level = clampi(int(data.get("charge_level", 0)), 0, charge_max) var firing := bool(data.get("firing", false)) if firing and not last_firing: recoil = 0.018 last_firing = firing var next := str(powered) + ":" + str(charge) + ":" + str(charge_level) if live_signature != next: live_signature = next var material: StandardMaterial3D = emitter.material_override material.emission_energy_multiplier = (0.15 + charge * 1.8) if powered else 0.0 emitter.visible = powered charge_fill.visible = powered and charge > 0.001 charge_fill.scale.x = maxf(0.001, charge * 0.11) charge_fill.position.x = -0.051 + charge * 0.055 var indicator_material: StandardMaterial3D = charge_fill.material_override indicator_material.albedo_color = Color("92e87d") if charge > 0.995 else Color("b8d97a") for i in range(charge_pips.size()): charge_pips[i].material_override.albedo_color = Color("92e87d") if powered and i < charge_level else Color("2a4245") func animate(delta: float) -> void: recoil = maxf(0.0, recoil - delta * 0.12) body.position.x = -recoil static func canonical_kind(native_kind: String) -> String: return {"missiles": "missile", "burst": "flak"}.get(native_kind.to_lower(), native_kind.to_lower())