Project: duelist
Lesson 12.6
A small server mod made only of mixins. Sword fights become sharper:
- critical hits do double damage instead of 1.5 times,
- there are no sweep attacks: a hit only hurts the mob you aimed at,
- lifesteal: every hit a player lands heals them by half a heart.
It keeps its mixins in a member of the Mod class, uses a value, a point and a ret mixin, and calls the entity.living.heal invoker.
Step 1: the project
> clight new cpp duelist --name "Duelist"
> cd duelist
All the targets the mod uses run on the server, so the players need nothing installed. Change side and sync in clight.mod.json (The manifest) and keep the runtime line the template wrote:
{
"schema": 1,
"id": "duelist",
"version": "1.0.0",
"name": "Duelist",
"description": "Double critical hits, no sweep attacks and lifesteal.",
"authors": ["Alex"],
"license": "MIT",
"api": 1,
"game": ">=26.3 <26.4",
"side": "server",
"sync": "server_only",
"runtime": { "type": "wasm", "entry": "bin/duelist.wasm" },
"permissions": []
}
Step 2: keep the handles
Each inject returns a handle that removes its mixin when destroyed. Instead of detach()ing every one, the mod keeps them in a std::vector member, so they live exactly as long as the mod. A small helper adds a handle and logs a failure:
#include <clight.hpp>
#include <vector>
class Mod final : public clight::Mod {
std::vector<clight::mixin::Handle> mixins_;
void keep(clight::Expected<clight::mixin::Handle> h, const char* what) {
if (h) mixins_.push_back(std::move(*h));
else clight::log::warn("Duelist: {} is not available ({})", what, static_cast<int>(h.error()));
}
public:
void on_construct() override;
};
Step 3: crits and sweeps
player.attack has a value crit_multiplier (1.5) and a point before_sweep, where a cancel skips the sweep (Points and values):
using Attack = clight::mx::PlayerAttack;
keep(clight::mixin::inject<Attack>(clight::at::value("crit_multiplier"), [](Attack::Ci& ci) {
ci.value<float>() = 2.0f;
}), "double crits");
keep(clight::mixin::inject<Attack>(clight::at::point("before_sweep"), [](Attack::Ci& ci) {
ci.cancel();
}), "no sweep");
Step 4: lifesteal
entity.living.hurt returns true when damage was dealt, and its source.attacker is who dealt it. A ret mixin sees both. When the attacker is a player, it heals them through the entity.living.heal invoker, so other mods’ heal rules apply too (Wrap, overwrite and invokers):
using Hurt = clight::mx::EntityLivingHurt;
using Heal = clight::mx::EntityLivingHeal;
keep(clight::mixin::inject<Hurt>(clight::at::ret, [](Hurt::Ci& ci) {
if (!ci.ret()) return; // no damage, no lifesteal
clight::Entity attacker = ci.args().source.attacker;
if (!attacker || !attacker.is_player()) return;
if (attacker == clight::Entity(cl_entity{ci.self()})) return; // hurting yourself doesn't count
Heal::Args a{sizeof(Heal::Args), 1.0f}; // half a heart
(void)clight::mixin::invoke(Heal::id, attacker.raw().h, &a, nullptr);
}), "lifesteal");
clight::Entity converts from the cl_entity in the damage source, and an empty entity is false.
Step 5: build and try it
> clight build
> clight check
> clight run
Open a survival world with cheats. Summon a few zombies next to each other (/summon minecraft:zombie ~2 ~ ~), get hurt a little, and fight with a sword: jump and hit for a critical hit, and watch your hearts come back with every hit. The zombies next to the one you hit don’t get swept.
Then type /clight mixins player.attack:
player.attack
value:crit_multiplier duelist priority 0 calls 4
point:before_sweep duelist priority 0 calls 6
/clight mixins conflicts should say no mixin conflicts.
The whole file
// Duelist: double critical hits, no sweep attacks and lifesteal.
#include <clight.hpp>
#include <vector>
class Mod final : public clight::Mod {
std::vector<clight::mixin::Handle> mixins_;
void keep(clight::Expected<clight::mixin::Handle> h, const char* what) {
if (h) mixins_.push_back(std::move(*h));
else clight::log::warn("Duelist: {} is not available ({})", what, static_cast<int>(h.error()));
}
public:
void on_construct() override {
using Attack = clight::mx::PlayerAttack;
using Hurt = clight::mx::EntityLivingHurt;
using Heal = clight::mx::EntityLivingHeal;
// critical hits: 2x instead of 1.5x
keep(clight::mixin::inject<Attack>(clight::at::value("crit_multiplier"), [](Attack::Ci& ci) {
ci.value<float>() = 2.0f;
}), "double crits");
// no sweep attack
keep(clight::mixin::inject<Attack>(clight::at::point("before_sweep"), [](Attack::Ci& ci) {
ci.cancel();
}), "no sweep");
// lifesteal: half a heart per hit a player lands
keep(clight::mixin::inject<Hurt>(clight::at::ret, [](Hurt::Ci& ci) {
if (!ci.ret()) return;
clight::Entity attacker = ci.args().source.attacker;
if (!attacker || !attacker.is_player()) return;
if (attacker == clight::Entity(cl_entity{ci.self()})) return;
Heal::Args a{sizeof(Heal::Args), 1.0f};
(void)clight::mixin::invoke(Heal::id, attacker.raw().h, &a, nullptr);
}), "lifesteal");
clight::log::info("Duelist loaded with {} mixins", mixins_.size());
}
};
CLIGHT_MOD(Mod)
Ideas
- Make lifesteal stronger with a better sword: read the held item with
attacker.inventory_get(attacker.selected_slot())(Players). - A
/duel offcommand that clearsmixins_(and with it every rule) and/duel onthat adds them again (Commands). - Shorter invulnerability after a hit (
value("invulnerability_ticks")) for fast combos.
Recap: keep handles in a member so the mixins live as long as the mod, use values and points for small precise changes, and invokers so your effects pass through other mods’ rules.