628 lines
16 KiB
Text
628 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();
|
|
}
|
|
|
|
::FormatTime <- function(ticks)
|
|
{
|
|
local seconds = ticks * 3 / 200.0;
|
|
local minutes = floor(seconds / 60);
|
|
seconds %= 60;
|
|
|
|
if (minutes >= 60)
|
|
{
|
|
local hours = floor(minutes / 60);
|
|
minutes %= 60;
|
|
return format("%02d:%02d:%06.3f", hours, minutes, seconds);
|
|
}
|
|
else
|
|
{
|
|
return format("%02d:%06.3f", minutes, seconds);
|
|
}
|
|
}
|
|
|
|
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 = FormatTime(run_ticks);
|
|
|
|
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 %s (%d ticks)", run_time, run_ticks));
|
|
PrintToPlayer(
|
|
null,
|
|
format(
|
|
"\x03%s\x01 finished a run in \x04%s\x01 (%d ticks)",
|
|
GetPlayerName(ply),
|
|
run_time,
|
|
run_ticks
|
|
)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
this.players[entindex].prev_zone_index = i;
|
|
Log(format("Entered zone %d at %s (%d ticks)", i, run_time, run_ticks));
|
|
PrintToPlayer(ply, format("Entered zone \x03%d\x01 at \x04%s\x01 (%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");
|
|
}
|
|
}
|