species
Server
modkit.species manages custom playable species. A custom species is a host species from the game with a new look, new values and its own attacks. The player controls a real host dinosaur, so movement, swimming, hunger, growth, saving and the game's own rules stay as they are. The client mod hides the host model and shows the custom model instead. The server applies the values and runs the attacks. The custom species guide walks through the whole flow.
Definitions come from two places. The Server Tool writes species\<name>\species.json under the server root, one folder per species, and builds the model into client-mods\species_<name>.pak. Scripts define a species with modkit.species.define. A script definition replaces a file definition of the same name while the server runs. The plugin looks at the files every 5 seconds and loads them again when they changed. Values and attacks apply right away. New models reach a player on the next join, because the launcher downloads the pak files before the game starts.
Species names have 1 to 32 characters from a-z, 0-9 and _.
Definition fields
Every field is optional except host. A missing field has its default. Distances are Unreal units (100 per metre), times are seconds, shares go from 0 to 1, angles are degrees.
| Field | Type | Default | Description |
|---|---|---|---|
label |
string | the name | display name |
host |
string | required | key of the game species the player really plays, from modkit.ai.species() |
enabled |
boolean | true | a disabled species can not be assigned and is not sent to the clients |
disableHostAttacks |
boolean | false | damage from the host's own attacks is dropped. Use it when the species has custom attacks only. |
source |
table | data of the asset pipeline. Kept as it is, not sent to the clients. |
visual
| Field | Default | Range | Description |
|---|---|---|---|
scale |
1 | 0.05 to 20 | size of the custom model relative to the host at full growth |
offset |
{0, 0, 0} |
-5000 to 5000 each | shift of the model on the host |
yaw |
0 | -360 to 360 | turns the model when it faces the wrong way |
hideHost |
true | hides the host model | |
attachTo |
"capsule" |
"capsule" or "mesh" |
|
castShadow |
true |
animations holds one entry per role. A role is a short id of an animation (idle = "idle"), a table { anim = "idle", rate = 1, loop = true }, or a blend space { blend = { { anim = "swim", x = 0, y = 0 }, ... }, rate = 1 } with up to 32 samples. x runs from -1 (turning left) to 1 (turning right), y from -1 (diving) to 1 (climbing). rate goes from 0.05 to 10.
A blend role plays the sample closest to the current turn and pitch. The client switches to another sample only when it is clearly closer and the current one has played for at least 0.3 seconds, and the new sample continues at the same point of its cycle, so a stroke does not restart. When the built blend space asset carries blend data, the client blends the samples smoothly instead.
| Role | When |
|---|---|
idle |
standing on land |
walk, trot, run |
moving on land by speed. A missing role falls back to the next slower one. |
swimIdle |
in water without speed, falls back to idle |
swim |
in water at normal speed, falls back to swimIdle |
swimFast |
in water above the swimFast threshold, falls back to swim |
swimBack |
swimming backwards, falls back to swim |
hit |
got hit, plays once |
death |
died, plays once, then the last frame holds or deathPose loops |
deathPose |
loop after death |
rest, eat, drink, call |
optional, otherwise idle |
thresholds sets the speeds in units per second at which the roles switch: { trot = 350, run = 700, swimFast = 900 }, each from 1 to 10000. A missing threshold comes from the host's movement values.
stats: a number replaces the host value, { factor = 1.5 } multiplies it.
| Field | As number | As factor | Description |
|---|---|---|---|
health |
1 to 1000000 | 0.01 to 100 | maximum health |
damageTaken |
0 to 100 | share of incoming damage. A plain number is read as a factor too. | |
walkSpeed, runSpeed |
1 to 20000 | 0.05 to 10 | land speeds. runSpeed scales the trot speed along with it. A plain number applies at every growth stage. |
swimSpeed, swimFastSpeed |
1 to 20000 | 0.05 to 10 | water speeds |
stamina |
1 to 1000000 | 0.01 to 100 | maximum stamina |
growthSeconds |
60 to 2592000 | 0.01 to 100 | time from hatchling to adult. Applied through the growth control, so player:setGrowthMultiplier overrides it. |
hungerRate, thirstRate |
0 to 100 | decay of hunger and thirst. A plain number is read as a factor too. |
habitat
| Field | Default | Range | Description |
|---|---|---|---|
water |
true | may be in water | |
land |
true | may be on land | |
outOfHabitatDamage |
0 | 0 to 1 | damage per second outside the habitat as a share of maximum health. 0 turns it off. |
outOfHabitatGrace |
10 | 0 to 3600 | seconds before the damage starts |
attacks holds up to 16 entries.
| Field | Default | Range | Description |
|---|---|---|---|
id |
required | 1 to 32 characters from a-z, 0-9 and _ |
|
label |
the id | ||
source |
"custom" |
"custom" for a modkit attack, "host:<ability>" to give a host attack a new animation. <ability> is a piece of the ability's class name. For a host attack only animation, animationLeft, animationRight and rate count. |
|
key |
"" |
Unreal key name, for example "LeftMouseButton", "RightMouseButton", "E", "SpaceBar". The client mod binds the key while the player has this species. |
|
animation |
"" |
short id of the attack animation | |
animationLeft, animationRight |
"" |
played instead of animation while the player turns left or right |
|
rate |
1 | 0.05 to 10 | play rate of the animation |
damage |
0 | 0 to 1000000 | |
staminaCost |
0 | 0 to 1000000 | taken when the attack starts |
cooldown |
1 | 0 to 3600 | seconds until the same attack may start again |
hit.time |
0.3 | 0 to 30 | seconds after the start until the hit window opens |
hit.duration |
0.15 | 0.01 to 10 | length of the hit window |
hit.shape |
"cone" |
"cone" uses range, angle and height. "sphere" uses range as the distance of the centre in front of the attacker and radius. |
|
hit.range |
400 | 0 to 20000 | |
hit.angle |
60 | 1 to 360 | |
hit.radius |
200 | 0 to 20000 | |
hit.height |
300 | 0 to 20000 | vertical tolerance of the cone |
move |
"free" |
"free", "slow" (half speed until the hit window closes) or "locked" |
|
allowIn |
{ "water", "land" } |
where the attack may start | |
minGrowth |
0 | 0 to 1 | |
targets.players, targets.ai |
true, true | AI targets are animals with a handle, see ai | |
knockback |
0 | 0 to 100000 | launch strength away from the attacker |
access
| Field | Default | Range | Description |
|---|---|---|---|
permission |
"" |
acl permission a player needs. Empty means everyone. | |
maxPlayers |
0 | 0 to 1000 | how many players may have the species at the same time. 0 means no limit. |
How an attack runs
- The player presses the key. The client mod sends the request to the server and plays nothing yet.
- The server checks the species, the growth, the cooldown, the stamina, that the player is alive and not in another attack, and
allowInagainst the water. customAttackfires. A handler that returnsfalsecancels the attack.- The stamina is taken, the cooldown starts, and every player within 600 m receives the animation cue.
- From
hit.timeon, forhit.duration, the server tests the shape against the positions and facing of the players and the animals with a handle. Every victim is hit once per attack. Damage goes through the normal damage path, so god mode and damage free zones apply. A victim with a custom species takesdamageTakentimes the damage. customAttackHitfires for every hit.
A host attack with source = "host:<ability>" keeps the game's own hit detection and damage. The server only tells the clients which animation to play when the ability fires.
modkit.species.list
modkit.species.list()
Returns: the names of all species, sorted.
modkit.species.get
modkit.species.get(name)
Returns: the definition with every field filled in, defaults included, or nil.
local mosa = modkit.species.get("mosasaurus")
print(mosa.label, mosa.host, #mosa.attacks)
modkit.species.reload
modkit.species.reload()
Loads every species.json again right away. A file with broken JSON is skipped with a log line, and the definition from before stays. A bad value in a file is replaced by its default, and a log line names the field.
Returns: true.
modkit.species.define
modkit.species.define(name, definition)
Defines a species for as long as the server runs. Nothing is written to disk. The model and the animations must already be in a pak under client-mods, named after the species. An unknown field, a wrong type, a value outside its range, a missing host or a bad name raises a Lua error that names the field.
Returns: true.
modkit.species.define("night_deino", {
label = "Night Deinosuchus",
host = "deinosuchus",
stats = { health = { factor = 1.5 }, swimSpeed = { factor = 1.2 } },
habitat = { land = false, outOfHabitatDamage = 0.02, outOfHabitatGrace = 15 },
access = { permission = "species.night" },
})
modkit.species.players
modkit.species.players(name)
Returns: array of player tables, the online players that have this species.
player:setCustomSpecies
player:setCustomSpecies(name [, force])
Gives the player the species, or removes it with nil. When the player plays another species than the host right now, the server first turns them into the host with player:setSpecies, which means a fresh dinosaur with full health and no skin. The values and attacks follow on the next spawn.
Without force the access rules apply: the permission and maxPlayers. force = true skips both.
The assignment is kept per Steam ID in data\species\players.json and applied again after every spawn as the host species. A spawn as another species removes the assignment. Parking in the garage stores the species with the dinosaur, and a restore brings it back.
Returns: true, or false and one of "unknown species", "species is disabled", "no permission", "species is full", "could not switch to the host species: ...".
modkit.commands.add("species", function(player, name)
if not name then return player:message("Usage: /species <name> or /species none") end
local ok, why = player:setCustomSpecies(name ~= "none" and name or nil)
player:message(ok and "Done." or why)
end)
player:getCustomSpecies
player:getCustomSpecies()
Returns: the species name, or nil.
The player table also carries customSpecies with the same value.
Events
| Event | Arguments | When |
|---|---|---|
customSpeciesChanged |
player, from, to |
the species of a player was set or removed. from and to are names or nil. |
customAttack |
player, attackId |
a custom attack is about to start. Return false to cancel it. |
customAttackHit |
player, attackId, victim, damage |
a custom attack hit. victim is a player table or { kind = "ai", id = 12 }. |
modkit.events.add("customAttack", function(player, attackId)
for _, zone in ipairs(modkit.zones.at(player.x, player.y, player.z)) do
if zone == "sanctuary" then return false end
end
end)
modkit.events.add("customAttackHit", function(player, attackId, victim, damage)
if victim.steamId then victim:message(player.name .. " hit you for " .. damage) end
end)