vscript-timer/timer.nut

610 lines
16 KiB
Text

MASK_ATTACK_TRACE <-
Constants.FContents.CONTENTS_SOLID |
Constants.FContents.CONTENTS_PLAYERCLIP |
Constants.FContents.CONTENTS_WINDOW |
Constants.FContents.CONTENTS_GRATE;
::Log <- function(msg)
{
local time = Time();
printl(format("[timer][%.2f] | %s", time, msg));
}
::PrintToPlayer <- function(ply, msg)
{
ClientPrint(ply, Constants.EHudNotify.HUD_PRINTTALK, msg);
}
::GetPlayerName <- function(ply)
{
return NetProps.GetPropString(ply, "m_szNetname");
}
::GetZoneFileName <- function()
{
return "zones_" + GetMapName();
}
class Zone
{
// aabb
min = Vector();
max = Vector();
// bottom vertices
b1 = Vector();
b2 = Vector();
b3 = Vector();
b4 = Vector();
// top vertices
t1 = Vector();
t2 = Vector();
t3 = Vector();
t4 = Vector();
constructor(min, max)
{
this.min = min;
this.max = max;
this.b1 = Vector(min.x, min.y, min.z);
this.b2 = Vector(min.x, max.y, min.z);
this.b3 = Vector(max.x, max.y, min.z);
this.b4 = Vector(max.x, min.y, min.z);
this.t1 = Vector(min.x, min.y, max.z);
this.t2 = Vector(min.x, max.y, max.z);
this.t3 = Vector(max.x, max.y, max.z);
this.t4 = Vector(max.x, min.y, max.z);
}
function Draw(duration)
{
// bottom
DebugDrawLine(this.b1, this.b2, 0, 255, 0, true, duration);
DebugDrawLine(this.b2, this.b3, 0, 255, 0, true, duration);
DebugDrawLine(this.b3, this.b4, 0, 255, 0, true, duration);
DebugDrawLine(this.b4, this.b1, 0, 255, 0, true, duration);
// top
DebugDrawLine(this.t1, this.t2, 0, 255, 0, true, duration);
DebugDrawLine(this.t2, this.t3, 0, 255, 0, true, duration);
DebugDrawLine(this.t3, this.t4, 0, 255, 0, true, duration);
DebugDrawLine(this.t4, this.t1, 0, 255, 0, true, duration);
// sides
DebugDrawLine(this.b1, this.t1, 0, 255, 0, true, duration);
DebugDrawLine(this.b2, this.t2, 0, 255, 0, true, duration);
DebugDrawLine(this.b3, this.t3, 0, 255, 0, true, duration);
DebugDrawLine(this.b4, this.t4, 0, 255, 0, true, duration);
}
// Returns true if any part of entity bounding box intersects with zone
function Clips(entity)
{
local entOrigin = entity.GetOrigin();
local entMin = entOrigin + entity.GetBoundingMins();
local entMax = entOrigin + entity.GetBoundingMaxs();
local xClip = entMin.x < this.max.x && entMax.x > this.min.x;
local yClip = entMin.y < this.max.y && entMax.y > this.min.y;
local zClip = entMin.z < this.max.z && entMax.z > this.min.z;
return xClip && yClip && zClip;
}
function Print()
{
Log(format("min: [%f, %f, %f]", this.min.x, this.min.y, this.min.z));
Log(format("max: [%f, %f, %f]", this.max.x, this.max.y, this.max.z));
}
// Serialize zone into space separated floats.
function ToString()
{
return format(
"%f %f %f %f %f %f",
this.min.x, this.min.y, this.min.z,
this.max.x, this.max.y, this.max.z
);
}
// Deserialize zone from a string of 6 floats.
function FromString(str)
{
local parts = split(str, " ");
if (parts.len() != 6)
{
return null;
}
local min = Vector();
min.x = parts[0].tofloat();
min.y = parts[1].tofloat();
min.z = parts[2].tofloat();
local max = Vector();
max.x = parts[3].tofloat();
max.y = parts[4].tofloat();
max.z = parts[5].tofloat();
return Zone(min, max);
}
}
class ZoneBuilder
{
points = [];
constructor()
{
}
// Pick a point for the new zone.
// Should be called 3 times in total.
function PickPoint(point)
{
local num_points = this.points.len();
if (num_points == 1)
{
// Only care about horizontal area
point.z = this.points[0].z;
}
else if (num_points == 2)
{
// Only care about the height
point.x = this.points[0].x;
point.y = this.points[0].y;
}
Log(format("Adding point [%f, %f, %f]", point.x, point.y, point.z));
this.points.append(point);
if (num_points == 0)
{
PrintToPlayer(null, "Pick second horizontal point");
return null;
}
else if (num_points == 1)
{
PrintToPlayer(null, "Pick the height");
return null;
}
// Create the zone
local min = Vector(this.points[0].x, this.points[0].y, this.points[0].z);
local max = Vector(this.points[0].x, this.points[0].y, this.points[0].z);
for (local i = 1; i < 3; i++)
{
if (this.points[i].x < min.x) min.x = points[i].x;
if (this.points[i].y < min.y) min.y = points[i].y;
if (this.points[i].z < min.z) min.z = points[i].z;
if (this.points[i].x > max.x) max.x = points[i].x;
if (this.points[i].y > max.y) max.y = points[i].y;
if (this.points[i].z > max.z) max.z = points[i].z;
}
this.points = [];
return Zone(min, max);
}
// Draw the zone we are building,
// including potential point we are going to pick.
function Draw(point)
{
local num_points = this.points.len();
if (num_points <= 0)
{
return;
}
local min = Vector(this.points[0].x, this.points[0].y, this.points[0].z);
local max = Vector(this.points[0].x, this.points[0].y, this.points[0].z);
// Adding a horizontal point?
if (num_points == 1)
{
if (point.x < min.x) min.x = point.x;
if (point.y < min.y) min.y = point.y;
if (point.x > max.x) max.x = point.x;
if (point.y > max.y) max.y = point.y;
}
// Adding height?
else if (num_points == 2)
{
if (this.points[1].x < min.x) min.x = this.points[1].x;
if (this.points[1].y < min.y) min.y = this.points[1].y;
if (this.points[1].x > max.x) max.x = this.points[1].x;
if (this.points[1].y > max.y) max.y = this.points[1].y;
if (point.z < min.z) min.z = point.z;
if (point.z > max.z) max.z = point.z;
}
local temp_zone = Zone(min, max);
temp_zone.Draw(0.1);
}
function IsBuilding()
{
return this.points.len() > 0;
}
function GetPlayerAimPoint(ply)
{
local vecStart = ply.EyePosition();
local vecEnd = vecStart + ply.EyeAngles().Forward() * 2048.0;
local tr =
{
"start": vecStart,
"end": vecEnd,
"mask": MASK_ATTACK_TRACE,
"ignore": ply
};
if (TraceLineEx(tr))
{
return tr["endpos"];
}
return vecEnd;
}
}
class PlayerState
{
prev_zone_index = -1;
start_tick = -1;
constructor()
{
}
}
class Timer
{
zones = [];
players = {};
tick_count = 0;
constructor()
{
}
function AddZone(zone)
{
zone.Draw(5.0);
zones.append(zone);
if (zones.len() > 1)
{
PrintToPlayer(null, "Zone " + (this.zones.len() - 1) + " added");
}
else
{
PrintToPlayer(null, "Starting zone added");
}
}
function ClearZones()
{
this.zones = [];
PrintToPlayer(null, "Zones cleared");
}
function RemoveZone()
{
local zone_num = this.zones.len();
if (zone_num <= 0)
{
PrintToPlayer(null, "No zones to remove");
return;
}
this.zones.pop();
if (zone_num == 1)
{
PrintToPlayer(null, "Removed the start zone");
}
else
{
PrintToPlayer(null, "Removed zone " + zone_num);
}
}
function SaveZones()
{
if (this.zones.len() <= 0)
{
PrintToPlayer(null, "No zones to save");
return;
}
local zone_str = "";
for (local i = 0; i < this.zones.len(); i++)
{
zone_str += this.zones[i].ToString();
if (i < this.zones.len() - 1)
{
zone_str += "\n";
}
}
local file_name = GetZoneFileName();
StringToFile(file_name, zone_str);
PrintToPlayer(null, format("Saved %d zones to '%s'", this.zones.len(), file_name));
}
function LoadZones()
{
local file_name = GetZoneFileName();
local zone_str = FileToString(file_name);
if (zone_str == null)
{
PrintToPlayer(null, format("Could not load zones from '%s'", file_name));
Log("No file");
return;
}
local lines = split(zone_str, "\n");
Log(format("Found %d lines in %s", lines.len(), file_name));
this.zones = [];
foreach (line in lines)
{
local zone = Zone.FromString(line);
if (zone == null)
{
PrintToPlayer(null, format("Could not load zones from '%s'", file_name));
Log(format("Malformed line: %s", line));
this.zones = [];
return;
}
this.zones.append(zone);
}
PrintToPlayer(null, format("Loaded %d zones from '%s'", this.zones.len(), file_name));
DrawZones();
}
function Tick()
{
this.tick_count++;
local ply = null;
while (ply = Entities.FindByClassname(ply, "player"))
{
local entindex = ply.entindex();
// Dead or spectator?
if (!ply.IsAlive() ||
(ply.GetTeam() != Constants.ETFTeam.TF_TEAM_RED &&
ply.GetTeam() != Constants.ETFTeam.TF_TEAM_BLUE)
)
{
if (entindex in this.players)
{
delete this.players[entindex];
}
continue;
}
// New player to track?
if (!(entindex in this.players))
{
this.players[entindex] <- PlayerState();
}
if (this.zones.len() <= 0)
{
continue;
}
local prev_zone_index = this.players[entindex].prev_zone_index;
local start_tick = this.players[entindex].start_tick;
if (prev_zone_index < 0)
{
// Not yet in start zone, only check if we are entering it.
if (this.zones[0].Clips(ply))
{
this.players[entindex].prev_zone_index = 0;
Log("Entered the start zone");
PrintToPlayer(ply, "Entered the start zone");
}
}
else if (prev_zone_index == 0 && start_tick < 0)
{
// In start zone, check if leaving.
if (!this.zones[0].Clips(ply))
{
this.players[entindex].start_tick = this.tick_count;
Log("Left the start zone");
PrintToPlayer(ply, "Run started");
}
}
else
{
// Run ongoing.
// Check if re-entering the start zone.
if (this.zones[0].Clips(ply))
{
this.players[entindex].prev_zone_index = 0;
this.players[entindex].start_tick = -1;
Log("Entered the start zone");
PrintToPlayer(ply, "Entered the start zone");
continue;
}
// Check if entering any later zone.
// We allow skipping zones on purpose here.
for (local i = prev_zone_index + 1; i < this.zones.len(); i++)
{
if (this.zones[i].Clips(ply))
{
local run_ticks = this.tick_count - start_tick;
local run_time = run_ticks * 3.0 / 200.0;
if (i == this.zones.len() - 1)
{
// Entered the final zone.
this.players[entindex].prev_zone_index = -1;
this.players[entindex].start_tick = -1;
Log(format("Finished run in %.3fs (%d ticks)", run_time, run_ticks));
PrintToPlayer(
null,
format(
"\x03%s\x01 finished a run in \x04%.3f\x01s (%d ticks)",
GetPlayerName(ply),
run_time,
run_ticks
)
);
}
else
{
this.players[entindex].prev_zone_index = i;
Log(format("Entered zone %d at %.3fs (%d ticks)", i, run_time, run_ticks));
PrintToPlayer(ply, format("Entered zone \x03%d\x01 at \x04%.3f\x01s (%d ticks)", i, run_time, run_ticks));
}
break;
}
}
}
}
}
}
// --------------------------------
// Commands
// --------------------------------
// Build a new zone!
// You should call this 3 times to pick the bounds.
::CreateZone <- function()
{
if ("zone_builder" in getroottable())
{
local ply = GetListenServerHost();
local zone = ::zone_builder.PickPoint(::zone_builder.GetPlayerAimPoint(ply));
if (zone != null)
{
::cool_timer.AddZone(zone);
}
}
}
// Draw all created zones.
::DrawZones <- function()
{
if ("cool_timer" in getroottable())
{
foreach (zone in ::cool_timer.zones)
{
zone.Draw(10.0);
}
}
}
// Remove the last zone.
::RemoveZone <- function()
{
if ("cool_timer" in getroottable())
{
::cool_timer.RemoveZone();
}
}
// Remove all the zones.
::ClearZones <- function()
{
if ("cool_timer" in getroottable())
{
::cool_timer.ClearZones();
}
}
// Debug print zone bounds to console.
::DumpZones <- function()
{
if ("cool_timer" in getroottable())
{
local num_zones = ::cool_timer.zones.len();
for (local i = 0; i < num_zones; i++)
{
Log("Zone " + i);
::cool_timer.zones[i].Print();
Log("");
}
}
}
// Save zones to a text file in scriptdata folder.
::SaveZones <- function()
{
if ("cool_timer" in getroottable())
{
::cool_timer.SaveZones();
}
}
// Load zones from a text file in scriptdata folder.
::LoadZones <- function()
{
if ("cool_timer" in getroottable())
{
::cool_timer.LoadZones();
}
}
// --------------------------------
// Hooks
// --------------------------------
function TimerThink()
{
if ("cool_timer" in getroottable())
{
::cool_timer.Tick();
}
if ("zone_builder" in getroottable())
{
if (::zone_builder.IsBuilding())
{
local ply = GetListenServerHost();
::zone_builder.Draw(::zone_builder.GetPlayerAimPoint(ply));
}
}
return -1; // Think every tick
}
if (!("cool_timer" in getroottable()))
{
::cool_timer <- Timer();
::zone_builder <- ZoneBuilder();
if (!("TIMER_HOOKED_EVENTS" in getroottable()))
{
Log("Hooking game events");
__CollectGameEventCallbacks(this);
::TIMER_HOOKED_EVENTS <- true;
}
local thinker = SpawnEntityFromTable("info_target", { targetname = "cool_timer_thinker" } );
if (thinker.ValidateScriptScope())
{
Log("Creating thinker");
thinker.GetScriptScope()["TimerThink"] <- TimerThink;
AddThinkToEnt(thinker, "TimerThink");
}
}