Skip to Content
Modlock

Ready up and count down

Hold the round until every player is ready, then count it in.

Most modes open a round the same way, and this page shows you how to build it: the round waits until every player is ready, then counts them in.

The round is a state machine with three phases, and each handler checks the phase before it acts:

PhasePlayers canMoves on when
waitingType /readyEvery human is ready.
countdownWatch the countThe count reaches zero.
livePlayYour mode ends the round.
import { command, onPlayerLeave, type Player, players, sequence } from 'modlock'

let phase: 'waiting' | 'countdown' | 'live' = 'waiting'
const ready = new Set<number>()

function humans(): Player[] {
  return players()
    .filter((connection) => !connection.bot)
    .map((connection) => connection.player)
}

command('ready', (player) => {
  if (phase !== 'waiting') {
    return
  }
  ready.add(player.slot)
  const everyone = humans()
  const missing = everyone.filter((p) => !ready.has(p.slot)).length
  if (missing > 0) {
    for (const p of everyone) {
      p.centerText(`Waiting for ${missing} more`)
    }
    return
  }
  countDown()
})

function countDown() {
  phase = 'countdown'
  for (const p of humans()) {
    p.freeze(true)
  }
  // Runs at 3, 2, 1 and then starts the round.
  sequence((run) => {
    const left = 4 - run
    for (const p of humans()) {
      if (left > 0) {
        p.centerText(String(left))
      } else {
        p.freeze(false)
        p.announce('Fight!', 'The round is live.')
      }
    }
    if (left > 0) {
      return 1
    }
    phase = 'live'
    ready.clear()
  })
}

// A player who leaves no longer holds the round up.
onPlayerLeave(({ connection }) => {
  ready.delete(connection.player.slot)
})
local modlock = require("@modlock")

local phase: "waiting" | "countdown" | "live" = "waiting"
local ready: { [number]: boolean } = {}

local function humans(): { modlock.Player }
	local list = {}
	for _, connection in modlock.players() do
		if not connection.bot then
			table.insert(list, connection.player)
		end
	end
	return list
end

local function countDown()
	phase = "countdown"
	for _, p in humans() do
		p:freeze(true)
	end
	-- Runs at 3, 2, 1 and then starts the round.
	modlock.sequence(function(run)
		local left = 4 - run
		for _, p in humans() do
			if left > 0 then
				p:centerText(tostring(left))
			else
				p:freeze(false)
				p:announce("Fight!", "The round is live.")
			end
		end
		if left > 0 then
			return 1
		end
		phase = "live"
		ready = {}
		return nil
	end)
end

modlock.command("ready", function(player)
	if phase ~= "waiting" then
		return
	end
	ready[player.slot] = true
	local everyone = humans()
	local missing = 0
	for _, p in everyone do
		if not ready[p.slot] then
			missing += 1
		end
	end
	if missing > 0 then
		for _, p in everyone do
			p:centerText(`Waiting for {missing} more`)
		end
		return
	end
	countDown()
end)

-- A player who leaves no longer holds the round up.
modlock.onPlayerLeave(function(left)
	ready[left.connection.player.slot] = nil
end)
import modlock

phase = "waiting"  # then "countdown", then "live"
ready: set[int] = set()


def humans() -> list[modlock.Player]:
    return [c.player for c in modlock.players() if not c.bot]


@modlock.command("ready")
def ready_up(player: modlock.Player, args: str) -> None:
    if phase != "waiting":
        return
    ready.add(player.slot)
    everyone = humans()
    missing = sum(1 for p in everyone if p.slot not in ready)
    if missing > 0:
        for p in everyone:
            p.center_text(f"Waiting for {missing} more")
        return
    count_down()


def count_down() -> None:
    global phase
    phase = "countdown"
    for p in humans():
        p.freeze(True)

    # Runs at 3, 2, 1 and then starts the round.
    def step(run: int) -> float | None:
        global phase
        left = 4 - run
        for p in humans():
            if left > 0:
                p.center_text(str(left))
            else:
                p.freeze(False)
                p.announce("Fight!", "The round is live.")
        if left > 0:
            return 1
        phase = "live"
        ready.clear()
        return None

    modlock.sequence(step)


# A player who leaves no longer holds the round up.
@modlock.on_player_leave
def left(event: modlock.PlayerLeftEvent) -> None:
    ready.discard(event.connection.player.slot)
package main

import (
	"fmt"
	"strconv"
	"time"

	"github.com/paralin/modlock/mod"
)

var (
	phase = "waiting" // then "countdown", then "live"
	ready = map[int32]bool{}
)

func humans() []mod.Player {
	var list []mod.Player
	connections, _ := mod.Players()
	for _, c := range connections {
		if !c.GetBot() {
			list = append(list, mod.PlayerOf(c.GetPlayer()))
		}
	}
	return list
}

func init() {
	mod.Command("ready", func(p mod.Player, args string) {
		if phase != "waiting" {
			return
		}
		ready[p.Slot()] = true
		everyone := humans()
		missing := 0
		for _, other := range everyone {
			if !ready[other.Slot()] {
				missing++
			}
		}
		if missing > 0 {
			for _, other := range everyone {
				_ = other.CenterText(fmt.Sprintf("Waiting for %d more", missing))
			}
			return
		}
		countDown()
	})

	// A player who leaves no longer holds the round up.
	mod.OnPlayerLeave(func(left *mod.PlayerLeftEvent) {
		delete(ready, left.GetConnection().GetPlayer())
	})
}

func countDown() {
	phase = "countdown"
	for _, p := range humans() {
		_ = p.Freeze(nil)
	}
	// Runs at 3, 2, 1 and then starts the round.
	mod.Sequence(func(run int) (time.Duration, bool) {
		left, released := 4-run, false
		for _, p := range humans() {
			if left > 0 {
				_ = p.CenterText(strconv.Itoa(left))
			} else {
				_ = p.Freeze(&released)
				_ = p.Announce("Fight!", "The round is live.")
			}
		}
		if left > 0 {
			return time.Second, true
		}
		phase = "live"
		clear(ready)
		return 0, false
	})
}

func main() {}

To try it, save while modlock dev runs and type /ready in chat. When you play alone, nobody else is missing, so the count starts at once.

While anyone is missing, every player sees how many, such as Waiting for 2 more, in the middle of the screen. When the last one is ready, their heroes freeze and they see 3, 2 and 1, one second apart. Then the heroes are released, and everyone sees the announcement "Fight!" with "The round is live."

Variations

On this page