115 lines
4.6 KiB
GDScript
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
|