Learn how to customize the player, students, routines, hairstyles, reactions, textures, the environment — and write Lua scripts for new mechanics.
The player is configured in player.json. This file controls the player character's base appearance and starting stats.
{
"player": "female",
"reputation": 0,
"breastSize": 0.75,
"hairstyleId": 4
}
male, female0.75)Students are defined in students.json. Each entry includes identity, appearance, routine, and social attributes. Both male and female students are fully supported. You can freely add new students by creating new entries with unique IDs, or remove existing ones by deleting their entry — the game will load exactly whoever is in the array.
{
"id": 1,
"name": "Haruto Katou",
"gender": "Male",
"type": "Student",
"club": "Computer_Science",
"socialReputation": 35,
"loveTargetID": -1,
"faceMaterial": "Male/MaleFace_Red",
"hairMaterial": "Male/MaleHair_Red",
"hairstyleID": 0,
"chestSize": 0,
"routineID": "1"
}
Male or FemaleStudent or Teacher-1 for noneMale/MaleFace_Red, Female/FemaleFace_Black)Male/MaleHair_Green, Female/FemaleHair_Yellow)0 for male charactersMaterial paths now use gender-based subfolders. Use Male/ for male characters and Female/ for female characters.
Male/MaleFace_[Color] and Male/MaleHair_[Color]Female/FemaleFace_[Color] and Female/FemaleHair_[Color]NewFemaleHair_Blue.pngMaleFace_Standard). This may be unified in a future update.
Hairstyle shapes are defined in hairstyles.json. Each hairstyle has an id and a list of parts, each describing a hair bone's local transform. The hairstyleID field in students.json and player.json references these IDs.
{
"hairstyles": [
{
"id": 0,
"parts": [
{
"objectName": "FrontHair",
"localPosition": { "x": 0.0, "y": -0.08, "z": 0.13 },
"localRotation": { "x": 0.0, "y": 0.0, "z": 0.0 },
"localScale": { "x": 1.0, "y": 1.0, "z": 1.0 },
"children": []
}
]
}
]
}
BackHair1, FrontHair, LeftHair1, LeftHair2, LeftHair3, RightHair1, RightHair2, RightHair3{ "x": 0, "y": 0, "z": 0 } to hide a partRoutines are defined in routines.json. Each entry includes time, destination, pause duration, look direction, and separate animations for movement and arrival.
{
"hour": 7,
"minute": 50,
"isPM": false,
"destination": "Sit 1 1",
"pauseSeconds": 0,
"lookDirectionName": "East",
"animationOnMove": "none",
"animationOnArrival": "sit"
}
isPM: true for afternoon)North, South, East, WestReaction messages are defined in reactions.json. These are the dialogue lines NPCs speak when they notice suspicious behavior from the player.
{
"bloodMessage": "Why are you covered in blood?!",
"weaponMessage": "Why are you carrying a weapon?",
"bloodAndWeaponMessage": "What the hell happened to you?!",
"professorMessage": "What are you doing? Stop right now!",
"corpseMessage": "Oh no... there's a body here!"
}
Student and Teacher — both fully functionalMale and Female are supported for both Students and TeachersNone, Cooking, Drama, Sports, Science, Occult, Computer_ScienceObject placement in the environment is defined in objects.json. This file covers the entire scene — any object in the game world can be repositioned, rotated, or rescaled. Just use the exact in-engine object name and the game will apply your transforms automatically.
{
"objects": [
{
"name": "Name1",
"position": { "x": 1, "y": 2, "z": 3 },
"rotation": { "x": 0, "y": 90, "z": 0 },
"scale": { "x": 2, "y": 2, "z": 2 }
}
]
}
Currently, only specific textures can be replaced via StreamingAssets:
Destination names in routines must match exactly (capitalization matters):
SchoolDop supports Lua mods via the MoonSharp interpreter. Lua scripts can react to game events, spawn and manipulate objects, control NPCs, set up proximity triggers, and run timed logic — all without recompiling the game.
Place all .lua files inside the StreamingAssets/Scripts/ folder. The game loads every file in that folder alphabetically at startup. Custom 3D models (AssetBundles) go in StreamingAssets/Bundles/.
00_init.lua, 01_mymod.lua to control load order when multiple scripts depend on each other.
Define any of the following global functions in your Lua file and the game will call them automatically:
| Hook | When it's called | Arguments |
|---|---|---|
| OnStart() | Once, right after all mods are loaded | — |
| OnUpdate() | Every frame | — |
| OnPlayerUpdate(player) | Every frame (only when the player exists) | player — the player GameObject |
| OnNPCUpdate(npc) | Every frame, once per NPC | npc — an NPC GameObject |
| OnCustomEvent(name, ...) | When the game fires a custom C# event | name (string) + optional extra args |
-- minimal example
function OnStart()
Game:Print("Mod loaded!")
end
function OnPlayerUpdate(player)
if Game:IsPlayerRunning() then
Game:Print("Player is running!")
end
end
OnUpdate, OnPlayerUpdate, and OnNPCUpdate run every single frame. Avoid expensive operations (object searches, heavy math) inside them. Prefer timers or proximity triggers for infrequent logic.
All functionality is exposed through the global Game object. Below is a full reference grouped by category.
Create Unity value types from Lua.
| Function | Returns | Description |
|---|---|---|
| Game:NewVector3(x, y, z) | Vector3 | Creates a 3D position/direction value |
| Game:NewColor(r, g, b, a) | Color | Creates an RGBA color (values 0–1) |
| Game:NewRotation(x, y, z) | Quaternion | Creates a rotation from Euler degrees |
Write to the Unity console. Useful for debugging your mod.
| Function | Returns | Description |
|---|---|---|
| Game:Print(msg) | void | Log an info message (prefixed with [Lua]) |
| Game:Warn(msg) | void | Log a warning message |
| Game:Error(msg) | void | Log an error message |
Create primitives, empty containers, or load custom 3D models from AssetBundles.
| Function | Returns | Description |
|---|---|---|
| Game:SpawnCube(pos, scale, color) | GameObject | Spawns a colored cube primitive |
| Game:SpawnSphere(pos, scale, color) | GameObject | Spawns a sphere primitive |
| Game:SpawnCapsule(pos, scale, color) | GameObject | Spawns a capsule primitive |
| Game:SpawnCylinder(pos, scale, color) | GameObject | Spawns a cylinder primitive |
| Game:SpawnPlane(pos, scale, color) | GameObject | Spawns a flat plane primitive |
| Game:CreateEmpty(name, position) | GameObject | Creates a named empty object at a position |
| Game:CreateEmpty(name) | GameObject | Creates a named empty object at origin |
| Game:LoadModel(bundle, asset, pos) | GameObject | Instantiates a prefab from an AssetBundle in StreamingAssets/Bundles/ |
| Game:LoadModelRotated(bundle, asset, pos, eulerRot) | GameObject | Same as above with an initial rotation |
Locate GameObjects already present in the scene.
| Function | Returns | Description |
|---|---|---|
| Game:Find(name) | GameObject | Finds a scene object by exact name |
| Game:FindByTag(tag) | List | Returns all objects with a given Unity tag |
| Game:GetPlayer() | GameObject | Returns the player's GameObject |
| Game:GetAllNPCs() | List | Returns a list of all NPC GameObjects |
| Game:GetAllNPCsInRange(center, range) | List | Returns NPCs within range units of center |
Read and write position, rotation, scale, and parent-child hierarchy.
| Function | Returns | Description |
|---|---|---|
| Game:SetPosition(obj, pos) | void | Teleports an object to a world position |
| Game:GetPosition(obj) | Vector3 | Returns the object's world position |
| Game:SetRotation(obj, euler) | void | Sets rotation from Euler degrees (Vector3) |
| Game:SetScale(obj, scale) | void | Sets the object's local scale |
| Game:LookAt(obj, target) | void | Rotates obj to face another GameObject |
| Game:LookAtPosition(obj, pos) | void | Rotates obj to face a world position |
| Game:GetForward(obj) | Vector3 | Returns the object's forward direction vector |
| Game:SetParent(child, parent) | void | Parents child under parent (keeps world position) |
| Game:Unparent(obj) | void | Detaches an object from its parent |
Measure distances between objects or positions.
| Function | Returns | Description |
|---|---|---|
| Game:Distance(a, b) | float | Distance between two GameObjects |
| Game:DistanceV3(a, b) | float | Distance between two Vector3 positions |
| Game:IsNear(a, b, maxDist) | bool | True if two objects are within maxDist |
| Game:IsPlayerNear(obj, maxDist) | bool | True if the player is within maxDist of obj |
| Game:DirectionTo(from, to) | Vector3 | Normalized direction vector from one object to another |
Add or retrieve Unity components on any GameObject.
| Function | Returns | Description |
|---|---|---|
| Game:AddRigidbody(obj, useGravity) | Rigidbody | Adds a physics Rigidbody |
| Game:GetRigidbody(obj) | Rigidbody | Gets the existing Rigidbody |
| Game:AddBoxCollider(obj, center, size) | BoxCollider | Adds a box collider |
| Game:AddSphereCollider(obj, radius) | SphereCollider | Adds a sphere collider |
| Game:AddCapsuleCollider(obj, radius, height) | CapsuleCollider | Adds a capsule collider |
| Game:AddTrigger(obj, center, size) | BoxCollider | Adds a box collider set as trigger (isTrigger = true) |
| Game:AddNavMeshAgent(obj, speed, radius) | NavMeshAgent | Adds a NavMesh navigation agent |
| Game:GetNavMeshAgent(obj) | NavMeshAgent | Gets the existing NavMeshAgent |
| Game:SetNavDestination(obj, dest) | void | Moves an agent toward a world position |
| Game:StopNav(obj) | void | Stops the agent and clears its path |
| Game:AddAnimator(obj) | Animator | Adds an Animator component |
| Game:GetAnimator(obj) | Animator | Gets the existing Animator |
Add dynamic lights to any GameObject.
| Function | Returns | Description |
|---|---|---|
| Game:AddLight(obj, type, color, intensity, range) | Light | Adds a light. type accepts: "point", "spot", "directional", "area" |
Modify the visual appearance of objects at runtime.
| Function | Returns | Description |
|---|---|---|
| Game:SetColor(obj, color) | void | Sets the main albedo color of an object's material |
| Game:SetMaterial(obj, color, metallic, smoothness) | void | Replaces the material with a new Standard shader material |
| Game:SetEmissive(obj, color) | void | Enables emission and sets the glow color |
Drive animation state machines by reading and writing Animator parameters.
| Function | Returns | Description |
|---|---|---|
| Game:SetAnimBool(obj, param, value) | void | Sets a boolean parameter |
| Game:SetAnimFloat(obj, param, value) | void | Sets a float parameter |
| Game:SetAnimInt(obj, param, value) | void | Sets an integer parameter |
| Game:SetAnimTrigger(obj, param) | void | Fires a trigger parameter |
| Game:GetAnimBool(obj, param) | bool | Returns the current value of a boolean parameter |
| Game:GetAnimFloat(obj, param) | float | Returns the current value of a float parameter |
Interact with the NPC routine and reaction systems.
| Function | Returns | Description |
|---|---|---|
| Game:PauseNPC(obj) | void | Temporarily freezes an NPC's routine |
| Game:ResumeNPC(obj) | void | Resumes a paused NPC's routine |
| Game:StopNPC(obj) | void | Permanently stops an NPC's routine for the session |
| Game:IsNPCPanicked(obj) | bool | Returns true if the NPC is currently panicked |
| Game:IsNPCReacting(obj) | bool | Returns true if the NPC is currently reacting to something |
Read the current state of the player character. These are read-only queries.
| Function | Returns | Description |
|---|---|---|
| Game:IsPlayerRunning() | bool | True if the player is currently running |
| Game:IsPlayerCrouching() | bool | True if the player is currently crouching |
| Game:GetPlayerSpeed() | float | Returns the player's current movement speed (m/s) |
Apply forces and perform raycasts against the scene geometry.
| Function | Returns | Description |
|---|---|---|
| Game:AddForce(obj, force) | void | Applies an impulse force to an object's Rigidbody |
| Game:Raycast(origin, direction, maxDist) | bool | Returns true if the ray hits any collider |
| Game:RaycastGetObject(origin, direction, maxDist) | GameObject | Returns the first GameObject hit by the ray, or nil |
| Function | Returns | Description |
|---|---|---|
| Game:Destroy(obj) | void | Removes an object from the scene immediately |
| Game:DestroyAfter(obj, seconds) | void | Removes an object after a delay |
Schedule code to run after a delay or on a repeating interval.
| Function | Returns | Description |
|---|---|---|
| Game:Wait(seconds, callback) | void | Calls callback once after seconds |
| Game:SetInterval(interval, callback) | int | Calls callback every interval seconds. Returns a timer ID |
| Game:CancelTimer(id) | void | Stops a repeating timer by its ID |
-- run something after 3 seconds
Game:Wait(3.0, function()
Game:Print("3 seconds passed!")
end)
-- repeat every 5 seconds, then stop after 30s
local myTimer = Game:SetInterval(5.0, function()
Game:Print("tick")
end)
Game:Wait(30.0, function()
Game:CancelTimer(myTimer)
Game:Print("interval stopped")
end)
Register callbacks that fire automatically when the player or an NPC enters a radius around a target object.
| Function | Returns | Description |
|---|---|---|
| Game:OnPlayerNear(target, radius, callback) | void | One-shot: fires once when the player enters radius around target, then unregisters itself |
| Game:OnPlayerNearContinuous(target, radius, callback) | void | Continuous: fires every frame while the player is within radius |
| Game:OnPlayerEnterExit(target, radius, onEnter, onExit) | void | Fires onEnter when the player enters and onExit when they leave. Either argument can be nil |
| Game:OnNPCNear(target, radius, callback) | void | Fires once per unique NPC that enters radius around target |
function OnStart()
local marker = Game:SpawnCube(
Game:NewVector3(5, 0, 10),
Game:NewVector3(0.5, 0.5, 0.5),
Game:NewColor(1, 0, 0, 1)
)
-- one-shot: fires once when player steps near
Game:OnPlayerNear(marker, 2.0, function()
Game:Print("Player reached the marker!")
Game:Destroy(marker)
end)
-- enter / exit zone
local zone = Game:Find("GardenArea")
Game:OnPlayerEnterExit(zone, 5.0,
function() Game:Print("Entered garden") end,
function() Game:Print("Left garden") end
)
end
Miscellaneous helper functions.
| Function | Returns | Description |
|---|---|---|
| Game:Random(min, max) | float | Random float in [min, max] |
| Game:RandomInt(min, max) | int | Random integer in [min, max) |
| Game:Time() | float | Seconds since the game started (Time.time) |
| Game:DeltaTime() | float | Time elapsed since last frame (Time.deltaTime) |
| Game:RandomPointAround(center, radius) | Vector3 | Random position on the XZ plane within radius of center |
| Game:IsNull(obj) | bool | Safe null-check for GameObjects (use instead of obj == nil) |
| Game:GetName(obj) | string | Returns the object's name |
| Game:SetName(obj, name) | void | Renames the object |
| Game:SetActive(obj, active) | void | Enables or disables an object in the scene |
| Game:IsActive(obj) | bool | Returns true if the object is active in the hierarchy |
Game:IsNull(obj) to test if a GameObject is valid. In MoonSharp, a destroyed C# object is not nil in Lua — it is a live reference to a dead C# object, so obj == nil will return false even after Game:Destroy(obj).
The following actions are not possible with the current Lua API:
Game:Wait() and Game:SetInterval() instead-- example_mod.lua
-- Spawns a glowing orb. When the player gets close it explodes
-- into smaller spheres and logs a message every 10 seconds.
local orb
local tickTimer
function OnStart()
-- spawn the orb above the garden
local pos = Game:NewVector3(0, 1.5, 8)
local scale = Game:NewVector3(0.6, 0.6, 0.6)
local color = Game:NewColor(0.2, 0.8, 1, 1)
orb = Game:SpawnCube(pos, scale, color)
Game:SetEmissive(orb, Game:NewColor(0, 0.5, 1, 1))
Game:SetName(orb, "GlowOrb")
-- start a repeating log
tickTimer = Game:SetInterval(10.0, function()
Game:Print("Orb is still alive at t=" .. Game:Time())
end)
-- explode when the player steps within 2 units
Game:OnPlayerNear(orb, 2.0, function()
Explode()
end)
end
function Explode()
if Game:IsNull(orb) then return end
local center = Game:GetPosition(orb)
Game:Destroy(orb)
Game:CancelTimer(tickTimer)
-- spawn 6 small debris spheres
for i = 1, 6 do
local offset = Game:RandomPointAround(center, 1.5)
local debris = Game:SpawnSphere(
offset,
Game:NewVector3(0.2, 0.2, 0.2),
Game:NewColor(Game:Random(0,1), Game:Random(0,1), Game:Random(0,1), 1)
)
Game:DestroyAfter(debris, 3.0)
end
Game:Print("Orb exploded!")
end
NewFemaleHair_Cyan.png)chestSize: 0 for all male charactersOnUpdate / OnNPCUpdate — prefer Game:SetInterval() for periodic checksGame:IsNull(obj) instead of obj == nil for safe null checks on GameObjectsGame:SetName() so they're easy to Game:Find() later00_, 01_) to control Lua mod load order