Skip to content

Scores

A Score is one scoreboard cell - an (objective, target) pair - with typed methods for every scoreboard operation. You get one from an Objective:

ts
const game = dp.objective("game");        // scoreboard objective
const score = game.score(ScoreTarget("total"));   // one cell on it

ScoreTarget is the holder - a fake-player name like "total", or a selector. The Score object is just a handle; nothing is emitted until you call a mutating method with a context.

Two families of operation

Minecraft splits score math into two commands, and Score mirrors that split with two naming families. Getting them straight is the one thing worth internalising here.

You want…MethodEmits
set/add/remove a literal integer.set(n) .add(n) .remove(n)scoreboard players set/add/remove … n
combine with another score.assign .plus .minus .times .divide .modulo .min .max .swapscoreboard players operation … <op> …

The verbs differ on purpose: add/remove are taken by the literal-constant commands, so score-to-score +=/-= are plus/minus. All the operation verbs return this, so they chain - acc.times(k).plus(delta) reads as algebra.

ts
import { Datapack, v26_2, ScoreTarget } from "helix";

const dp = new Datapack("scores", v26_2);
const game = dp.objective("game");

const tick = dp.createFunction("tick");
tick.build((ctx) => {
  const score = game.score(ScoreTarget("score"));
  const bonus = game.score(ScoreTarget("bonus"));

  score.add(1, ctx);        // literal:        scoreboard players add … 1
  score.plus(bonus, ctx);   // score-to-score: score += bonus
});

▶ Open in playground

Compiled output - the real files helix emits for the code above:

txt
scoreboard players add score game 1
scoreboard players operation score game += bonus game
json
{
  "values": [
    "scores:tick"
  ]
}

The context argument

Every mutating method takes an optional trailing ctx. That's where the command is emitted. Inside a .build(), .run(), or .if() callback there is an ambient context, so the operation verbs (plus, times, …) can find it themselves - but the literal set/add/remove only emit when you pass ctx explicitly (without it they just record the pending value). The habit that always works: pass ctx. Pass it explicitly too when two contexts are in scope and you mean the outer one.

Comparisons drive control flow

.equal, .greaterThan, and .lessThan don't emit - they return a condition you hand to ctx.if. The if body compiles to its own child function, which is why you'll see a second .mcfunction in the output:

ts
import { Datapack, v26_2, ScoreTarget } from "helix";

const dp = new Datapack("scores", v26_2);
const game = dp.objective("game");

const tick = dp.createFunction("tick");
tick.build((ctx) => {
  const score = game.score(ScoreTarget("score"));
  ctx.if(score.greaterThan(100), (ctx) => {
    score.set(0, ctx);   // reset once we cross the threshold
  });
});

▶ Open in playground

Compiled output - the real files helix emits for the code above:

txt
execute if score score game matches 100.. run scoreboard players set score game 0
json
{
  "values": [
    "scores:tick"
  ]
}

(Score-to-score comparisons aren't wired up yet - greaterThan(otherScore) throws. Feed a literal, or diff into a scratch score first.)

Capturing a command's result

score.storeResult(ctx, command) wraps a command in execute store result score …, so a count or query lands directly in a cell - the typed form of execute store result score.

Going further

For vector algebra over three cells at once, see Score vectors; for sub-integer precision, Fixed tracks a scale factor on top of this same Score primitive.

Released under the MIT License · Credits