Checkpoint race
Time each runner through a course of checkpoints.
This page shows you how to build a race that times each runner through a course. The course is a list of points: a start line, checkpoints and a finish.
A runner starts with /race, and each frame your mod checks whether they have
reached their next checkpoint:
import {
command,
onFrame,
onPlayerLeave,
type Player,
type Vector,
} from 'modlock'
// The first point is the start line and the last is the finish.
const course: Vector[] = [
{ x: 0, y: 0, z: 64 },
{ x: 1200, y: 0, z: 64 },
{ x: 1200, y: 1600, z: 64 },
]
const reach = 150
interface Run {
player: Player
next: number
started: number
}
const runs = new Map<number, Run>()
let now = 0
command('race', (player) => {
const start = course[0]
if (start === undefined) {
return
}
player.teleport(start, { pitch: 0, yaw: 0, roll: 0 })
runs.set(player.slot, { player, next: 1, started: now })
player.centerText('Go!')
})
onFrame((frame) => {
now = frame.timeSeconds
for (const [slot, run] of runs) {
const at = run.player.pawn()?.position
const gate = course[run.next]
if (at === undefined || gate === undefined) {
continue
}
if (Math.hypot(at.x - gate.x, at.y - gate.y, at.z - gate.z) > reach) {
continue
}
run.next += 1
if (run.next < course.length) {
run.player.centerText(
`Checkpoint ${run.next - 1} of ${course.length - 2}`,
)
continue
}
run.player.announce('Finished', `${(now - run.started).toFixed(2)} seconds`)
runs.delete(slot)
}
})
onPlayerLeave(({ connection }) => {
runs.delete(connection.player.slot)
})local modlock = require("@modlock")
-- The first point is the start line and the last is the finish.
local course = {
{ x = 0, y = 0, z = 64 },
{ x = 1200, y = 0, z = 64 },
{ x = 1200, y = 1600, z = 64 },
}
local reach = 150
type Run = { player: modlock.Player, next: number, started: number }
local runs: { [number]: Run } = {}
local now = 0
modlock.command("race", function(player)
player:teleport(course[1], { pitch = 0, yaw = 0, roll = 0 })
runs[player.slot] = { player = player, next = 2, started = now }
player:centerText("Go!")
end)
modlock.onFrame(function(frame)
now = frame.timeSeconds
for slot, run in runs do
local pawn = run.player:pawn()
local gate = course[run.next]
if pawn and gate then
local at = pawn.position
local dx, dy, dz = at.x - gate.x, at.y - gate.y, at.z - gate.z
if math.sqrt(dx * dx + dy * dy + dz * dz) <= reach then
run.next += 1
if run.next <= #course then
run.player:centerText(`Checkpoint {run.next - 2} of {#course - 2}`)
else
run.player:announce("Finished", string.format("%.2f seconds", now - run.started))
runs[slot] = nil
end
end
end
end
end)
modlock.onPlayerLeave(function(left)
runs[left.connection.player.slot] = nil
end)import dataclasses
import math
import modlock
# The first point is the start line and the last is the finish.
COURSE = [
modlock.Vector(0, 0, 64),
modlock.Vector(1200, 0, 64),
modlock.Vector(1200, 1600, 64),
]
REACH = 150
@dataclasses.dataclass
class Run:
player: modlock.Player
next: int
started: float
runs: dict[int, Run] = {}
now = 0.0
@modlock.command("race")
def race(player: modlock.Player, args: str) -> None:
player.teleport(COURSE[0], modlock.Angles(0, 0, 0))
runs[player.slot] = Run(player, 1, now)
player.center_text("Go!")
@modlock.on_frame
def frame(frame: modlock.FrameEvent) -> None:
global now
now = frame.time_seconds
for slot, run in list(runs.items()):
pawn = run.player.pawn()
if pawn is None:
continue
at, gate = pawn.position, COURSE[run.next]
if math.dist((at.x, at.y, at.z), (gate.x, gate.y, gate.z)) > REACH:
continue
run.next += 1
if run.next < len(COURSE):
run.player.center_text(f"Checkpoint {run.next - 1} of {len(COURSE) - 2}")
continue
run.player.announce("Finished", f"{now - run.started:.2f} seconds")
del runs[slot]
@modlock.on_player_leave
def left(event: modlock.PlayerLeftEvent) -> None:
runs.pop(event.connection.player.slot, None)package main
import (
"fmt"
"math"
"github.com/paralin/modlock/mod"
)
// The first point is the start line and the last is the finish.
var course = []*mod.Vector{
{X: 0, Y: 0, Z: 64},
{X: 1200, Y: 0, Z: 64},
{X: 1200, Y: 1600, Z: 64},
}
const reach = 150
type run struct {
next int
started float64
}
var (
runs = map[int32]*run{}
now float64
)
func init() {
mod.Command("race", func(p mod.Player, args string) {
_ = p.Teleport(course[0], &mod.Angles{}, nil)
runs[p.Slot()] = &run{next: 1, started: now}
_ = p.CenterText("Go!")
})
mod.OnFrame(func(frame *mod.FrameEvent) {
now = frame.GetTimeSeconds()
for slot, r := range runs {
p := mod.PlayerOf(slot)
pawn, err := p.Pawn()
if err != nil || pawn == nil {
continue
}
at, gate := pawn.GetPosition(), course[r.next]
dx, dy, dz := at.GetX()-gate.GetX(), at.GetY()-gate.GetY(), at.GetZ()-gate.GetZ()
if math.Sqrt(float64(dx*dx+dy*dy+dz*dz)) > reach {
continue
}
r.next++
if r.next < len(course) {
_ = p.CenterText(fmt.Sprintf("Checkpoint %d of %d", r.next-1, len(course)-2))
continue
}
_ = p.Announce("Finished", fmt.Sprintf("%.2f seconds", now-r.started))
delete(runs, slot)
}
})
mod.OnPlayerLeave(func(left *mod.PlayerLeftEvent) {
delete(runs, left.GetConnection().GetPlayer())
})
}
func main() {}Move the course's points to where your hero can run, then type /race in chat.
The runner sees Go! in the middle of the screen at the start line, then each
checkpoint's number, such as Checkpoint 1 of 1. At the finish they see the
announcement "Finished" with their time in seconds.
Variations
- Show each checkpoint with text in the world.
In TypeScript, use
RaceGatefor a glowing gate to run through. - A reload loses a run in progress, so hold the reload while anyone is racing.
- Rank the times with a board in milliseconds, ordered ascending.
- The
raceexample is a complete race in Go, with its rules tested by plaingo test.