Files

115 lines
4.6 KiB
GDScript

class_name CombatHistory
extends Node3D
var game: Node3D
var frames: Array = []
var clip: Array = []
var playing := false
var elapsed := 0.0
var killer_id := 0
var ghosts := {}
func record(roster: Array, stamp: float = -1) -> void:
var time := Time.get_ticks_msec() / 1000.0 if stamp < 0 else stamp
var actors := {}
for row: Dictionary in roster:
actors[row.id] = {"p": row.p, "yaw": row.yaw, "pitch": row.pitch, "hp": row.hp,
"team": row.team, "weapon": row.weapon, "duck": row.duck, "life": row.get("life", 0),
"operator": row.get("operator", "steve"), "v": row.get("v", Vector3.ZERO),
"ads": row.get("ads", false), "slide": row.get("slide", 0), "grounded": row.get("grounded", true),
"reload": row.get("reload", 0), "grenades": row.get("grenades", 2), "flashes": row.get("flashes", 1),
"shot": row.get("rewards", [0, 0, 0, 0, 0])[4]}
frames.append({"time": time, "actors": actors})
while frames.size() > 45 or (frames.size() > 1 and time - frames[0].time > 2.2): frames.pop_front()
func begin(id: int) -> void:
stop()
if game.headless or id == 0 or frames.size() < 4: return
if not frames[-1].actors.has(id): return
killer_id = id
clip = frames.duplicate(true)
elapsed = 0
playing = true
func sample(time: float, source: Array = frames) -> Dictionary:
if source.is_empty(): return {}
var a: Dictionary = source[0]
var b: Dictionary = source[-1]
for frame: Dictionary in source:
if frame.time <= time: a = frame
if frame.time >= time:
b = frame
break
var weight := clampf((time - float(a.time)) / maxf(0.001, float(b.time) - float(a.time)), 0, 1)
var result: Dictionary = a.actors.duplicate(true)
for id in result:
if b.actors.has(id) and result[id].life == b.actors[id].life:
result[id].p = result[id].p.lerp(b.actors[id].p, weight)
result[id].yaw = lerp_angle(result[id].yaw, b.actors[id].yaw, weight)
result[id].pitch = lerpf(result[id].pitch, b.actors[id].pitch, weight)
return result
func advance(dt: float, camera: Camera3D) -> bool:
if not playing: return false
elapsed += dt
var duration: float = minf(2, clip[-1].time - clip[0].time)
if elapsed >= duration:
stop()
return false
var actors := sample(float(clip[-1].time) - duration + elapsed, clip)
if not actors.has(killer_id):
stop()
return false
for id in actors:
var row: Dictionary = actors[id]
if id == killer_id: continue
if not ghosts.has(id):
var model := OperatorModel.new()
add_child(model)
model.setup(row.team, row.operator)
model.weapon_mount.add_child(WeaponModels.build(row.weapon))
ghosts[id] = model
var ghost: OperatorModel = ghosts[id]
ghost.position = row.p
ghost.rotation.y = row.yaw
ghost.visible = row.hp > 0
var mounted: Node3D = ghost.weapon_mount.get_child(0)
if int(mounted.get_meta("weapon_index", -1)) != int(row.weapon):
ghost.weapon_mount.remove_child(mounted)
mounted.queue_free()
ghost.weapon_mount.add_child(WeaponModels.build(row.weapon))
ghost.animate_state(dt, {"id": id, "life": row.life, "hp": row.hp, "shot": row.shot,
"velocity": Basis(Vector3.UP, row.yaw).inverse() * row.v,
"yaw": row.yaw, "pitch": row.pitch, "crouch": row.duck, "aim": row.ads,
"grounded": row.grounded, "slide": row.slide, "reload": row.reload,
"reload_duration": float(Arsenal.DATA[row.weapon].reload), "grenades": row.grenades, "flashes": row.flashes})
var killer: Dictionary = actors[killer_id]
camera.global_position = killer.p + Vector3.UP * (1 if killer.duck else 1.65)
camera.global_rotation = Vector3(killer.pitch, killer.yaw, 0)
camera.fov = 88
camera.make_current()
return true
func stop() -> void:
playing = false
clip.clear()
for ghost in ghosts.values(): ghost.queue_free()
ghosts.clear()
func rewind_hit(source: Fighter, start: Vector3, end: Vector3, seconds: float) -> Dictionary:
var wall := game.get_world_3d().direct_space_state.intersect_ray(PhysicsRayQueryParameters3D.create(start, end, 1))
if not wall.is_empty(): end = wall.position
var actors := sample(Time.get_ticks_msec() / 1000.0 - clampf(seconds, 0, 0.2))
var best := {}
var nearest := start.distance_to(end)
for id in actors:
var row: Dictionary = actors[id]
var target: Fighter = game.players.get(id)
if target == null or target == source or target.hp <= 0 or row.hp <= 0 or target.life != row.life: continue
var bounds := AABB(row.p + Vector3(-0.32, 0, -0.32), Vector3(0.64, 1.15 if row.duck else 1.8, 0.64))
var hit: Variant = bounds.intersects_segment(start, end)
if hit is Vector3 and start.distance_to(hit) < nearest:
nearest = start.distance_to(hit)
best = {"position": hit, "collider": target, "origin": row.p}
return wall if best.is_empty() else best