Files
SurvivorCore/docs/combat.md
T
Samuel LisonandClaude Opus 4.8 2c68bcc6b0 feat: mob & AI engine + combat (melee + ranged) (#17, #12, #14)
Mob & AI engine (#17): a `Mob` component + a `Mobs` FSM runtime
(idle/wander/chase/attack/flee/leash/death), faction-driven profiles
(hostile/passive/neutral), Humanoid-based mobs so combat/health/death share one
path, per-mob-type reactions, and a `Mobs` Config section.

Combat (#12, #14): server-authoritative melee + ranged reusing the v0.4.0 swing
pipeline; the `combat:hit`/`combat:kill` kill-event schema designed once. Bows use
a TCE-style aim (hold RMB to aim with an over-the-shoulder camera + crosshair and
charge ring, hold LMB to draw, release to fire); the server simulates the gravity
arc and sends the path so the client flies a cosmetic arrow along the real curve.
Arrows are their own configurable ammo item (weight/damage/curve/range/speed).

No-code: admin plugin gains Mob, Weapon and Arrow editors (+ "Add to World" /
"Tool model" drops); the engine loads Weapons/Arrows/Mobs from SurvivorCoreContent.

Fix: scope the orphan hotbar-pin sweep to the removed/consumed item, so consuming
one item never clears an unrelated hotbar-pinned weapon.

Docs: combat.md + mobs.md (with demo video); CHANGELOG Unreleased entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:18:51 +10:00

6.3 KiB
Raw Blame History

Combat (melee + ranged)

📹 Demo: melee hits, bows & arrows, and creating weapons/ammo/mobs in the admin plugin

Combat (src/systems/Combat.luau) is server-authoritative and reuses the v0.4.0 client-input → RemoteEvent → server-validated-hit pipeline (the same shape as harvesting). The client only requests an attack; the server validates everything — equipped weapon, range, line-of-sight, cooldown — and decides the hit. It never trusts the client.

Targets are anything with a Humanoid: engine mobs (always damageable) and, when Config "Combat".FriendlyFire is on, other players. Damage flows through Humanoid:TakeDamage, so a player victim's survival Health HUD updates with no extra wiring and death/respawn is clean.

Weapons are items

A weapon is just an Items def with category = "weapon", a toolType (so the hotbar→Tool bridge equips it as a real Tool), and flat weapon* fields. Flat attributes keep weapons fully no-code-authorable (the admin plugin's Weapons editor writes exactly these):

SurvivorCore.Items.register({
    id = "wood_club",
    name = "Wooden Club",
    category = "weapon",
    toolType = "club",        -- lets the hotbar equip it
    weaponKind = "melee",     -- "melee" | "bow"
    weaponDamage = 20,
    weaponRange = 8,          -- melee reach (server-validated)
    weaponCooldown = 0.6,
})

SurvivorCore.Items.register({
    id = "short_bow",
    name = "Short Bow",
    category = "weapon",
    toolType = "bow",
    weaponKind = "bow",
    weaponDamage = 34,            -- base; multiplied by the arrow's ammoDamage and the draw
    weaponDrawTime = 1,           -- seconds to a full-power draw (the bow's "pullback")
    weaponProjectileSpeed = 200,
    weaponMaxRange = 260,         -- studs (fallback if the arrow sets no ammoRange)
    weaponAmmo = "arrow",         -- the arrow id consumed per shot ("" = no ammo)
})

Melee

Equip a melee weapon from the hotbar and click. The client (CombatInput) asks the server to swing; the server finds the nearest valid target within weaponRange (falling back to Combat.MeleeRange) with line-of-sight and applies weaponDamage. A registered swing animation (Assets "WeaponAnims" by toolType) plays client-side.

Ranged (bow) — the aiming flow

Bows use a TCE-style two-button aim (CombatInput):

  1. Hold right-click to aim — the camera shifts over-the-shoulder (Humanoid.CameraOffset) and zooms (FOV), and a crosshair + charge ring appear.
  2. Hold left-click to draw — the ring fills and greens up over weaponDrawTime.
  3. Release left-click to fire along the crosshair; release right-click exits aim.

The client sends the world point under the crosshair (a camera-centre ray); the server recomputes the true direction from the bow's own muzzle (killing shoulder-camera parallax and any spoofed direction), times the draw itself (anti-cheat), consumes one weaponAmmo arrow from the inventory (no arrows → no shot), and simulates the arrow authoritatively — a gravity arc stepped as short raycasts. It sends back the arc's path, and the client flies a cosmetic arrow along that exact curve. The melee and bow paths route by the equipped Tool's WeaponKind, so they coexist with harvesting without ever double-firing.

Arrows — configurable, weighted ammo

An arrow is its own item (category = "ammo"), authored in code or via the admin Arrows / Ammo editor — so a game can ship many arrow types with different ballistics. A shot combines the bow's pullback with the arrow's stats and the draw strength:

Arrow field Effect
ammoDamage × the bow's weaponDamage (e.g. 1.4 = +40%)
ammoDrop × gravity — the curve; heavier arrows (> 1) arc steeply and fall short
ammoRange caps the flight in studs (0 = use the bow's weaponMaxRange)
ammoSpeed × the bow's projectile speed (0 = use the bow's speed)
weight carry weight per arrow (the inventory cost of ammo)
SurvivorCore.Items.register({
    id = "heavy_arrow", name = "Heavy Arrow", category = "ammo",
    weight = 0.12, stack = 16,
    ammoDamage = 1.4,   -- hits harder
    ammoDrop = 2,       -- doubles gravity → a pronounced arc
    ammoSpeed = 0.85,   -- a touch slower
    ammoRange = 140,    -- short effective range
})

Final shot = damage = weaponDamage × ammoDamage × draw, speed = (bow speed × ammoSpeed) × draw, gravity = Combat.Bow.Gravity × ammoDrop, range = ammoRange or weaponMaxRange or Combat.Bow.MaxRange. Draw strength scales from Combat.Bow.MinDrawDamageMult (no draw) to full (full draw). Point a bow's weaponAmmo at any arrow id to set its ammo.

The kill-event schema (designed once)

Both paths fire the same schema, through Hooks (engine extension) and EventBridge (analytics / quests / achievements):

Event Payload
combat:hit { attacker, victim, weapon, source, damage, victimHpLeft }
combat:kill { attacker, victim, weapon, source }source = "melee" | "bow"

This replaces the scattered, content-specific kill events a survival game tends to grow (zombie_killed / animal_killed / pvp_kill / bow_kill / …) with one schema every consumer subscribes to. A mob's own death also fires the lifecycle mob:died (see mobs.md) — the two are complementary: combat:kill is attacker-attributed, mob:died is the creature lifecycle.

SurvivorCore.Hooks.on("combat:kill", function(ctx)
    if ctx.source == "bow" then awardArcheryXp(ctx.attacker) end
end)

Runtime & tuning

SurvivorCore.Combat boots with the engine (after Inventory + Mobs). Tuning:

Config.override("Combat", {
    MeleeRange = 8, MeleeCooldown = 0.6, RequireLineOfSight = true, FriendlyFire = false,
    Bow = { Gravity = 80, ProjectileSpeed = 180, MaxRange = 300, MinDrawDamageMult = 0.3, StepSize = 4 },
})

Per-weapon weapon* values override these fallbacks. Out of scope: durability, blocking/parrying, and AoE are creator content via the hooks above; mob AI lives in mobs.md.