Skip to Content
Modlock

Aim drill

Give one player a target and score thirty seconds of hits.

An aim drill gives one player thirty seconds to land as many hits on a target as they can, and counts them.

A player starts the drill with /drill, and your mod adds a bot named Target for them to shoot:

import {
  addBot,
  after,
  command,
  onDamaged,
  onWorld,
  type Player,
} from 'modlock'

const seconds = 30

interface Drill {
  player: Player
  target: Player
  hits: number
  damage: number
}
let drill: Drill | undefined

command('drill', (player) => {
  if (drill !== undefined) {
    player.chat('A drill is already running.')
    return
  }
  // The target joins the other team, so hits on it count.
  const target = addBot({
    name: 'Target',
    team: player.pawn()?.team === 2 ? 3 : 2,
  })
  if (target === undefined) {
    return
  }
  const current: Drill = { player, target, hits: 0, damage: 0 }
  drill = current
  player.announce('Aim drill', `Hit the target for ${seconds} seconds.`)
  after(seconds, () => {
    if (drill !== current) {
      return
    }
    player.announce('Time!', `${current.hits} hits, ${current.damage} damage`)
    player.addMetric('drill.damage', current.damage)
    target.removeBot()
    drill = undefined
  })
})

// Count the player's hits, and keep the target standing.
onDamaged((hit) => {
  if (
    drill === undefined ||
    hit.victim !== drill.target.pawn()?.entity ||
    hit.attacker !== drill.player.pawn()?.entity
  ) {
    return
  }
  drill.hits += 1
  drill.damage += hit.healthLost
  drill.target.heal(hit.healthLost)
})

// A new world has already removed the target, so forget the drill.
onWorld(() => {
  drill = undefined
})
local modlock = require("@modlock")

local seconds = 30

type Drill = { player: modlock.Player, target: modlock.Player, hits: number, damage: number }
local drill: Drill? = nil

modlock.command("drill", function(player)
	if drill then
		player:chat("A drill is already running.")
		return
	end
	-- The target joins the other team, so hits on it count.
	local pawn = player:pawn()
	local target = modlock.addBot({
		name = "Target",
		team = if pawn and pawn.team == 2 then 3 else 2,
	})
	if target == nil then
		return
	end
	local current: Drill = { player = player, target = target, hits = 0, damage = 0 }
	drill = current
	player:announce("Aim drill", `Hit the target for {seconds} seconds.`)
	modlock.after(seconds, function()
		if drill ~= current then
			return
		end
		player:announce("Time!", `{current.hits} hits, {current.damage} damage`)
		player:addMetric("drill.damage", current.damage)
		target:removeBot()
		drill = nil
	end)
end)

-- Count the player's hits, and keep the target standing.
modlock.onDamaged(function(hit)
	if drill == nil then
		return
	end
	local target, shooter = drill.target:pawn(), drill.player:pawn()
	if target == nil or shooter == nil or hit.victim ~= target.entity or hit.attacker ~= shooter.entity then
		return
	end
	drill.hits += 1
	drill.damage += hit.healthLost
	drill.target:heal(hit.healthLost)
end)

-- A new world has already removed the target, so forget the drill.
modlock.onWorld(function()
	drill = nil
end)
import dataclasses

import modlock

SECONDS = 30


@dataclasses.dataclass
class Drill:
    player: modlock.Player
    target: modlock.Player
    hits: int = 0
    damage: int = 0


drill: Drill | None = None


@modlock.command("drill")
def start(player: modlock.Player, args: str) -> None:
    global drill
    if drill is not None:
        player.chat("A drill is already running.")
        return
    # The target joins the other team, so hits on it count.
    pawn = player.pawn()
    team = 3 if pawn is not None and pawn.team == 2 else 2
    target = modlock.add_bot(modlock.BotOptions(name="Target", team=team))
    if target is None:
        return
    current = drill = Drill(player, target)
    player.announce("Aim drill", f"Hit the target for {SECONDS} seconds.")

    def finish() -> None:
        global drill
        if drill is not current:
            return
        player.announce("Time!", f"{current.hits} hits, {current.damage} damage")
        player.add_metric("drill.damage", current.damage)
        target.remove_bot()
        drill = None

    modlock.after(SECONDS, finish)


# Count the player's hits, and keep the target standing.
@modlock.on_damaged
def hit(hit: modlock.DamagedEvent) -> None:
    if drill is None:
        return
    target, shooter = drill.target.pawn(), drill.player.pawn()
    if target is None or shooter is None:
        return
    if hit.victim != target.entity or hit.attacker != shooter.entity:
        return
    drill.hits += 1
    drill.damage += hit.health_lost
    drill.target.heal(hit.health_lost)


# A new world has already removed the target, so forget the drill.
@modlock.on_world
def forget(map_name: str) -> None:
    global drill
    drill = None
package main

import (
	"fmt"
	"time"

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

const seconds = 30

type drillRun struct {
	player, target mod.Player
	hits, damage   int
}

var drill *drillRun

// entityOf returns the entity of a player's hero, or 0 without one.
func entityOf(p mod.Player) uint32 {
	pawn, err := p.Pawn()
	if err != nil || pawn == nil {
		return 0
	}
	return pawn.GetEntity()
}

func init() {
	mod.Command("drill", func(p mod.Player, args string) {
		if drill != nil {
			_ = p.Chat("A drill is already running.")
			return
		}
		// The target joins the other team, so hits on it count.
		team := int32(2)
		if pawn, err := p.Pawn(); err == nil && pawn != nil && pawn.GetTeam() == 2 {
			team = 3
		}
		target, err := mod.AddBot(&mod.BotOptions{Name: "Target", Team: team})
		if err != nil {
			return
		}
		current := &drillRun{player: p, target: target}
		drill = current
		_ = p.Announce("Aim drill", fmt.Sprintf("Hit the target for %d seconds.", seconds))
		mod.After(seconds*time.Second, func() {
			if drill != current {
				return
			}
			_ = p.Announce("Time!", fmt.Sprintf("%d hits, %d damage", current.hits, current.damage))
			_ = p.AddMetric("drill.damage", float64(current.damage), nil)
			_ = target.RemoveBot()
			drill = nil
		})
	})

	// Count the player's hits, and keep the target standing.
	mod.OnDamaged(func(hit *mod.DamagedEvent) {
		if drill == nil || hit.GetVictim() != entityOf(drill.target) || hit.GetAttacker() != entityOf(drill.player) {
			return
		}
		drill.hits++
		drill.damage += int(hit.GetHealthLost())
		_ = drill.target.Heal(int32(hit.GetHealthLost()))
	})

	// A new world has already removed the target, so forget the drill.
	mod.OnWorld(func(string) { drill = nil })
}

func main() {}

Save, then type /drill in chat.

The player sees the announcement "Aim drill" with "Hit the target for 30 seconds." After each hit, the mod heals Target by the damage that hit dealt, so Target stays standing for the whole drill. When the time is up, the player sees "Time!" with their hits and damage.

The mod declares the metric in mod.json:

"metrics": [{ "name": "drill.damage", "kind": "KIND_MAX" }]

Variations

  • Move the target between hits with teleport so the player tracks it.
  • Freeze the target so it stands still, or leave it free to fight back.
  • Rank scores with a board, partitioned by scenario and difficulty.
  • The drill example is a complete drill mode.

On this page