Files

6.2 KiB

Mobs & AI

📹 Demos: creating mobs & fleeing hunt-NPCs, plus combat and the admin plugin · hunting a boar & butchering the carcass

The mob & AI engine (src/systems/Mobs.luau) is the shared, content-free creature substrate that combat (#12), animals (#13) and monsters (#14) all build on.

The key idea: a mob is a Humanoid. Concretely, a mob is a Model tagged Mob that contains a Humanoid and a PrimaryPart (its HumanoidRootPart). Because of that, a mob is damaged, healed and killed exactly like a player — Humanoid:TakeDamage, Humanoid.Health, Humanoid.Died — so combat has one code path for players and mobs, and there are no bespoke "mob HP" systems to keep in sync.

The engine ships zero creatures. You supply the rigged model (or let the engine build a blocky placeholder), the Mobs def (stats), and the death/spawn juice via reactions.

Defining a mob type

A mob's stats come from a Mobs registry def — in code or no-code via the admin plugin:

SurvivorCore.Mobs.register({
    id = "husk",
    faction = "hostile",   -- "hostile" | "passive" | "neutral"  (picks the AI profile)
    health = 60,
    walkSpeed = 5,         -- idle/wander speed
    runSpeed = 15,         -- chase / flee speed
    aggroRange = 40,       -- how close a player must be to be noticed (hostile) or fled from (passive)
    leashRange = 70,       -- studs from spawn before a chaser gives up and returns
    attackRange = 6,       -- melee reach of the mob's own attack (hostile)
    attackDamage = 8,
    attackCooldown = 1.5,
})

Anything you leave out falls back to the Mobs Config defaults (see Tuning). Every field also has a Config default, so a minimal def is just { id, faction }.

The behavior profile is data

The FSM is one state machine; faction selects how it behaves — you don't subclass or script it:

Faction Behavior
hostile idle/wander near spawn → chase a player in aggroRange (with line-of-sight) → attack within attackRangereturn to spawn if it's dragged past leashRange.
passive idle/wander → flee from a player who comes within aggroRange (or who strikes it).
neutral idle/wander only; flees briefly when struck.

Placing a mob in the world

Two ways, and they coexist:

1. Tag a model (no-code). Build/import any rigged Model with a Humanoid + PrimaryPart, tag it Mob (CollectionService), and set one attribute:

Attribute Meaning
MobType the Mobs def id to inherit from (e.g. "husk"); blank = the model's Name

Per-instance attributes (Faction, Health, WalkSpeed, RunSpeed, AggroRange, LeashRange, AttackRange, AttackDamage, AttackCooldown, WanderRadius) override the def for that one mob. (The admin plugin's Mobs editor writes the def and its + Add to World button drops a tagged placeholder rig for you.)

2. Spawn from code (SurvivorCore.Mobs, available after start()):

Function Behavior
spawn(mobType, cframe, opts?) -> Model clone a creator template (SurvivorCoreContent.MobModels.<mobType>) or build a placeholder, tag + adopt it. opts.respawn = true re-spawns it on death after opts.respawnSeconds (default Config).
adopt(model) bring an existing tagged Model under AI control (idempotent).
damage(model, amount, source?) apply damage to a mob (combat uses this; records the attacker + fires mob:hit).
getActive() -> { Model } · isMob(model) -> bool the live roster / a membership test.
SurvivorCore.Mobs.spawn("husk", CFrame.new(40, 5, 20), { respawn = true })

Hunting & butchering

Give a mob def carcass fields and slaying it leaves a butcherable carcass — which is simply a tagged Gatherable node, so butchering reuses the whole harvesting pipeline (tool gate, per-hit yields, floating HP bar, gather:* hooks, progression counters):

SurvivorCore.Mobs.register({
    id = "boar", faction = "passive", health = 40,
    carcassItem = "raw_meat",   -- blank = no carcass on death
    carcassHp = 3,              -- butcher interactions to deplete
    carcassTool = "knife",      -- tool type required ("" = bare-hand)
    carcassYieldMin = 1,
    carcassYieldMax = 2,
    -- carcassSeconds = 120,    -- lifetime before despawn (0 = stays until depleted)
})

The carcass uses a creator template (SurvivorCoreContent.Carcasses.<mobType>) or a plain placeholder, and its prompt reads "Butcher — carcass". Per-type juice keys on "<mobType>_carcass":

SurvivorCore.Gather.onReaction("boar_carcass", "depleted", function(ctx) scatterBones(ctx.position) end)

Butcher counters (gathers_raw_meat, …) feed quests and achievements automatically. All six fields are editable in the admin plugin's Mobs editor.

Reactions (the juice)

Global Hooks.on("mob:died", …) fire for every mob. For behavior tied to one mob type, use the reaction API — no core edits:

SurvivorCore.Mobs.onReaction("husk", "died", function(ctx)
    -- ctx = { instance, mobType, killer?, position? }
    fadeCorpse(ctx.instance) -- creator content; the engine just removes the body after CorpseSeconds
end)

Events: "spawned" · "hit" · "attack" (the mob hit a player) · "died". Anims/sounds are content-free — register them in Assets under MobAnims / MobSounds, keyed mobType.."_"..state (e.g. husk_attack); the FSM plays them if present.

Tuning

Config.override("Mobs", { TickRate = 0.2, DefaultAggroRange = 40, DefaultLeashRange = 60, DefaultAttackRange = 6, DefaultAttackDamage = 8, DefaultAttackCooldown = 1.5, RequireLineOfSight = true, RespawnSeconds = 30, CorpseSeconds = 5 }) — these are the fallbacks a def (or per-mob attribute) overrides.

Out of scope (for now)

Pathfinding (mobs walk straight via Humanoid:MoveTo — open terrain), butchering carcasses into yields (#13), and spawn-zone scattering (#16) are follow-ups that build on this substrate. Combat (#12) is its first consumer — see combat.md.