104 lines
6.0 KiB
GDScript
104 lines
6.0 KiB
GDScript
class_name WeaponAttachments
|
||
extends RefCounted
|
||
|
||
const SLOTS := ["optic", "muzzle", "underbarrel", "magazine", "stock"]
|
||
const LABELS := ["Visier", "Mündung", "Unterlauf", "Magazin", "Schaft"]
|
||
const OPTIONS := {
|
||
"optic": ["Standard", "Reflexvisier"], "muzzle": ["Standard", "Schalldämpfer", "Kompensator"],
|
||
"underbarrel": ["Standard", "Vordergriff"], "magazine": ["Standard", "Erweitert", "Schnellwechsel"], "stock": ["Standard", "Leichtbau"]}
|
||
const HELP := {
|
||
"optic": ["Werksvisierung", "−10 % Streuung; +5 % Zielzeit"],
|
||
"muzzle": ["Werksmündung", "Kein Schuss-Radarping; −15 % Reichweite", "−25 % Rückstoß; +8 % Zielzeit"],
|
||
"underbarrel": ["Ohne Griff", "−20 % Streuung; −3 % Lauftempo"],
|
||
"magazine": ["Standardmagazin", "+40 % Kapazität; +20 % Nachladezeit", "−20 % Nachladezeit; −15 % Kapazität"],
|
||
"stock": ["Standardschaft", "+6 % Lauftempo; +15 % Rückstoß"]}
|
||
|
||
static func clean(value: Variant) -> Dictionary:
|
||
var result := {}
|
||
if value is Dictionary:
|
||
for slot in SLOTS:
|
||
var v: Variant = value.get(slot, 0)
|
||
result[slot] = clampi(int(v), 0, OPTIONS[slot].size() - 1) if (v is int or v is float) and is_finite(float(v)) else 0
|
||
return result
|
||
|
||
static func stats(index: int, design: Dictionary) -> Dictionary:
|
||
var w: Dictionary = Arsenal.DATA[index].duplicate()
|
||
var a := clean(design.get("attachments", {}))
|
||
w.ads = float(WeaponHandling.DATA[index].ads)
|
||
if a.get("optic", 0) == 1: w.spread *= 0.9; w.ads *= 1.05
|
||
if a.get("muzzle", 0) == 1: w.range *= 0.85
|
||
if a.get("muzzle", 0) == 2: w.recoil *= 0.75; w.ads *= 1.08
|
||
if a.get("underbarrel", 0) == 1: w.spread *= 0.8; w.speed *= 0.97
|
||
if a.get("magazine", 0) == 1: w.mag = int(ceil(w.mag * 1.4)); w.reload *= 1.2
|
||
if a.get("magazine", 0) == 2: w.mag = maxi(1, int(floor(w.mag * 0.85))); w.reload *= 0.8
|
||
if index != 4 and a.get("stock", 0) == 1: w.speed *= 1.06; w.recoil *= 1.15
|
||
return w
|
||
|
||
static func apply(model: Node3D, design: Dictionary) -> void:
|
||
var old := model.get_node_or_null("Attachments")
|
||
if old: model.remove_child(old); old.queue_free()
|
||
# Pistol optics follow the animated slide.
|
||
var bolt := model.get_node("Bolt") as Node3D
|
||
var old_slide := bolt.get_node_or_null("SlideAttachments")
|
||
if old_slide: bolt.remove_child(old_slide); old_slide.queue_free()
|
||
var group := Node3D.new(); group.name = "Attachments"; model.add_child(group)
|
||
var index := int(model.get_meta("weapon_index", 0))
|
||
var family: int = Arsenal.FAMILIES[index]
|
||
var pistol := index == 4
|
||
model.set_meta("reflex_lens", null)
|
||
var a := clean(design.get("attachments", {}))
|
||
var muzzle := model.get_node_or_null("Muzzle") as Node3D
|
||
var factory_optic := model.get_node_or_null("Frame/FactoryOptic") as Node3D
|
||
if factory_optic: factory_optic.visible = a.get("optic", 0) == 0
|
||
# Never hide the heuristically classified Frame/optic structural group.
|
||
var structural := model.get_node_or_null("Frame/optic") as Node3D
|
||
if structural: structural.show()
|
||
if muzzle and a.get("muzzle", 0) > 0:
|
||
var mesh := CylinderMesh.new()
|
||
mesh.top_radius = 0.017 if pistol else 0.023; mesh.bottom_radius = mesh.top_radius
|
||
mesh.height = (0.11 if pistol else 0.18) if a.muzzle == 1 else 0.045
|
||
WeaponGeometry.part(group, mesh, muzzle.position + Vector3(0, 0, -mesh.height / 2 + 0.002), "black", Vector3(PI / 2, 0, 0))
|
||
if a.get("optic", 0) == 1:
|
||
var mount := Node3D.new(); mount.name = "Reflex"
|
||
if pistol:
|
||
var slide := Node3D.new(); slide.name = "SlideAttachments"; bolt.add_child(slide); slide.add_child(mount)
|
||
else: group.add_child(mount)
|
||
mount.position = Vector3(0, 0.058 if pistol else 0.057, 0.018 if pistol else -0.065)
|
||
var width := 0.032 if pistol else 0.048
|
||
var height := 0.029 if pistol else 0.043
|
||
WeaponGeometry.block(mount, Vector3(0, 0.004, 0), Vector3(width, 0.008, 0.042), "black")
|
||
for side in [-1, 1]:
|
||
WeaponGeometry.block(mount, Vector3(side * (width / 2 - 0.002), height / 2 + 0.008, 0), Vector3(0.004, height, 0.008), "edge")
|
||
WeaponGeometry.block(mount, Vector3(0, height + 0.008, 0), Vector3(width, 0.004, 0.008), "edge")
|
||
var lens := MeshInstance3D.new(); lens.name = "Lens"
|
||
var pane := QuadMesh.new(); pane.size = Vector2(width - 0.008, height - 0.003); lens.mesh = pane
|
||
lens.position = Vector3(0, height / 2 + 0.008, 0)
|
||
var glass := StandardMaterial3D.new()
|
||
glass.albedo_color = Color(0.55, 0.85, 0.9, 0.09)
|
||
glass.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||
glass.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||
glass.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||
lens.material_override = glass; lens.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF; mount.add_child(lens)
|
||
model.set_meta("reflex_lens", lens)
|
||
WeaponGeometry.block(mount, Vector3(0, height / 2 + 0.008, 0.001), Vector3(0.0012, 0.0012, 0.0005), "red")
|
||
if a.get("underbarrel", 0) == 1:
|
||
var z := -0.115 if pistol else (-0.22 if family == 1 else (-0.34 if index == 7 else -0.3))
|
||
var top := -0.037 if pistol else (-0.073 if family == 1 else (-0.060 if family == 2 else -0.032))
|
||
var length := 0.045 if pistol else 0.10
|
||
WeaponGeometry.block(group, Vector3(0, top - length / 2 + 0.003, z), Vector3(0.028 if pistol else 0.039, length, 0.04), "polymer")
|
||
var mag := model.get_node_or_null("Magazine") as Node3D
|
||
if mag:
|
||
mag.scale = Vector3.ONE
|
||
for child in mag.get_children():
|
||
if not child is MeshInstance3D: continue
|
||
var scale_y := 1.3 if a.get("magazine", 0) == 1 else (0.85 if a.get("magazine", 0) == 2 else 1.0)
|
||
# Scale around the seating surface, not around the weapon origin.
|
||
var top := -0.177 if pistol else -0.035
|
||
if not child.has_meta("seated_transform"): child.set_meta("seated_transform",child.transform)
|
||
var seated: Transform3D = child.get_meta("seated_transform")
|
||
child.transform = Transform3D(Basis.IDENTITY.scaled(Vector3(1,scale_y,1)),Vector3(0,top*(1-scale_y),0))*seated
|
||
var stock := model.get_node_or_null("Frame/stock") as Node3D
|
||
if stock: stock.scale = Vector3.ONE
|
||
if a.get("stock", 0) == 1 and not pistol:
|
||
WeaponGeometry.block(group, Vector3(0, -0.015 if family == 1 else -0.043, 0.315 if family == 1 else 0.352), Vector3(0.06, 0.07, 0.008), "edge")
|