Skip to content

Points and values

Lesson 17.3

"head" and "return" work on the edges of a target. Some changes need to happen in the middle: after the shield took its part of the damage, or right before the knockback. For that, targets have sub-points: named places inside them.

There are two kinds:

  • A point is a moment inside the target. Your function can read the values the game has at that moment (ci.locals), change the ones marked !, and on some points skip the next step.
  • A value is one number (or block) the game uses inside the target, like the knockback strength or the critical hit multiplier. Your function gets it as ci.value and can replace it.

You write them as "point:<name>" and "value:<name>".

Values

-- Critical hits do double damage instead of 1.5 times.
cl.mixin.inject("player.attack", "value:crit_multiplier", function(ci)
  ci.value = 2.0
end)

-- Sheep are knocked back three times as far.
cl.mixin.inject("entity.living.hurt", "value:knockback_strength", function(ci)
  ci.value = ci.value * 3
end, { filter = "minecraft:sheep" })

A value mixin changes one number and leaves everything around it alone. That makes it the gentlest kind of mixin: the critical hit still needs a falling player, still plays its sound and shows its particles. Only the multiplier is different.

Points

-- After a shield blocked part of a hit, the rest is halved too.
cl.mixin.inject("entity.living.hurt", "point:after_blocking", function(ci)
  if ci.locals.blocked > 0 then
    ci.locals.amount = ci.locals.amount / 2
  end
end)

At after_blocking, ci.locals.blocked is the damage the shield took, and ci.locals.amount (changeable) is what is left for the entity. ci.args still works inside points too.

Some points can be cancelled, and then cancelling skips one step instead of the whole target:

-- Hits do damage as usual, but nobody is knocked back.
cl.mixin.inject("entity.living.hurt", "point:before_knockback", function(ci)
  ci:cancel()
end)

-- No sweep attacks: the main target is hit, the mobs around it aren't.
cl.mixin.inject("player.attack", "point:before_sweep", function(ci)
  ci:cancel()
end)

Compare that with a head cancel on entity.living.hurt, which would stop the damage as well.

Every point and value in API 1.5

Target Points Values
entity.living.hurt after_blocking {amount!, blocked}, before_knockback {strength!, dx, dz} (cancel skips the knockback), before_death invulnerability_ticks (20), knockback_strength (0.4)
entity.living.actually_hurt after_armor, after_magic
entity.living.cause_fall_damage safe_distance
entity.mob.check_despawn far_distance (128), random_chance (800)
player.attack before_sweep (cancel skips the sweep) crit_multiplier (1.5)
player.food_tick exhaustion_threshold (4.0)
player.respawn before_place {x!, y!, z!, yaw!, pitch!}
block.place_item placement_state

The numbers in brackets are vanilla’s values. A misspelt name is an error that lists the names the target has: entity.living.hurt has no point 'before_knock'.

-- Players respawn 5 blocks higher than vanilla would put them.
cl.mixin.inject("player.respawn", "point:before_place", function(ci)
  ci.locals.y = ci.locals.y + 5
end)

-- Mobs only despawn when no player is within 64 blocks (vanilla: 128).
cl.mixin.inject("entity.mob.check_despawn", "value:far_distance", function(ci)
  ci.value = 64
end)

Calls are planned

CLight also has a third sub-point kind, "call:<name>", for redirecting one call a target makes (like Java’s @Redirect). No API 1.5 target has a call site yet; they are Planned for the next game-logic batch, together with more points and values. Existing points and values never change, so what you write now keeps working.

Client points (API 1.16)

Client targets have points too. client.render.hud has one point per HUD layer, and cancelling a point skips that layer; client.render.fog and client.camera.setup have a computed point where you change what is drawn. These run only in your own game, and the HUD needs the ui.inject permission in your manifest.

-- no hotbar and no hearts on this screen (needs "ui.inject")
cl.mixin.inject("client.render.hud", "point:hotbar", function(ci) ci:cancel() end)
cl.mixin.inject("client.render.hud", "point:player_health", function(ci) ci:cancel() end)

-- the fog starts twice as far away (only the picture changes, never what is loaded)
cl.mixin.inject("client.render.fog", "point:computed", function(ci)
  ci.locals.render_distance_start = ci.locals.render_distance_start * 2
end)

World and data points (API 1.17)

The world and data targets have points too: loot.table.roll has each_drop (like the block and mob drops), recipe.match has matched (cancel it to refuse a recipe), and command.suggest has each_suggestion (cancel it to drop one completion). Commands cross as text: read one with cl.text.to_plain, replace one with cl.text.literal.

-- fishing catches only cod
cl.mixin.inject("loot.table.roll", "point:each_drop", function(ci)
  ci.locals.item = "minecraft:cod"
end, { filter = "minecraft:gameplay/fishing" })

-- the crafting table recipe is switched off
cl.mixin.inject("recipe.match", "point:matched", function(ci)
  if ci.locals.recipe == "minecraft:crafting_table" then ci:cancel() end
end)

-- nobody is offered /stop in tab completion
cl.mixin.inject("command.suggest", "point:each_suggestion", function(ci)
  if cl.text.to_plain(ci.locals.suggestion) == "stop" then ci:cancel() end
end)

The world generation points (worldgen.biome.fill each cell, worldgen.terrain.build before the noise fill, the surface and the carvers) are for C and C++ mods: generation runs on worker threads, where Lua can’t run.

Try it

Make two rules: with every hit, entities are invulnerable for only 10 ticks instead of 20 (so fast weapons hit more often), and player respawns are rotated to look straight north (yaw 180).

Show a solution
cl.mixin.inject("entity.living.hurt", "value:invulnerability_ticks", function(ci)
  ci.value = 10
end)

cl.mixin.inject("player.respawn", "point:before_place", function(ci)
  ci.locals.yaw = 180
end)

invulnerability_ticks is a value, so you replace it. before_place is a point whose yaw is marked !, so you change it in ci.locals.

Recap: "value:<name>" replaces one number inside a target (ci.value), and "point:<name>" runs at a named place, where ci.locals holds its values and a cancel skips one step.