Skip to Content
Modlock

King of the hill

Score a point each second a hero holds a spot alone.

In king of the hill, heroes fight over one spot, and a hero who holds it alone scores. Here you build it with a once-a-second timer and a hill that is a point with a radius of 400 units.

Your mod marks the hill each time a world loads, then scores the hero who stands on it alone:

import {
  createText,
  every,
  onWorld,
  players,
  type Player,
  type Vector,
} from 'modlock'

const hill: Vector = { x: 0, y: 0, z: 0 }
const radius = 400
const goal = 30
const points = new Map<Player, number>()

// Mark the hill each time a world loads, and start the score over.
onWorld(() => {
  points.clear()
  createText({
    text: 'HILL',
    position: { x: hill.x, y: hill.y, z: hill.z + 200 },
    faceCamera: true,
  })
})

every(1, () => {
  const holders = players().filter(({ player }) => {
    const pawn = player.pawn()
    return (
      pawn !== undefined &&
      pawn.health > 0 &&
      Math.hypot(pawn.position.x - hill.x, pawn.position.y - hill.y) <= radius
    )
  })
  const [holder] = holders
  if (holder === undefined || holders.length > 1) {
    return // empty or contested
  }
  const total = (points.get(holder.player) ?? 0) + 1
  points.set(holder.player, total)
  holder.player.centerText(`${total} / ${goal}`)
  if (total >= goal) {
    for (const { player } of players()) {
      player.announce(
        `${holder.name} holds the hill`,
        'A new round starts now.',
      )
    }
    points.clear()
  }
})
local modlock = require("@modlock")

local hill = { x = 0, y = 0, z = 0 }
local radius = 400
local goal = 30
local points: { [modlock.Player]: number } = {}

-- Mark the hill each time a world loads, and start the score over.
modlock.onWorld(function()
	points = {}
	modlock.createText({
		text = "HILL",
		position = { x = hill.x, y = hill.y, z = hill.z + 200 },
		faceCamera = true,
	})
end)

modlock.every(1, function()
	local holders: { modlock.Connection } = {}
	for _, connection in modlock.players() do
		local pawn = connection.player:pawn()
		if pawn and pawn.health > 0 then
			local dx, dy = pawn.position.x - hill.x, pawn.position.y - hill.y
			if math.sqrt(dx * dx + dy * dy) <= radius then
				table.insert(holders, connection)
			end
		end
	end
	if #holders ~= 1 then
		return -- empty or contested
	end
	local holder = holders[1]
	local total = (points[holder.player] or 0) + 1
	points[holder.player] = total
	holder.player:centerText(`{total} / {goal}`)
	if total >= goal then
		for _, connection in modlock.players() do
			connection.player:announce(`{holder.name} holds the hill`, "A new round starts now.")
		end
		points = {}
	end
end)
import math

import modlock

HILL = modlock.Vector(0, 0, 0)
RADIUS = 400
GOAL = 30
points: dict[modlock.Player, int] = {}


# Mark the hill each time a world loads, and start the score over.
@modlock.on_world
def mark(map_name: str) -> None:
    points.clear()
    modlock.create_text(
        modlock.TextOptions(
            text="HILL",
            position=modlock.Vector(HILL.x, HILL.y, HILL.z + 200),
            face_camera=True,
        )
    )


def on_hill(connection: modlock.Connection) -> bool:
    pawn = connection.player.pawn()
    if pawn is None or pawn.health <= 0:
        return False
    return math.hypot(pawn.position.x - HILL.x, pawn.position.y - HILL.y) <= RADIUS


def score() -> None:
    holders = [c for c in modlock.players() if on_hill(c)]
    if len(holders) != 1:
        return  # empty or contested
    holder = holders[0]
    total = points.get(holder.player, 0) + 1
    points[holder.player] = total
    holder.player.center_text(f"{total} / {GOAL}")
    if total >= GOAL:
        for connection in modlock.players():
            connection.player.announce(f"{holder.name} holds the hill", "A new round starts now.")
        points.clear()


modlock.every(1, score)
package main

import (
	"fmt"
	"math"
	"time"

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

const (
	radius = 400
	goal   = 30
)

var (
	hill   = &mod.Vector{}
	points = map[mod.Player]int{}
)

func onHill(c *mod.Connection) bool {
	pawn, err := mod.PlayerOf(c.GetPlayer()).Pawn()
	if err != nil || pawn == nil || pawn.GetHealth() <= 0 {
		return false
	}
	at := pawn.GetPosition()
	return math.Hypot(float64(at.GetX()-hill.GetX()), float64(at.GetY()-hill.GetY())) <= radius
}

func init() {
	// Mark the hill each time a world loads, and start the score over.
	mod.OnWorld(func(string) {
		clear(points)
		faceCamera := true
		_, _ = mod.CreateText(&mod.TextOptions{
			Text:       "HILL",
			Position:   &mod.Vector{X: hill.GetX(), Y: hill.GetY(), Z: hill.GetZ() + 200},
			FaceCamera: &faceCamera,
		})
	})

	mod.Every(time.Second, func() {
		connections, _ := mod.Players()
		var holders []*mod.Connection
		for _, c := range connections {
			if onHill(c) {
				holders = append(holders, c)
			}
		}
		if len(holders) != 1 {
			return // empty or contested
		}
		holder := holders[0]
		p := mod.PlayerOf(holder.GetPlayer())
		points[p]++
		total := points[p]
		_ = p.CenterText(fmt.Sprintf("%d / %d", total, goal))
		if total >= goal {
			for _, c := range connections {
				_ = mod.PlayerOf(c.GetPlayer()).Announce(holder.GetName()+" holds the hill", "A new round starts now.")
			}
			clear(points)
		}
	})
}

func main() {}

Put the hill where your hero stands, as the first variation below shows, and stand there alone.

Each point shows the hero their total, such as 3 / 30, in the middle of the screen. At 30, every player sees the announcement "name holds the hill", and the score starts over. The timer runs on the game's clock, so the score does not depend on the frame rate.

Variations

  • Put the hill where you stand: read your position with a command, as the tutorial does, and copy it in.
  • Move the hill every minute with a second timer, and move its text with it.
  • Score teams instead of players by counting the teams on the hill. A pawn's team is 2 or 3.
  • The hill example is a complete mode built this way.

On this page