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"); } 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)); } } 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_time = -1; constructor() { } } class Timer { zones = []; players = {}; 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 Tick() { 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_time = this.players[entindex].start_time; 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_time < 0) { // In start zone, check if leaving. if (!this.zones[0].Clips(ply)) { this.players[entindex].start_time = Time(); 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_time = -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_time = Time() - start_time; if (i == this.zones.len() - 1) { // Entered the final zone. this.players[entindex].prev_zone_index = -1; this.players[entindex].start_time = -1; Log("Finished run in " + run_time + "s"); PrintToPlayer( null, format( "\x03%s\x01 finished a run in \x04%f\x01s", GetPlayerName(ply), run_time ) ); } else { this.players[entindex].prev_zone_index = i; Log("Entered zone " + i + " at " + run_time + "s"); PrintToPlayer(ply, format("Entered zone \x03%d\x01 at \x04%f\x01s", i, run_time)); } 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(""); } } } // -------------------------------- // 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 0.0; // 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"); } }