mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Add Lua audio, dynamic vehicle cameras, dashboard, and Skyline updates
This commit is contained in:
1 parent
cb7968930f
commit
1399555ef2
140 files changed
+11400
-7917
No files matched your search
+15
-65
@@ -1,77 +1,27 @@
|
||||
# Agent guide: make a mod for this game
|
||||
# Agent guide: physics/graphics mods (API 2)
|
||||
|
||||
Use this file as the starting point when asked to make a mod. The current SDK is
|
||||
embedded Lua 5.4, API major 1. Deliver a validated `.zip` in the project-root `mods/`.
|
||||
Do not invent API calls or assume this is an unrestricted Lua/native-code plugin system.
|
||||
Use this when asked to make a mod. Deliver a validated package under `mods/` (folder or `.zip`).
|
||||
Do **not** invent vehicle/wheel/suspension host APIs — assemble those in Lua from primitives.
|
||||
|
||||
## Read these sources in order
|
||||
## Read first
|
||||
|
||||
1. `docs/mod-packages.md`: ZIP structure, packaging and installation.
|
||||
2. `sdk/skate.lua`: callable functions, argument types and bounds; annotations only.
|
||||
3. `docs/lua-modding.md`: manifest, settings, lifecycle, quotas, callbacks and examples.
|
||||
4. `docs/multiplayer-mods.md`: automatic replication, matching packages, ownership and shared state.
|
||||
5. For vehicles, `docs/vehicle-sdk.md` and `sdk/examples/mario-kart/vehicle.json`.
|
||||
6. For rider animations, `docs/mixamo-vehicle-workflow.md`. For character conversion,
|
||||
`docs/mixamo-to-skate.md` and `docs/custom-models.md`.
|
||||
|
||||
Paths are relative to the repository root. Authoritative implementations are
|
||||
`crates/skate-mods/src/{schema,vm}.rs`, `crates/skate-vehicles/src/definition.rs`, and
|
||||
`crates/skate-game/src/modding/`. If a requested feature has no API, explain the gap
|
||||
or implement/test a shared host API; do not fake an unsupported call in the mod.
|
||||
1. `docs/physics-graphics-sdk.md` — layer philosophy and deferred work
|
||||
2. `sdk/skate.lua` — annotations
|
||||
3. `sdk/examples/physics-sandbox/` — reference mod
|
||||
4. Host: `crates/skate-game/src/modding/`, runtime `crates/skate-mods/`, dynamics `crates/skate-dynamics/`
|
||||
|
||||
## Workflow
|
||||
|
||||
- Create an editable folder outside live mods/, e.g. `sdk/examples/your-mod`.
|
||||
Copy the smallest suitable example, not the whole repository. Use a unique ID
|
||||
like `author.feature` and keep that ID stable across updates.
|
||||
- Write mod.json with api=1, name, version, author, description and entry. Each
|
||||
setting needs type, label, description and default, plus bounds/options for its type.
|
||||
- Return a callback table from main.lua. Use owner-local stable keys for visuals,
|
||||
read settings via sdk.settings, edge-detect held inputs, and clean/reset transient
|
||||
state on world_changed. Refresh vehicle controls from on_fixed_update.
|
||||
- Decide which player owns each shared rule. Cubes and vehicles replicate automatically;
|
||||
UI/input stay local. Use sdk.net.publish/read for Lua state and retain player IDs as strings.
|
||||
Reuse stable keys; do not broadcast one-frame events or independently award shared scores
|
||||
on every client. Consider host changes, late joins and mod disable cleanup.
|
||||
- Keep assets inside the package and use relative forward-slash paths. The entry
|
||||
and vehicle filenames need not be the example names, but their references must match.
|
||||
The game loads one Lua entry file; do not assume require/dofile or arbitrary file I/O.
|
||||
- Build and validate:
|
||||
|
||||
```powershell
|
||||
cargo run --locked -p skate-mods --example check_mod -- sdk/examples/your-mod
|
||||
python tools/package_mod.py sdk/examples/your-mod mods/your-mod.zip
|
||||
# copy/symlink into mods/
|
||||
```
|
||||
|
||||
- For code/API changes, also run `cargo test --locked -p skate-mods -p skate-vehicles -p skate-net`.
|
||||
Check the game crate and build with `Build-VehicleSDK.ps1` when the host changed.
|
||||
The helper packages the two bundled examples; package a new mod separately.
|
||||
- The checker checks structure, limits, schema and Lua syntax without running the mod.
|
||||
It does not prove callbacks, render results, rig compatibility or physics behaviour.
|
||||
Run appropriate headless tests. Do not launch the game automatically in this workspace;
|
||||
provide `PLAY-MARIO-KART.bat` for the user to test and state what remains unverified.
|
||||
- Report the ZIP path, controls/settings, validation results and relevant limitations.
|
||||
The Native Trainer and Mario Kart ZIPs are intentionally committed, as are the
|
||||
prepared kart.glb and rider.json source assets. Keep those ZIPs in sync with their
|
||||
editable examples. Other imported assets/raw authoring files remain ignored.
|
||||
Manifest must use `"api": 2`. Return a callback table from the entry Lua file.
|
||||
|
||||
## Available building blocks
|
||||
## Building blocks
|
||||
|
||||
- Player/input snapshots, native teleport requests, log/text, coloured cubes, timers,
|
||||
package text reads and supported authored animation replacement slots.
|
||||
- Native Trainer tuning: pop, push, braking, steering, grip, fakie hold and related
|
||||
multipliers. Only one mod can own trainer tuning at a time.
|
||||
- Vehicles: spawn/read/control/tune/enter/exit/reset/remove, Rapier map and vehicle
|
||||
collision, raycast wheels/suspension, collider offsets/rounding, independent inertia
|
||||
and centre of mass, throttle-responsive engine synthesis and live engine volume.
|
||||
Rider safety adds an occupied capsule, crash/inversion ejection, carried momentum
|
||||
into the native bail state and vehicle_bailed events; configure rider_safety.
|
||||
- Native-rig rider enter/exit/drive/idle/reverse/brake/steering slots. The host handles
|
||||
steering blending, vanilla hand-offs, board hiding and shared motion interpolation.
|
||||
- Automatic per-mod draggable/resizable/collapsible settings windows and enable badges.
|
||||
|
||||
No native DLL/EXE loading, arbitrary network/filesystem access, external Lua imports,
|
||||
inter-mod dependencies, custom audio recordings or arbitrary rig playback are exposed.
|
||||
Vehicle replay recording is not implemented. Multiplayer and car/player collision
|
||||
bridging are available; see docs/multiplayer-mods.md for ownership and approximation
|
||||
limits. Assets can be bundled but only documented asset types have loaders.
|
||||
Physics bodies (box/sphere/capsule/convex/`mesh` from named GLB objects), extra
|
||||
`add_collider` hulls, forces/joints(+motors)/sensors, graphics meshes/overlays,
|
||||
player attach/detach, camera follow/set, `sdk.assets.objects`, `sdk.input.pad`,
|
||||
settings/log/timers. No `sdk.vehicle`.
|
||||
@@ -1,15 +0,0 @@
|
||||
# Mod SDK
|
||||
|
||||
**Start here: [AGENTS.md](AGENTS.md).** Give this file to your coding agent when asking
|
||||
it to make a mod. It links the package format, complete API, examples and validation.
|
||||
|
||||
Editable sources: `examples/`. Player-installable output: project-root `mods/*.zip`.
|
||||
Use `tools/package_mod.py` to validate and package. The current integrated game launcher
|
||||
is `PLAY-MARIO-KART.bat`; older feature-specific launchers are historical builds.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
Use matching enabled ZIPs and the same integrated build on all peers. World objects
|
||||
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
|
||||
See [Multiplayer mod SDK](../docs/multiplayer-mods.md) for ownership, collisions,
|
||||
late joins and shared-state examples.
|
||||
@@ -1,56 +0,0 @@
|
||||
# Mario Kart vehicle mod
|
||||
Version 1.5 adds arcade rollover resistance and airborne left-stick control.
|
||||
In air, stick left/right rolls, forward pitches the nose down, and back pitches
|
||||
the nose up. Keyboard: A/D roll, Up/Down arrows pitch. Neutral input damps rotation.
|
||||
No lift is added. Aerial inversion is allowed; sustained grounded inversion still
|
||||
ejects. Minor bumps are more forgiving; severe collisions still eject the rider.
|
||||
The matching updated host executable is required.
|
||||
Enable this mod in Mods, resume, then press F10 to spawn the kart in front of you.
|
||||
E (controller Y) enters/exits within 4 metres (slow below 3 m/s to exit). WASD drives; Space brakes;
|
||||
left Shift is the handbrake; R or right-stick click rights/resets the kart. The controller bind can be changed to left-stick click in the mod settings. F10 replaces the kart.
|
||||
|
||||
Controller: RT/LT accelerate/reverse, left stick steers, A brakes, B handbrakes.
|
||||
Version 1.4 uses the updated host's physical tire model: progressive steering,
|
||||
Ackermann wheel angles, torque-driven wheel spin, shared braking/cornering grip,
|
||||
and contact-point weight transfer. Brake overrides throttle; the opposing pedal
|
||||
brakes before engaging reverse. Tire friction defaults to 1.3. The old arcade
|
||||
grip preference is replaced by this new coefficient; other settings are retained.
|
||||
This package requires the matching updated game executable. It remains a
|
||||
simplified raycast-wheel kart, without a gearbox or ABS.
|
||||
The enabled mod shows keyboard and controller controls on screen, including the current reset bind. Toggle Show driving HUD in its settings to hide/show them, even before spawning.
|
||||
|
||||
The GLB is prepared from your supplied ZIP with tools/prepare_mario_kart.py and is
|
||||
committed with the mod. To rebuild it: python tools/prepare_mario_kart.py PATH_TO_ZIP sdk/examples/mario-kart
|
||||
The script requires NumPy. The prepared kart.glb and fitted rider.json are included in source control and in mods/mario-kart.zip.
|
||||
|
||||
Edit vehicle.json for dimensions, wheels, suspension, seat, exit and camera offsets.
|
||||
The bundled rider.json contains fitted entry, exit, seated and left/right steering poses.
|
||||
The host blends steering with stick input and eases between vanilla and vehicle poses
|
||||
using the native local-transform interpolation routine. The skateboard is hidden while
|
||||
seated and restored during the exit hand-off. Camera changes are eased too.
|
||||
|
||||
Rebuild rider.json with Blender and tools/export_kart_rider.py, passing --enter and
|
||||
--exit fitted .blend previews, --bone-names a JSON list from the stock bank hierarchy,
|
||||
--reference the stock skater.glb and --output the local
|
||||
rider.json path. Raw FBX files, Blender projects and the stock character mesh remain local; only the prepared kart model and fitted rider clips are bundled.
|
||||
See docs/vehicle-sdk.md for the animation format and lifecycle details.
|
||||
|
||||
Ramp tuning uses a shorter rounded chassis and a lower centre of mass, with mass
|
||||
distribution independent of the collider. The shared engine_audio definition enables
|
||||
throttle/speed-responsive synthesized engine sound; Engine volume in the mod window
|
||||
controls it live (zero mutes). Full authoring guide: docs/mixamo-vehicle-workflow.md.
|
||||
|
||||
Author here, then run `python tools/package_mod.py sdk/examples/mario-kart mods/mario-kart.zip`.
|
||||
The live ZIP loads automatically; do not edit mods/.cache. See sdk/AGENTS.md.
|
||||
|
||||
Hard crashes, strong head/torso hits and sustained inversion now eject the rider into
|
||||
native bail physics with carried momentum. An occupied-seat capsule protects the torso
|
||||
against map geometry. Tune rider_safety in vehicle.json; see docs/vehicle-sdk.md for
|
||||
threshold units, vehicle_bailed events and the native/Rapier collision boundary.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
Use matching enabled ZIPs and the same integrated build on all peers. World objects
|
||||
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
|
||||
See [Multiplayer mod SDK](../../../docs/multiplayer-mods.md) for ownership, collisions,
|
||||
late joins and shared-state examples.
|
||||
Binary file not shown.
@@ -1,61 +0,0 @@
|
||||
-- The original Lua/JSON example uses the user's separately supplied kart model.
|
||||
local previous={}
|
||||
local interact_was_down=false
|
||||
local reset_was_down=false
|
||||
local function hud(car)
|
||||
local lines={
|
||||
car and string.format('MARIO KART | %.0f km/h | %s',math.abs(car.speed)*3.6,car.ready and car.phase or 'loading') or 'MARIO KART | F10: spawn kart',
|
||||
'KEYBOARD | F10 spawn | E enter / exit | W / S accelerate / reverse | A / D steer',
|
||||
'KEYBOARD | Space brake | Left Shift handbrake | R reset',
|
||||
'CONTROLLER | Y enter / exit | RT / LT accelerate / reverse | Left stick steer',
|
||||
'CONTROLLER | A brake | B handbrake | '..(sdk.settings.reset_button=='Left stick' and 'L3' or 'R3')..' (stick click) reset',
|
||||
'Slow below 3 m/s to exit | Hard crashes / rollovers eject the rider | Escape: settings'
|
||||
}
|
||||
for i,line in ipairs(lines) do
|
||||
local key=i==1 and 'kart-hud' or 'kart-hud-'..i
|
||||
if sdk.settings.hud then sdk.ui.text(key,line) else sdk.scene.remove(key) end
|
||||
end
|
||||
end
|
||||
local function pressed(key)
|
||||
local down=sdk.input.down(key);local edge=down and not previous[key];previous[key]=down;return edge
|
||||
end
|
||||
local function tune()
|
||||
local s=sdk.settings
|
||||
sdk.vehicle.tune('kart',{engine_volume=s.engine_volume,engine_force=s.engine,max_speed=s.speed,brake_impulse=s.brake,steering_angle=s.steering,tire_grip=s.tire_friction})
|
||||
end
|
||||
local function nearby()
|
||||
local p=sdk.player.read();local h=p.heading or 0
|
||||
return {p.position[1]+math.sin(h)*3,p.position[2]+1,p.position[3]+math.cos(h)*3},h
|
||||
end
|
||||
local function spawn()
|
||||
local car=sdk.vehicle.read('kart')
|
||||
if car and car.occupied then sdk.vehicle.reset('kart',{car.position[1],car.position[2]+1,car.position[3]},car.heading);return end
|
||||
if car then sdk.vehicle.remove('kart') end
|
||||
local p,h=nearby();sdk.vehicle.spawn('kart','vehicle.json',p,h);tune()
|
||||
end
|
||||
return {
|
||||
on_load=function() hud() end,
|
||||
on_fixed_update=function()
|
||||
if pressed('F10') then spawn() end
|
||||
local car=sdk.vehicle.read('kart')
|
||||
if not car then hud();return end
|
||||
local interact=sdk.vehicle.input().interact
|
||||
local interact_pressed=interact and not interact_was_down;interact_was_down=interact
|
||||
if interact_pressed then if car.occupied then sdk.vehicle.exit('kart') else sdk.vehicle.enter('kart') end end
|
||||
local reset_mask=sdk.settings.reset_button=='Left stick' and 0x40 or 0x80
|
||||
local reset_down=car.occupied and ((sdk.vehicle.input().pad_buttons or 0) & reset_mask)~=0
|
||||
local reset_pressed=reset_down and not reset_was_down;reset_was_down=reset_down
|
||||
if pressed('KeyR') or reset_pressed then
|
||||
local p,h=nearby()
|
||||
if car.occupied then p={car.position[1],car.position[2]+1,car.position[3]};h=car.heading end
|
||||
sdk.vehicle.reset('kart',p,h)
|
||||
end
|
||||
if car.phase=='driving' then sdk.vehicle.control('kart',sdk.vehicle.input() and {
|
||||
throttle=sdk.vehicle.input().throttle,steering=sdk.vehicle.input().steering,
|
||||
pitch=sdk.vehicle.input().pitch or 0,
|
||||
brake=sdk.vehicle.input().brake,handbrake=sdk.vehicle.input().handbrake}) end
|
||||
hud(car)
|
||||
end,
|
||||
on_settings=function() local car=sdk.vehicle.read('kart');if car then tune() end;hud(car) end,
|
||||
on_event=function(e) if e.name=='world_changed' then previous={};interact_was_down=false;reset_was_down=false;hud() end end,
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
{
|
||||
"id": "community.mario-kart",
|
||||
"api": 1,
|
||||
"name": "Mario Kart",
|
||||
"version": "1.5.0",
|
||||
"author": "Local vehicle SDK example",
|
||||
"description": "Rapier kart using your supplied model. F10 spawns nearby, E enters/exits, WASD drives, Space brakes, Shift handbrake, R resets. Slow down before exiting. Fitted entry/exit and steering animations included.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"engine": {
|
||||
"type": "number",
|
||||
"label": "Engine force",
|
||||
"description": "Applied live to the kart.",
|
||||
"default": 1800,
|
||||
"min": 100,
|
||||
"max": 8000,
|
||||
"step": 100
|
||||
},
|
||||
"speed": {
|
||||
"type": "number",
|
||||
"label": "Maximum speed (m/s)",
|
||||
"description": "Applied live to the kart.",
|
||||
"default": 25,
|
||||
"min": 5,
|
||||
"max": 60,
|
||||
"step": 1
|
||||
},
|
||||
"brake": {
|
||||
"type": "number",
|
||||
"label": "Brake impulse",
|
||||
"description": "Applied live to the kart.",
|
||||
"default": 90,
|
||||
"min": 10,
|
||||
"max": 300,
|
||||
"step": 10
|
||||
},
|
||||
"steering": {
|
||||
"type": "number",
|
||||
"label": "Steering angle",
|
||||
"description": "Applied live to the kart.",
|
||||
"default": 0.5,
|
||||
"min": 0.1,
|
||||
"max": 1,
|
||||
"step": 0.05
|
||||
},
|
||||
"tire_friction": {
|
||||
"type": "number",
|
||||
"label": "Tire friction coefficient",
|
||||
"description": "Combined braking and cornering grip. 1.3 is the dry kart setup; lower values allow more sliding.",
|
||||
"default": 1.3,
|
||||
"min": 0.2,
|
||||
"max": 2,
|
||||
"step": 0.05
|
||||
},
|
||||
"hud": {
|
||||
"type": "boolean",
|
||||
"label": "Show driving HUD",
|
||||
"description": "Show keyboard and controller controls, current reset bind, speed and vehicle state.",
|
||||
"default": true
|
||||
},
|
||||
"reset_button": {
|
||||
"type": "choice",
|
||||
"label": "Controller reset bind",
|
||||
"description": "Click this stick while driving to reset the kart. R remains the keyboard shortcut.",
|
||||
"default": "Right stick",
|
||||
"choices": [
|
||||
"Right stick",
|
||||
"Left stick"
|
||||
]
|
||||
},
|
||||
"engine_volume": {
|
||||
"type": "number",
|
||||
"label": "Engine volume",
|
||||
"default": 0.45,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.05,
|
||||
"description": "Engine loudness; zero mutes the motor."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":1,"bone_names":["TRAJECTORY","HIPS","SPINE","SPINE1","SPINE2","SPINE3","NECK","NECK1","HEAD","RIGHTSHOULDER","RIGHTARM","RIGHTFOREARM","RIGHTHAND","LEFTSHOULDER","LEFTARM","LEFTFOREARM","LEFTHAND","RIGHTUPLEG","RIGHTLEG","RIGHTFOOT","RIGHTTOEBASE","LEFTUPLEG","LEFTLEG","LEFTFOOT","LEFTTOEBASE","SKATEBOARD_ROOT","TRUCK_FRONT","RIGHT_WHEELFRONT","LEFT_WHEELFRONT","TRUCK_BACK","LEFT_WHEELBACK","RIGHT_WHEELBACK","RIGHTTOEBASE_REPARENTED","LEFTTOEBASE_REPARENTED","RIGHTHAND_REPARENTED","LEFTHAND_REPARENTED"],"clips":{"enter":{"fps":60,"frames":[[[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0],[0.247126,0.965941,0.076725,0.0,-0.964343,0.25291,-0.077973,0.0,-0.094722,-0.05472,0.993999,0.0,1.826877,0.184085,0.134088,1.0],[-0.010874,0.999876,0.011391,0.0,-0.997631,-0.010074,-0.068046,0.0,-0.067923,-0.012104,0.997617,0.0,1.853606,0.275625,0.143949,1.0],[0.077093,0.996972,-0.010229,0.0,-0.99506,0.076293,-0.063535,0.0,-0.062562,0.015077,0.997927,0.0,1.853971,0.37509,0.147294,1.0],[0.170703,0.985314,-0.004184,0.0,-0.983335,0.170088,-0.064217,0.0,-0.062562,0.015077,0.997927,0.0,1.862298,0.482773,0.146189,1.0],[-0.134993,0.989598,-0.049735,0.0,-0.989193,-0.137496,-0.050907,-0.0,-0.057216,0.042326,0.997464,0.0,1.884411,0.613769,0.144682,1.0],[-0.440967,0.894681,-0.071377,0.0,-0.895543,-0.443881,-0.03119,-0.0,-0.059588,0.050167,0.996961,0.0,1.864403,0.749667,0.137649,1.0],[-0.276798,0.958742,-0.064788,0.0,-0.959079,-0.279818,-0.043244,-0.0,-0.059588,0.050167,0.996962,0.0,1.838417,0.802391,0.133443,1.0],[-0.06035,0.996886,-0.050769,0.0,-0.997542,-0.062048,-0.032579,-0.0,-0.035627,0.048678,0.998179,0.0,1.828663,0.844764,0.13103,1.0],[0.063381,-0.304268,-0.950476,-0.0,-0.994333,-0.10071,-0.034066,-0.0,-0.085357,0.947248,-0.308927,0.0,1.843193,0.722202,0.113186,1.0],[-0.055441,-0.973607,-0.221399,-0.0,-0.735036,-0.110275,0.669001,0.0,-0.675758,0.199827,-0.709522,0.0,1.848127,0.677019,-0.045639,1.0],[-0.122471,-0.992314,0.017735,-0.0,-0.718667,0.100993,0.687982,0.0,-0.684485,0.071512,-0.725511,0.0,1.839006,0.387418,-0.110069,1.0],[-0.003577,-0.882047,0.471148,-0.0,-0.472976,-0.413626,-0.777951,0.0,0.881068,-0.225625,-0.415708,-0.0,1.806323,0.144883,-0.118319,1.0],[-0.088402,-0.18525,0.978707,-0.0,-0.990129,-0.090947,-0.106648,-0.0,0.108767,-0.978474,-0.175381,-0.0,1.842112,0.72302,0.160986,1.0],[-0.062181,-0.978667,0.195816,-0.0,-0.753674,-0.082578,-0.652041,0.0,0.654301,-0.188126,-0.732461,-0.0,1.822075,0.697518,0.325838,1.0],[-0.072891,-0.997233,0.014646,-0.0,-0.748069,0.044955,-0.662097,0.0,0.659606,-0.059217,-0.749276,-0.0,1.813966,0.408338,0.384805,1.0],[-0.111244,-0.898037,-0.425624,-0.0,-0.236578,-0.392039,0.889009,0.0,-0.965223,0.19959,-0.168844,0.0,1.796902,0.164993,0.399195,1.0],[-0.055255,-0.998217,-0.022604,0.0,-0.961268,0.059305,-0.26916,0.0,0.27002,0.006856,-0.96283,0.0,1.834232,0.199152,0.054516,1.0],[0.151135,-0.988131,0.027482,0.0,-0.891985,-0.148306,-0.427047,0.0,0.426054,0.040028,-0.903812,0.0,1.811971,-0.220373,0.044531,1.0],[-0.782362,-0.481747,-0.394752,0.0,-0.430099,0.87631,-0.217013,0.0,0.45047,-0.0,-0.892791,0.0,1.873222,-0.619946,0.055971,1.0],[-0.885607,0.126606,-0.446846,0.0,0.113032,0.991953,0.057032,0.0,0.45047,0.0,-0.892791,0.0,1.74596,-0.700553,-0.008241,1.0],[-0.23299,-0.964857,0.121518,0.0,-0.951872,0.251854,0.17467,0.0,-0.199137,-0.074974,-0.977099,0.0,1.819936,0.188216,0.212871,1.0],[0.109024,-0.989689,0.092899,0.0,-0.967091,-0.083991,0.240169,0.0,-0.22989,-0.116027,-0.966275,0.0,1.722869,-0.219471,0.26457,1.0],[-0.849439,-0.483431,0.211536,0.0,-0.469696,0.87538,0.114437,0.0,-0.240497,-0.002151,-0.970648,0.0,1.766883,-0.618771,0.302604,1.0],[-0.967257,0.084077,0.23947,0.0,0.081094,0.996457,-0.022301,0.0,-0.240497,-0.002151,-0.970647,0.0,1.628154,-0.699175,0.337155,1.0],[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,-0.641,0.2,1.0],[-0.0,-0.374607,-0.927184,0.0,-1.0,-0.0,0.0,0.0,-0.0,0.927184,-0.374607,0.0,-0.0,-0.668862,0.459179,1.0],[1.0,0.0,-0.0,0.0,0.0,-0.374607,-0.927184,0.0,-0.0,0.927184,-0.374607,0.0,-0.097497,-0.697312,0.442804,1.0],[1.0,0.0,-0.0,0.0,0.0,-0.374607,-0.927184,0.0,-0.0,0.927184,-0.374607,0.0,0.097497,-0.697312,0.442804,1.0],[-0.0,-0.374606,0.927184,0.0,1.0,-0.0,0.0,0.0,0.0,0.927184,0.374606,0.0,0.0,-0.668863,-0.059179,1.0],[1.0,-0.0,0.0,0.0,0.0,0.374606,-0.927184,0.0,0.0,0.927184,0.374606,0.0,0.097497,-0.697312,-0.043508,1.0],[1.0,-0.0,0.0,0.0,0.0,0.374606,-0.927184,0.0,0.0,0.927184,0.374606,0.0,-0.097497,-0.697312,-0.043508,1.0],[1.7e-05,0.0,1.0,0.0,-2e-06,1.0,0.0,0.0,-1.0,-2e-06,1.7e-05,0.0,-0.112785,-0.699299,0.309098,1.0],[1.7e-05,0.0,1.0,0.0,-2e-06,1.0,0.0,0.0,-1.0,-2e-06,1.7e-05,0.0,0.112806,-0.699299,0.309098,1.0],[0.5843,-0.679796,0.443249,0.0,0.759174,0.650881,-0.002524,0.0,-0.286787,0.337978,0.896395,0.0,0.512949,0.311738,0.300137,1.0],[-0.581212,-0.68309,0.442245,0.0,-0.761452,0.648221,0.000517,0.0,-0.287026,-0.336448,-0.896894,0.0,-0.512609,0.311738,0.300137,1.0]],[[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,Line truncated
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"model": "kart.glb",
|
||||
"half_extents": [
|
||||
0.5,
|
||||
0.14,
|
||||
0.65
|
||||
],
|
||||
"mass": 220,
|
||||
"engine_force": 1800,
|
||||
"max_speed": 25,
|
||||
"brake_impulse": 90,
|
||||
"steering_angle": 0.5,
|
||||
"suspension_length": 0.25,
|
||||
"suspension_stiffness": 30,
|
||||
"suspension_damping": 4,
|
||||
"tire_grip": 1.3,
|
||||
"ground_stability": 0.8,
|
||||
"air_control": 6,
|
||||
"wheels": [
|
||||
{
|
||||
"node": "wheel_rl",
|
||||
"position": [
|
||||
0.5815213608992547,
|
||||
0.057323790779298445,
|
||||
-0.673016238225116
|
||||
],
|
||||
"radius": 0.3338663063949073,
|
||||
"steering": false,
|
||||
"driven": true
|
||||
},
|
||||
{
|
||||
"node": "wheel_rr",
|
||||
"position": [
|
||||
-0.5302057197312625,
|
||||
0.06093891045413358,
|
||||
-0.6730162402958683
|
||||
],
|
||||
"radius": 0.3338663063949072,
|
||||
"steering": false,
|
||||
"driven": true
|
||||
},
|
||||
{
|
||||
"node": "wheel_fl",
|
||||
"position": [
|
||||
0.5573244172948093,
|
||||
0.016883687342375997,
|
||||
0.4261990809959044
|
||||
],
|
||||
"radius": 0.31535000391066886,
|
||||
"steering": true,
|
||||
"driven": false
|
||||
},
|
||||
{
|
||||
"node": "wheel_fr",
|
||||
"position": [
|
||||
-0.5201046643741327,
|
||||
0.01739195285686146,
|
||||
0.4231450809914261
|
||||
],
|
||||
"radius": 0.31535000391066886,
|
||||
"steering": true,
|
||||
"driven": false
|
||||
}
|
||||
],
|
||||
"seat": [
|
||||
0,
|
||||
0.18,
|
||||
-0.2
|
||||
],
|
||||
"exit": [
|
||||
1.8,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"camera_distance": 5,
|
||||
"camera_height": 2,
|
||||
"animations": {
|
||||
"file": "rider.json",
|
||||
"enter": "enter",
|
||||
"exit": "exit",
|
||||
"drive": "drive",
|
||||
"idle": "drive",
|
||||
"reverse": "drive",
|
||||
"brake": "drive",
|
||||
"steer_left": "steer_left",
|
||||
"steer_right": "steer_right"
|
||||
},
|
||||
"collider_offset": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"collider_rounding": 0.1,
|
||||
"chassis_friction": 0.05,
|
||||
"engine_audio": {
|
||||
"enabled": true,
|
||||
"volume": 0.45,
|
||||
"idle_pitch": 0.7,
|
||||
"max_pitch": 2.8
|
||||
},
|
||||
"center_of_mass": [
|
||||
0,
|
||||
-0.2,
|
||||
0
|
||||
],
|
||||
"inertia_half_extents": [
|
||||
1.1,
|
||||
0.35,
|
||||
1.4
|
||||
],
|
||||
"rider_safety": {
|
||||
"enabled": true,
|
||||
"offset": [
|
||||
0,
|
||||
0.5,
|
||||
0
|
||||
],
|
||||
"radius": 0.25,
|
||||
"half_height": 0.25,
|
||||
"crash_delta_v": 12,
|
||||
"hit_impulse": 1200,
|
||||
"inverted_up_y": -0.2,
|
||||
"inverted_seconds": 1.0,
|
||||
"eject_up_speed": 2
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# Native Trainer Showcase 2.1
|
||||
|
||||
One original mod demonstrating the actual SDK, with no game assets. Enable it in
|
||||
Escape -> Mods. Its independent draggable settings window opens on Escape.
|
||||
The eleven physics controls start at 1x stock. Disable the mod to restore stock
|
||||
parameters and remove all its HUD and world objects.
|
||||
|
||||
## Native controls
|
||||
|
||||
Pop height, grind pop, push speed, push power, brake strength, steering response,
|
||||
speed wobble, off-board jump height, ground wheel/slide grip, heading turn force,
|
||||
and manual balance drag. These scale existing inputs/settings, not simulation
|
||||
speed or arbitrary body velocities. Most controls range 0.25x-4x; wobble is 0x-2x.
|
||||
Current difficulty, native state gates and surface rules still apply.
|
||||
|
||||
## Showcase features
|
||||
|
||||
- Compact/full/off telemetry: board speed, peak, approximate travelled distance,
|
||||
root position, bail count, grind entries and active grind time.
|
||||
- F5 saves a trainer checkpoint; F6 requests return through the native reset path.
|
||||
This does not replace the game's own session marker. Save on valid clear ground.
|
||||
- F7 starts/stops the active-play stopwatch; its clock pauses with Escape/replay.
|
||||
- F8 clears session statistics, stopwatch, checkpoint and all visual markers.
|
||||
- F9 places a coloured visual beacon. Eight keyed beacons are reused in a ring.
|
||||
- Optional breadcrumb trail reuses 24 tiny cubes at configurable distance spacing.
|
||||
- Colour choices, units, HUD label, help hints and configurable keyboard shortcuts.
|
||||
- Temporary notifications expire using owned timers. The log reads help.txt using
|
||||
the package-relative file API. Edit source to see debounced hot reload in action.
|
||||
|
||||
Markers have no collision and are not grind rails. Statistics are local sampled
|
||||
observations, not native career scores: travel rejects single-frame jumps of 50m
|
||||
or more but is not replay-grade odometry. Board velocity is not walking speed.
|
||||
The full HUD uses separate owned lines to avoid overlapping other notifications.
|
||||
|
||||
All transient state resets on script reload/disable or map change. Persisted
|
||||
settings survive. The shortcut choice lists let authors try rebinding; use distinct
|
||||
keys. If bindings coincide, actions execute in save/return/timer/clear/beacon order.
|
||||
A native teleport rejection stops this mod and reports the error, as with any
|
||||
SDK host-command failure; recover via Reload. No invincibility, general gravity
|
||||
edit, physics collision insertion, arbitrary animation slot or score API is claimed.
|
||||
|
||||
## Source tour
|
||||
|
||||
main.lua deliberately separates apply (validated native tuning), update (input,
|
||||
HUD, visual objects), on_event (map/bail/grind observations), and timer cleanup.
|
||||
Use this as a practical starting point for training tools, navigation helpers,
|
||||
telemetry overlays and original visual extensions. The engine's full API reference
|
||||
is docs/lua-modding.md and language-server annotations are sdk/skate.lua.
|
||||
|
||||
Hold fakie stance disables automatic fakie stance switching; normal steering and slides still work. Off by default. Escape settings windows can be resized by dragging the bottom-right // grip.
|
||||
|
||||
Authoring folder: sdk/examples/native-trainer. Package with tools/package_mod.py into
|
||||
mods/native-trainer.zip. See sdk/AGENTS.md and docs/mod-packages.md.
|
||||
|
||||
## Multiplayer
|
||||
|
||||
Use matching enabled ZIPs and the same integrated build on all peers. World objects
|
||||
and vehicles synchronize through the SDK; Lua shared rules use `sdk.net`.
|
||||
See [Multiplayer mod SDK](../../../docs/multiplayer-mods.md) for ownership, collisions,
|
||||
late joins and shared-state examples.
|
||||
@@ -1,4 +0,0 @@
|
||||
Native Trainer Showcase 2.0
|
||||
F5 save checkpoint; F6 return; F7 start/stop stopwatch; F8 clear session; F9 place beacon.
|
||||
All bindings and visual options are configurable. All physics multipliers default to 1.
|
||||
Telemetry reads the existing engine; cubes are visual only. No game assets are bundled.
|
||||
@@ -1,110 +0,0 @@
|
||||
-- Native Trainer Showcase: original Lua, no game assets.
|
||||
local tuning_keys={'pop','grind_pop','push_speed','push_power','braking','steering','wobble','offboard_jump','grip','turn_power','manual_drag','hold_fakie'}
|
||||
local previous, checkpoint, last_position, last_trail = {}, nil, nil, nil
|
||||
local trail_index, beacon_index = 0, 0
|
||||
local stats = {distance=0,peak=0,bails=0,grinds=0,grind_time=0}
|
||||
local stopwatch, running, elapsed = 0, false, 0
|
||||
local function copy(p) return {p[1],p[2],p[3]} end
|
||||
local function distance(a,b) return math.sqrt((a[1]-b[1])^2+(a[2]-b[2])^2+(a[3]-b[3])^2) end
|
||||
local function color()
|
||||
return sdk.settings.marker_color=='orange' and {1,0.45,0.1} or sdk.settings.marker_color=='purple' and {0.7,0.25,1} or {0.1,0.85,0.85}
|
||||
end
|
||||
local function notice(text)
|
||||
sdk.ui.text('notice',text)
|
||||
sdk.time.after('notice',3,function() sdk.scene.remove('notice') end)
|
||||
end
|
||||
local function clear_trail()
|
||||
for i=1,24 do sdk.scene.remove('trail'..i) end
|
||||
last_trail=nil; trail_index=0
|
||||
end
|
||||
local function clear_session()
|
||||
clear_trail()
|
||||
for i=1,8 do sdk.scene.remove('beacon'..i) end
|
||||
sdk.scene.remove('checkpoint'); sdk.scene.remove('timer'); sdk.scene.remove('notice')
|
||||
sdk.time.cancel('notice')
|
||||
checkpoint=nil; beacon_index=0; last_position=nil
|
||||
stopwatch=0; running=false; elapsed=0
|
||||
stats={distance=0,peak=0,bails=0,grinds=0,grind_time=0}
|
||||
end
|
||||
local function apply()
|
||||
local tuning={}
|
||||
for _,key in ipairs(tuning_keys) do tuning[key]=sdk.settings[key] end
|
||||
sdk.trainer.apply(tuning)
|
||||
if not sdk.settings.trail then clear_trail() end
|
||||
if sdk.settings.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help') end
|
||||
end
|
||||
local function draw()
|
||||
local s=sdk.settings
|
||||
if s.hud=='off' then sdk.scene.remove('hud'); sdk.scene.remove('hud_stats'); sdk.scene.remove('hud_help'); return end
|
||||
local p=sdk.player.read(); local v=p.velocity
|
||||
local speed=math.sqrt(v[1]^2+v[2]^2+v[3]^2)
|
||||
local factor=s.units=='km/h' and 3.6 or s.units=='mph' and 2.236936 or 1
|
||||
local label=s.hud_label; local cut=utf8.offset(label,25); if cut then label=label:sub(1,cut-1) end
|
||||
local text=string.format('%s | %.1f %s | peak %.1f | %.0f m',label,speed*factor,s.units,stats.peak*factor,stats.distance)
|
||||
if s.hud=='full' then
|
||||
sdk.ui.text('hud_stats',string.format('XYZ %.1f %.1f %.1f | bails %d | grinds %d / %.1fs',p.position[1],p.position[2],p.position[3],stats.bails,stats.grinds,stats.grind_time))
|
||||
else sdk.scene.remove('hud_stats') end
|
||||
if s.show_help then sdk.ui.text('hud_help',string.format('%s save %s return %s timer %s clear %s beacon',s.save_key,s.return_key,s.timer_key,s.reset_key,s.beacon_key)) else sdk.scene.remove('hud_help') end
|
||||
sdk.ui.text('hud',text)
|
||||
end
|
||||
local function update(e)
|
||||
local p=sdk.player.read(); local position=p.position
|
||||
elapsed=elapsed+e.dt
|
||||
if last_position then local d=distance(position,last_position); if d<50 then stats.distance=stats.distance+d end end
|
||||
last_position=copy(position)
|
||||
local v=p.velocity; stats.peak=math.max(stats.peak,math.sqrt(v[1]^2+v[2]^2+v[3]^2))
|
||||
if p.grind and p.grind.active then stats.grind_time=stats.grind_time+e.dt end
|
||||
local edges={}; local down={}
|
||||
for _,setting in ipairs({'save_key','return_key','timer_key','reset_key','beacon_key'}) do
|
||||
local key=sdk.settings[setting]; down[key]=sdk.input.down(key); edges[setting]=down[key] and not previous[key]
|
||||
end
|
||||
previous=down
|
||||
if edges.save_key then
|
||||
if p.bailing or p.state==702 then notice('Wait until the skater is ready to save.')
|
||||
else
|
||||
checkpoint={position=copy(position),heading=p.heading or 0,on_board=p.on_board}
|
||||
sdk.scene.cube('checkpoint',{position[1],position[2]+0.5,position[3]},{0.15,1,0.15},color())
|
||||
notice('Trainer checkpoint saved (separate from the native session marker).')
|
||||
end
|
||||
end
|
||||
if edges.return_key then
|
||||
if not checkpoint then notice('Save a trainer checkpoint first.')
|
||||
elseif p.bailing or p.state==702 then notice('Wait until recovery finishes.')
|
||||
else
|
||||
sdk.player.teleport(checkpoint.position,checkpoint.heading,checkpoint.on_board)
|
||||
last_position=nil; last_trail=nil
|
||||
notice('Returning to trainer checkpoint.')
|
||||
end
|
||||
end
|
||||
if edges.timer_key then
|
||||
if running then running=false; notice(string.format('Stopwatch stopped: %.2f seconds',stopwatch))
|
||||
else stopwatch=0; running=true; notice('Stopwatch started.') end
|
||||
end
|
||||
if edges.reset_key then clear_session(); notice('Session stats, checkpoint and visual markers cleared.') end
|
||||
if edges.beacon_key then
|
||||
beacon_index=beacon_index%8+1
|
||||
sdk.scene.cube('beacon'..beacon_index,{position[1],position[2]+1,position[3]},{0.25,2,0.25},color())
|
||||
notice('Visual beacon placed; it has no collision.')
|
||||
end
|
||||
if sdk.settings.trail and (not last_trail or distance(position,last_trail)>=sdk.settings.trail_spacing) then
|
||||
trail_index=trail_index%24+1; last_trail=copy(position)
|
||||
sdk.scene.cube('trail'..trail_index,{position[1],position[2]+0.1,position[3]},{0.2,0.2,0.2},color())
|
||||
end
|
||||
if running then stopwatch=stopwatch+e.dt end
|
||||
if running or stopwatch>0 then sdk.ui.text('timer',string.format('STOPWATCH: %.2f s%s',stopwatch,running and '' or ' (stopped)')) end
|
||||
draw()
|
||||
end
|
||||
return {
|
||||
on_load=function()
|
||||
apply(); draw()
|
||||
sdk.log(sdk.read_text('help.txt'))
|
||||
notice('Native Trainer Showcase ready. Open Escape for settings.')
|
||||
end,
|
||||
on_settings=function() apply(); draw() end,
|
||||
on_update=update,
|
||||
on_event=function(e)
|
||||
if e.name=='world_changed' then clear_session(); previous={}; apply(); draw(); notice('New map: trainer session cleared.') end
|
||||
if e.name=='bail_changed' and e.bailing then stats.bails=stats.bails+1 end
|
||||
if e.name=='grind_changed' and e.grind.active then stats.grinds=stats.grinds+1 end
|
||||
end
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
{
|
||||
"id": "community.native-trainer",
|
||||
"api": 1,
|
||||
"name": "Native Trainer Showcase",
|
||||
"version": "2.1.0",
|
||||
"author": "Skate Rust Engine",
|
||||
"description": "Native tuning, telemetry, checkpoint return, stopwatch, breadcrumbs and beacons in one original Lua mod. Manage options here; default keyboard shortcuts F5 save, F6 return, F7 stopwatch, F8 reset, F9 beacon. Stock tuning is 1x.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"pop": {
|
||||
"type": "number",
|
||||
"label": "Pop height",
|
||||
"description": "Multiplies native ground-jump minimum and maximum heights. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"grind_pop": {
|
||||
"type": "number",
|
||||
"label": "Grind pop height",
|
||||
"description": "Multiplies the existing grind-launch height. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"push_speed": {
|
||||
"type": "number",
|
||||
"label": "Maximum push speed",
|
||||
"description": "Multiplies the speed up to which pushing works. This is not a downhill speed cap. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"push_power": {
|
||||
"type": "number",
|
||||
"label": "Push power",
|
||||
"description": "Multiplies native difficulty-specific push velocity changes. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"braking": {
|
||||
"type": "number",
|
||||
"label": "Brake strength",
|
||||
"description": "Multiplies foot and tail braking forces. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"steering": {
|
||||
"type": "number",
|
||||
"label": "Steering response",
|
||||
"description": "Multiplies the native general steering scalar. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"wobble": {
|
||||
"type": "number",
|
||||
"label": "Speed wobble",
|
||||
"description": "Multiplies native wobble amplitude. Zero removes wobble. 1.00x is stock.",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.25
|
||||
},
|
||||
"offboard_jump": {
|
||||
"type": "number",
|
||||
"label": "Off-board jump",
|
||||
"description": "Multiplies native biped jump height. 1x preserves stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"grip": {
|
||||
"type": "number",
|
||||
"label": "Wheel / slide grip",
|
||||
"description": "Multiplies ground wheel static/dynamic friction and sideways slide friction. 1x preserves stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"turn_power": {
|
||||
"type": "number",
|
||||
"label": "Turning force",
|
||||
"description": "Multiplies native heading turn torque strength. 1x preserves stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"manual_drag": {
|
||||
"type": "number",
|
||||
"label": "Manual drag",
|
||||
"description": "Multiplies native manual balance linear drag. 1x preserves stock.",
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4,
|
||||
"step": 0.25
|
||||
},
|
||||
"hud": {
|
||||
"type": "choice",
|
||||
"label": "Telemetry HUD",
|
||||
"default": "full",
|
||||
"choices": [
|
||||
"off",
|
||||
"compact",
|
||||
"full"
|
||||
],
|
||||
"description": "Live board speed and session statistics. Full adds position, altitude and event counts."
|
||||
},
|
||||
"units": {
|
||||
"type": "choice",
|
||||
"label": "Speed units",
|
||||
"default": "km/h",
|
||||
"choices": [
|
||||
"km/h",
|
||||
"mph",
|
||||
"m/s"
|
||||
],
|
||||
"description": "Display conversion only; does not alter physics."
|
||||
},
|
||||
"trail": {
|
||||
"type": "boolean",
|
||||
"label": "Breadcrumb trail",
|
||||
"default": false,
|
||||
"description": "Leave up to 24 small visual markers behind you. No collision. Cleared on map change."
|
||||
},
|
||||
"trail_spacing": {
|
||||
"type": "number",
|
||||
"label": "Trail spacing (m)",
|
||||
"default": 6,
|
||||
"min": 1,
|
||||
"max": 30,
|
||||
"step": 1,
|
||||
"description": "Minimum distance between breadcrumb markers."
|
||||
},
|
||||
"marker_color": {
|
||||
"type": "choice",
|
||||
"label": "Marker colour",
|
||||
"default": "cyan",
|
||||
"choices": [
|
||||
"cyan",
|
||||
"orange",
|
||||
"purple"
|
||||
],
|
||||
"description": "Colour of newly placed checkpoints, breadcrumbs and beacons."
|
||||
},
|
||||
"hud_label": {
|
||||
"type": "string",
|
||||
"label": "HUD label",
|
||||
"default": "TRAINER",
|
||||
"description": "An editable label demonstrating string settings."
|
||||
},
|
||||
"show_help": {
|
||||
"type": "boolean",
|
||||
"label": "Show key hints",
|
||||
"default": true,
|
||||
"description": "Show current shortcut bindings in the HUD."
|
||||
},
|
||||
"save_key": {
|
||||
"type": "choice",
|
||||
"label": "Save checkpoint key",
|
||||
"default": "F5",
|
||||
"choices": [
|
||||
"F5",
|
||||
"KeyJ",
|
||||
"KeyK",
|
||||
"KeyL"
|
||||
],
|
||||
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
|
||||
},
|
||||
"return_key": {
|
||||
"type": "choice",
|
||||
"label": "Return checkpoint key",
|
||||
"default": "F6",
|
||||
"choices": [
|
||||
"F6",
|
||||
"KeyJ",
|
||||
"KeyK",
|
||||
"KeyL"
|
||||
],
|
||||
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
|
||||
},
|
||||
"timer_key": {
|
||||
"type": "choice",
|
||||
"label": "Stopwatch key",
|
||||
"default": "F7",
|
||||
"choices": [
|
||||
"F7",
|
||||
"KeyJ",
|
||||
"KeyK",
|
||||
"KeyL"
|
||||
],
|
||||
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
|
||||
},
|
||||
"reset_key": {
|
||||
"type": "choice",
|
||||
"label": "Reset / clear key",
|
||||
"default": "F8",
|
||||
"choices": [
|
||||
"F8",
|
||||
"KeyJ",
|
||||
"KeyK",
|
||||
"KeyL"
|
||||
],
|
||||
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
|
||||
},
|
||||
"beacon_key": {
|
||||
"type": "choice",
|
||||
"label": "Place beacon key",
|
||||
"default": "F9",
|
||||
"choices": [
|
||||
"F9",
|
||||
"KeyJ",
|
||||
"KeyK",
|
||||
"KeyL"
|
||||
],
|
||||
"description": "Choose distinct shortcuts. If keys coincide, save, return, timer, reset, beacon run in that order."
|
||||
},
|
||||
"hold_fakie": {
|
||||
"type": "boolean",
|
||||
"label": "Hold fakie stance",
|
||||
"description": "Prevents the automatic idle/bump stance-switch animation. Normal steering, slides and tricks remain available. Turn off to restore stock correction.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
local box_key = "crate"
|
||||
local mass = 25
|
||||
local friction = 0.35
|
||||
local spawned = false
|
||||
local attached = false
|
||||
local prev = { e = false, r = false }
|
||||
local log_cooldown = 0
|
||||
local peak_speed = 0
|
||||
local samples = 0
|
||||
|
||||
local function breakaway_newtons()
|
||||
return friction * mass * 9.81
|
||||
end
|
||||
|
||||
local function push_newtons()
|
||||
return sdk.settings.push_newtons or 350
|
||||
end
|
||||
|
||||
local function ground_y(x, z)
|
||||
local hit = sdk.physics.raycast(
|
||||
{ x, 500, z },
|
||||
{ 0, -1, 0 },
|
||||
{ max_distance = 1000, filter = "ground" }
|
||||
)
|
||||
if hit and hit.point then return hit.point[2] end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function touching_ground(key)
|
||||
for _, t in ipairs(sdk.physics.touching() or {}) do
|
||||
if (t.a == key and t.b == "ground") or (t.b == key and t.a == "ground") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function spawn_box()
|
||||
if spawned then return end
|
||||
local p = sdk.player.read().position
|
||||
local gy = ground_y(p[1] + 3, p[3] + 3)
|
||||
sdk.physics.spawn(box_key, {
|
||||
shape = { type = "box", half_extents = { 0.5, 0.5, 0.5 } },
|
||||
body_type = "dynamic",
|
||||
mass = mass,
|
||||
position = { p[1] + 3, gy + 0.55, p[3] + 3 },
|
||||
heading = 0,
|
||||
friction = friction,
|
||||
ccd = true,
|
||||
})
|
||||
sdk.graphics.mesh("crate_vis", {
|
||||
body = box_key,
|
||||
scale = { 1, 1, 1 },
|
||||
color = { 0.2, 0.65, 0.95 },
|
||||
})
|
||||
sdk.camera.follow(box_key, { 0, 3, -7 })
|
||||
sdk.log(string.format(
|
||||
"physics-sandbox: crate mass=%.0fkg friction=%.2f breakaway≈%.0fN push=%dN",
|
||||
mass, friction, breakaway_newtons(), push_newtons()
|
||||
))
|
||||
spawned = true
|
||||
peak_speed = 0
|
||||
samples = 0
|
||||
end
|
||||
|
||||
local function reset_box()
|
||||
sdk.physics.remove(box_key)
|
||||
sdk.graphics.remove("crate_vis")
|
||||
if attached then
|
||||
sdk.player.detach()
|
||||
attached = false
|
||||
end
|
||||
spawned = false
|
||||
spawn_box()
|
||||
end
|
||||
|
||||
local function cmd_force()
|
||||
local force = push_newtons()
|
||||
local f = { 0, 0, 0 }
|
||||
local keys = {}
|
||||
if sdk.input.down("KeyW") then f[3] = f[3] + force; keys[#keys + 1] = "W" end
|
||||
if sdk.input.down("KeyS") then f[3] = f[3] - force; keys[#keys + 1] = "S" end
|
||||
if sdk.input.down("KeyA") then f[1] = f[1] - force; keys[#keys + 1] = "A" end
|
||||
if sdk.input.down("KeyD") then f[1] = f[1] + force; keys[#keys + 1] = "D" end
|
||||
return f, table.concat(keys, "")
|
||||
end
|
||||
|
||||
return {
|
||||
on_load = function()
|
||||
sdk.ui.text("help", "Physics Sandbox — WASD push · E attach/detach · R reset")
|
||||
spawn_box()
|
||||
end,
|
||||
|
||||
on_fixed_update = function(ev)
|
||||
if not spawned then
|
||||
spawn_box()
|
||||
end
|
||||
|
||||
local f, keys = cmd_force()
|
||||
local driving = f[1] ~= 0 or f[3] ~= 0
|
||||
if driving then
|
||||
sdk.physics.force(box_key, f)
|
||||
end
|
||||
|
||||
local e = sdk.input.down("KeyE")
|
||||
if e and not prev.e then
|
||||
if attached then
|
||||
sdk.player.detach()
|
||||
attached = false
|
||||
sdk.log("detached")
|
||||
else
|
||||
sdk.player.attach(box_key, { 0, 1.1, 0 })
|
||||
attached = true
|
||||
sdk.log("attached to crate")
|
||||
end
|
||||
end
|
||||
prev.e = e
|
||||
|
||||
local r = sdk.input.down("KeyR")
|
||||
if r and not prev.r then
|
||||
reset_box()
|
||||
end
|
||||
prev.r = r
|
||||
|
||||
local body = sdk.physics.read(box_key)
|
||||
if not body then
|
||||
return
|
||||
end
|
||||
|
||||
samples = samples + 1
|
||||
local speed = body.speed or 0
|
||||
if speed > peak_speed then
|
||||
peak_speed = speed
|
||||
end
|
||||
local rf = body.force or { 0, 0, 0 }
|
||||
local p = body.position
|
||||
local v = body.linvel
|
||||
local need = breakaway_newtons()
|
||||
local stuck = driving and speed < 0.05 and push_newtons() < need
|
||||
|
||||
sdk.ui.text(
|
||||
"status",
|
||||
string.format(
|
||||
"pos %.1f %.1f %.1f | vel %.2f %.2f %.2f | speed %.2f (peak %.2f)\ncmd force %.0f %.0f %.0f | read force %.0f %.0f %.0f | keys %s | μmg≈%.0fN | gnd %s%s",
|
||||
p[1], p[2], p[3],
|
||||
v[1], v[2], v[3],
|
||||
speed, peak_speed,
|
||||
f[1], f[2], f[3],
|
||||
rf[1], rf[2], rf[3],
|
||||
keys ~= "" and keys or "-",
|
||||
need,
|
||||
touching_ground(box_key) and "yes" or "no",
|
||||
stuck and " | STUCK: raise Push force" or ""
|
||||
)
|
||||
)
|
||||
|
||||
log_cooldown = log_cooldown - (ev and ev.dt or (1 / 60))
|
||||
local runaway = (not driving) and speed > 1.5 and speed >= peak_speed * 0.98
|
||||
if log_cooldown <= 0 or runaway or stuck then
|
||||
log_cooldown = (runaway or stuck) and 0.25 or 1.0
|
||||
sdk.log(string.format(
|
||||
"crate tick=%d pos=(%.2f,%.2f,%.2f) vel=(%.2f,%.2f,%.2f) speed=%.2f cmdF=(%.0f,%.0f,%.0f) readF=(%.0f,%.0f,%.0f) keys=%s stuck=%s",
|
||||
samples,
|
||||
p[1], p[2], p[3],
|
||||
v[1], v[2], v[3],
|
||||
speed,
|
||||
f[1], f[2], f[3],
|
||||
rf[1], rf[2], rf[3],
|
||||
keys ~= "" and keys or "-",
|
||||
stuck and "yes" or "no"
|
||||
))
|
||||
end
|
||||
end,
|
||||
|
||||
on_event = function(ev)
|
||||
if ev.name == "world_changed" then
|
||||
spawned = false
|
||||
attached = false
|
||||
prev.e = false
|
||||
prev.r = false
|
||||
peak_speed = 0
|
||||
samples = 0
|
||||
end
|
||||
end,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "examples.physics-sandbox",
|
||||
"api": 2,
|
||||
"name": "Physics Sandbox",
|
||||
"version": "0.1.1",
|
||||
"author": "Skate3clone",
|
||||
"description": "Demonstrates the physics/graphics layer: spawn a dynamic box, push it with WASD, attach with E.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"push_newtons": {
|
||||
"type": "number",
|
||||
"label": "Push force (N)",
|
||||
"description": "Horizontal force while holding WASD. Must exceed friction≈μmg (about 60–100N for this crate) or the box will not slide.",
|
||||
"default": 350,
|
||||
"min": 50,
|
||||
"max": 1200,
|
||||
"step": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
N-- Propulsion is exclusively axle-joint motors plus Rapier tire friction.
|
||||
-- Steering is exclusively front revolute motors. Suspension is four
|
||||
-- prismatic joints with physical springs. No drive/steering forces or impulses.
|
||||
local MODEL = "skyline.glb"
|
||||
local CHASSIS = "chassis"
|
||||
local SEAT = {-0.38, 0.12, -0.23}
|
||||
local CAMERA = {0, 2.1, -5.8}
|
||||
local ENTER_RANGE = 8.0
|
||||
local WHEEL_RADIUS = 0.34
|
||||
local WHEEL_HALF_WIDTH = 0.146
|
||||
local SUSPENSION_TRAVEL = 0.30
|
||||
local SUSPENSION_STIFFNESS = 26000
|
||||
local SUSPENSION_DAMPING = 5600
|
||||
local STEER_LIMIT = 0.65
|
||||
local WHEELBASE = 2.7906
|
||||
local TRACK_WIDTH = 1.591
|
||||
local FINAL_DRIVE = 3.545
|
||||
local GEAR_RATIOS = {3.827, 2.360, 1.685, 1.312, 1.000, 0.793}
|
||||
local GEAR_TORQUE_COMPENSATION = {1.0, 2.0, 4.0, 5.0, 6.0, 7.0}
|
||||
local REVERSE_RATIO = 3.280
|
||||
local IDLE_RPM = 900
|
||||
local REDLINE_RPM = 8000
|
||||
local SHIFT_TIME = 0.18
|
||||
local DRIVELINE_EFFICIENCY = 0.85
|
||||
-- Converts RB26 crank torque to Rapier's constrained axle-motor force scale.
|
||||
local JOINT_TORQUE_CALIBRATION = 2.5
|
||||
local TWO_PI = math.pi * 2
|
||||
|
||||
local WHEELS = {
|
||||
{ key = "wheel_rl", local_pos = { 0.795353, -0.330587, -1.347033 }, driven = true, steer = false, drive_share = 0.45 },
|
||||
{ key = "wheel_rr", local_pos = {-0.796153, -0.330635, -1.347024 }, driven = true, steer = false, drive_share = 0.45 },
|
||||
{ key = "wheel_fl", local_pos = { 0.794235, -0.330559, 1.443641 }, driven = true, steer = true, drive_share = 0.05 },
|
||||
{ key = "wheel_fr", local_pos = {-0.796149, -0.330652, 1.443556 }, driven = true, steer = true, drive_share = 0.05 },
|
||||
}
|
||||
|
||||
local spawned = false
|
||||
local occupied = false
|
||||
local pending_spawn = false
|
||||
local previous = {}
|
||||
local previous_buttons = 0
|
||||
local previous_y = false
|
||||
local elapsed = 0
|
||||
local enter_after = 0
|
||||
local gear = 1
|
||||
local shift_timer = 0
|
||||
local pending_pose = nil
|
||||
local pending_reenter = false
|
||||
local reset_was_down = false
|
||||
|
||||
local function pad()
|
||||
return sdk.input.pad() or {
|
||||
buttons = 0,
|
||||
triggers = {0, 0},
|
||||
left = {0, 0},
|
||||
right = {0, 0},
|
||||
}
|
||||
end
|
||||
|
||||
local function down(key)
|
||||
return sdk.input.down(key) == true
|
||||
end
|
||||
|
||||
local function pressed(key)
|
||||
local held = down(key)
|
||||
local edge = held and not previous[key]
|
||||
previous[key] = held
|
||||
return edge
|
||||
end
|
||||
|
||||
local function y_pressed()
|
||||
local buttons = math.floor(tonumber(pad().buttons) or 0)
|
||||
local held = (buttons & 0x8000) ~= 0
|
||||
local edge = held and not previous_y
|
||||
previous_y = held
|
||||
return edge
|
||||
end
|
||||
|
||||
local function clamp(value, low, high)
|
||||
if value < low then return low end
|
||||
if value > high then return high end
|
||||
return value
|
||||
end
|
||||
|
||||
local function wheel_collider()
|
||||
local points = {}
|
||||
for index = 0, 63 do
|
||||
local angle = TWO_PI * index / 64
|
||||
local y = math.cos(angle) * WHEEL_RADIUS
|
||||
local z = math.sin(angle) * WHEEL_RADIUS
|
||||
points[#points + 1] = {-WHEEL_HALF_WIDTH, y, z}
|
||||
points[#points + 1] = { WHEEL_HALF_WIDTH, y, z}
|
||||
end
|
||||
return {type = "convex", points = points}
|
||||
end
|
||||
|
||||
local function quat_yaw(q)
|
||||
return math.atan(2 * (q[4] * q[2] + q[1] * q[3]),
|
||||
1 - 2 * (q[2] * q[2] + q[3] * q[3]))
|
||||
end
|
||||
|
||||
local function gear_ratio()
|
||||
if gear == -1 then return REVERSE_RATIO * FINAL_DRIVE end
|
||||
if gear == 0 then return nil end
|
||||
return GEAR_RATIOS[gear] * FINAL_DRIVE
|
||||
end
|
||||
|
||||
local function gear_label()
|
||||
if gear == -1 then return "R" end
|
||||
if gear == 0 then return "N" end
|
||||
return tostring(gear)
|
||||
end
|
||||
|
||||
local function engine_torque(rpm)
|
||||
local curve = {
|
||||
{900, 210},
|
||||
{2000, 300},
|
||||
{3000, 350},
|
||||
{4400, 392},
|
||||
{6800, 380},
|
||||
{8000, 300},
|
||||
}
|
||||
rpm = clamp(rpm, curve[1][1], curve[#curve][1])
|
||||
for index = 2, #curve do
|
||||
local high = curve[index]
|
||||
if rpm <= high[1] then
|
||||
local low = curve[index - 1]
|
||||
local t = (rpm - low[1]) / (high[1] - low[1])
|
||||
return low[2] + (high[2] - low[2]) * t
|
||||
end
|
||||
end
|
||||
return curve[#curve][2]
|
||||
end
|
||||
|
||||
local function yaw_quat(heading)
|
||||
local half = heading * 0.5
|
||||
return {0, math.sin(half), 0, math.cos(half)}
|
||||
end
|
||||
|
||||
local function rotate(q, v)
|
||||
local x, y, z, w = q[1], q[2], q[3], q[4]
|
||||
local ix = w * v[1] + y * v[3] - z * v[2]
|
||||
local iy = w * v[2] + z * v[1] - x * v[3]
|
||||
local iz = w * v[3] + x * v[2] - y * v[1]
|
||||
local iw = -x * v[1] - y * v[2] - z * v[3]
|
||||
return {
|
||||
ix * w + iw * -x + iy * -z - iz * -y,
|
||||
iy * w + iw * -y + iz * -x - ix * -z,
|
||||
iz * w + iw * -z + ix * -y - iy * -x,
|
||||
}
|
||||
end
|
||||
|
||||
local function relative_yaw(a, b)
|
||||
local ax, ay, az, aw = -a[1], -a[2], -a[3], a[4]
|
||||
local bx, by, bz, bw = b[1], b[2], b[3], b[4]
|
||||
local x = aw * bx + ax * bw + ay * bz - az * by
|
||||
local y = aw * by - ax * bz + ay * bw + az * bx
|
||||
local z = aw * bz + ax * by - ay * bx + az * bw
|
||||
local w = aw * bw - ax * bx - ay * by - az * bz
|
||||
return math.atan(2 * (w * y + x * z), 1 - 2 * (y * y + z * z))
|
||||
end
|
||||
|
||||
local function add(a, b)
|
||||
return {a[1] + b[1], a[2] + b[2], a[3] + b[3]}
|
||||
end
|
||||
|
||||
local function terrain_y(x, z)
|
||||
local hit = sdk.physics.raycast(
|
||||
{x, 500, z},
|
||||
{0, -1, 0},
|
||||
{max_distance = 1000, filter = "ground"}
|
||||
)
|
||||
return hit and hit.point and hit.point[2] or nil
|
||||
end
|
||||
|
||||
local function carrier_key(wheel)
|
||||
return "carrier_" .. wheel.key
|
||||
end
|
||||
|
||||
local function knuckle_key(wheel)
|
||||
return "knuckle_" .. wheel.key
|
||||
end
|
||||
|
||||
local function suspension_key(wheel)
|
||||
return "suspension_" .. wheel.key
|
||||
end
|
||||
|
||||
local function axle_key(wheel)
|
||||
return "axle_" .. wheel.key
|
||||
end
|
||||
|
||||
local function steering_key(wheel)
|
||||
return "steering_" .. wheel.key
|
||||
end
|
||||
|
||||
local function wheel_contact(key)
|
||||
for _, pair in ipairs(sdk.physics.touching() or {}) do
|
||||
if (pair.a == key and pair.b == "ground")
|
||||
or (pair.b == key and pair.a == "ground") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function stop_motor(key)
|
||||
sdk.physics.joint_motor(key, {
|
||||
mode = "velocity",
|
||||
target_velocity = 0,
|
||||
factor = 0,
|
||||
max_force = 0,
|
||||
})
|
||||
end
|
||||
|
||||
local function steer_to(wheel, target)
|
||||
local carrier = sdk.physics.read(carrier_key(wheel))
|
||||
local knuckle = sdk.physics.read(knuckle_key(wheel))
|
||||
local current = 0
|
||||
if carrier and carrier.rotation and knuckle and knuckle.rotation then
|
||||
current = relative_yaw(carrier.rotation, knuckle.rotation)
|
||||
end
|
||||
sdk.physics.joint_motor(steering_key(wheel), {
|
||||
mode = "velocity",
|
||||
target_velocity = clamp((target - current) * 10, -5, 5),
|
||||
factor = 5000,
|
||||
max_force = 30000,
|
||||
})
|
||||
end
|
||||
|
||||
local function steering_targets(input, speed)
|
||||
local angle = -input * STEER_LIMIT / (1 + speed * speed * 0.0015)
|
||||
if math.abs(angle) < 0.001 then
|
||||
return {wheel_fl = 0, wheel_fr = 0}
|
||||
end
|
||||
local sign = angle < 0 and -1 or 1
|
||||
local radius = WHEELBASE / math.tan(math.abs(angle))
|
||||
local inner = math.atan(WHEELBASE / math.max(0.2, radius - TRACK_WIDTH * 0.5))
|
||||
local outer = math.atan(WHEELBASE / (radius + TRACK_WIDTH * 0.5))
|
||||
if sign < 0 then
|
||||
return {wheel_fl = -outer, wheel_fr = -inner}
|
||||
end
|
||||
return {wheel_fl = inner, wheel_fr = outer}
|
||||
end
|
||||
|
||||
local function wheel_omega(wheel)
|
||||
local body = sdk.physics.read(wheel.key)
|
||||
if not body or not body.rotation or not body.angvel then return 0 end
|
||||
local axle = rotate(body.rotation, {1, 0, 0})
|
||||
return body.angvel[1] * axle[1]
|
||||
+ body.angvel[2] * axle[2]
|
||||
+ body.angvel[3] * axle[3]
|
||||
end
|
||||
|
||||
local function driven_wheel_omega()
|
||||
local total, count = 0, 0
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
if wheel.driven then
|
||||
total = total + math.abs(wheel_omega(wheel))
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count > 0 and total / count or 0
|
||||
end
|
||||
|
||||
local function remove_rig()
|
||||
sdk.player.detach()
|
||||
sdk.camera.clear_follow()
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
sdk.physics.remove_joint(axle_key(wheel))
|
||||
sdk.physics.remove_joint(suspension_key(wheel))
|
||||
if wheel.steer then
|
||||
sdk.physics.remove_joint(steering_key(wheel))
|
||||
sdk.physics.remove(knuckle_key(wheel))
|
||||
end
|
||||
sdk.physics.remove(wheel.key)
|
||||
sdk.physics.remove(carrier_key(wheel))
|
||||
end
|
||||
sdk.physics.remove(CHASSIS)
|
||||
sdk.graphics.remove("skyline_visual")
|
||||
spawned = false
|
||||
occupied = false
|
||||
end
|
||||
|
||||
local function spawn_rig()
|
||||
local player = sdk.player.read()
|
||||
local pose = pending_pose
|
||||
pending_pose = nil
|
||||
local heading = pose and pose.heading or player.heading or 0
|
||||
local rotation = yaw_quat(heading)
|
||||
local forward = rotate(rotation, {0, 0, 1})
|
||||
local x = pose and pose.x or player.position[1] + forward[1] * 4
|
||||
local z = pose and pose.z or player.position[3] + forward[3] * 4
|
||||
local chassis_y = -math.huge
|
||||
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
local offset = rotate(rotation, wheel.local_pos)
|
||||
local ground = terrain_y(x + offset[1], z + offset[3])
|
||||
if not ground then
|
||||
sdk.log("skyline: no Rapier terrain below wheel " .. wheel.key)
|
||||
return false
|
||||
end
|
||||
chassis_y = math.max(chassis_y, ground + WHEEL_RADIUS - offset[2])
|
||||
end
|
||||
|
||||
local chassis_position = {x, chassis_y, z}
|
||||
sdk.physics.spawn(CHASSIS, {
|
||||
shape = {type = "mesh", path = MODEL, object = "skyline_mesh"},
|
||||
body_type = "dynamic",
|
||||
mass = 1400,
|
||||
position = chassis_position,
|
||||
heading = heading,
|
||||
friction = 0.45,
|
||||
membership = 1,
|
||||
filter = 1,
|
||||
ccd = true,
|
||||
center_of_mass = {0, -0.35, -0.18},
|
||||
inertia_half_extents = {1.02, 0.61, 2.43},
|
||||
linear_damping = 0.025,
|
||||
angular_damping = 0.12,
|
||||
})
|
||||
|
||||
local tire_friction = sdk.settings.tire_friction or 1.3
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
local center = add(chassis_position, rotate(rotation, wheel.local_pos))
|
||||
local carrier = carrier_key(wheel)
|
||||
sdk.physics.spawn(carrier, {
|
||||
shape = {type = "sphere", radius = 0.055},
|
||||
body_type = "dynamic",
|
||||
mass = 30,
|
||||
position = center,
|
||||
heading = heading,
|
||||
friction = 0,
|
||||
sensor = true,
|
||||
ccd = true,
|
||||
inertia_half_extents = {0.18, 0.18, 0.18},
|
||||
linear_damping = 0.05,
|
||||
angular_damping = 0.2,
|
||||
})
|
||||
sdk.physics.spawn(wheel.key, {
|
||||
-- Smooth, GLB-dimensioned tire proxy. The authored tread is visually
|
||||
-- correct but produces polygon impacts when used as a rolling collider.
|
||||
shape = wheel_collider(),
|
||||
body_type = "dynamic",
|
||||
mass = 28,
|
||||
position = center,
|
||||
heading = heading,
|
||||
friction = tire_friction,
|
||||
membership = 2,
|
||||
filter = 2,
|
||||
ccd = true,
|
||||
linear_damping = 0.01,
|
||||
angular_damping = 0.015,
|
||||
})
|
||||
sdk.physics.prismatic(suspension_key(wheel), {
|
||||
body_a = CHASSIS,
|
||||
body_b = carrier,
|
||||
anchor_a = wheel.local_pos,
|
||||
anchor_b = {0, 0, 0},
|
||||
axis = {0, -1, 0},
|
||||
limits = {-SUSPENSION_TRAVEL, SUSPENSION_TRAVEL},
|
||||
contacts_enabled = false,
|
||||
})
|
||||
sdk.physics.joint_spring(suspension_key(wheel), {
|
||||
position = 0,
|
||||
stiffness = SUSPENSION_STIFFNESS,
|
||||
damping = SUSPENSION_DAMPING,
|
||||
max_force = 18000,
|
||||
})
|
||||
|
||||
local axle_parent = carrier
|
||||
if wheel.steer then
|
||||
local knuckle = knuckle_key(wheel)
|
||||
sdk.physics.spawn(knuckle, {
|
||||
shape = {type = "sphere", radius = 0.05},
|
||||
body_type = "dynamic",
|
||||
mass = 24,
|
||||
position = center,
|
||||
heading = heading,
|
||||
friction = 0,
|
||||
sensor = true,
|
||||
ccd = true,
|
||||
inertia_half_extents = {0.20, 0.22, 0.20},
|
||||
linear_damping = 0.05,
|
||||
angular_damping = 0.2,
|
||||
})
|
||||
sdk.physics.revolute(steering_key(wheel), {
|
||||
body_a = carrier,
|
||||
body_b = knuckle,
|
||||
anchor_a = {0, 0, 0},
|
||||
anchor_b = {0, 0, 0},
|
||||
axis = {0, 1, 0},
|
||||
limits = {-STEER_LIMIT, STEER_LIMIT},
|
||||
contacts_enabled = false,
|
||||
})
|
||||
sdk.physics.joint_motor(steering_key(wheel), {
|
||||
mode = "velocity",
|
||||
target_velocity = 0,
|
||||
factor = 5000,
|
||||
max_force = 30000,
|
||||
})
|
||||
axle_parent = knuckle
|
||||
end
|
||||
|
||||
sdk.physics.revolute(axle_key(wheel), {
|
||||
body_a = axle_parent,
|
||||
body_b = wheel.key,
|
||||
anchor_a = {0, 0, 0},
|
||||
anchor_b = {0, 0, 0},
|
||||
axis = {1, 0, 0},
|
||||
contacts_enabled = false,
|
||||
})
|
||||
stop_motor(axle_key(wheel))
|
||||
end
|
||||
|
||||
sdk.graphics.mesh("skyline_visual", {path = MODEL, body = CHASSIS})
|
||||
spawned = true
|
||||
gear = 1
|
||||
shift_timer = 0
|
||||
previous_buttons = 0
|
||||
previous_y = false
|
||||
reset_was_down = false
|
||||
enter_after = elapsed + 0.5
|
||||
if pending_reenter then
|
||||
sdk.player.attach(CHASSIS, SEAT)
|
||||
sdk.camera.follow(CHASSIS, CAMERA)
|
||||
occupied = true
|
||||
end
|
||||
pending_reenter = false
|
||||
sdk.log("skyline: spawned physical 11-body/10-joint assembly")
|
||||
return true
|
||||
end
|
||||
|
||||
local function request_respawn()
|
||||
if spawned then
|
||||
remove_rig()
|
||||
end
|
||||
pending_pose = nil
|
||||
pending_reenter = false
|
||||
pending_spawn = true
|
||||
end
|
||||
|
||||
local function request_reset(car)
|
||||
pending_pose = {
|
||||
x = car.position[1],
|
||||
z = car.position[3],
|
||||
heading = quat_yaw(car.rotation),
|
||||
}
|
||||
pending_reenter = occupied
|
||||
remove_rig()
|
||||
pending_spawn = true
|
||||
end
|
||||
|
||||
local function drive(car, dt)
|
||||
local controller = pad()
|
||||
local triggers = controller.triggers or {0, 0}
|
||||
local throttle = clamp(
|
||||
(tonumber(triggers[2]) or 0) + (down("KeyW") and 1 or 0),
|
||||
0,
|
||||
1
|
||||
)
|
||||
local steer = clamp(
|
||||
((controller.left or {0, 0})[1] or 0)
|
||||
+ (down("KeyD") and 1 or 0) - (down("KeyA") and 1 or 0),
|
||||
-1,
|
||||
1
|
||||
)
|
||||
local buttons = math.floor(tonumber(controller.buttons) or 0)
|
||||
local rb = (buttons & 0x0200) ~= 0
|
||||
local lb = (buttons & 0x0100) ~= 0
|
||||
local old_rb = (previous_buttons & 0x0200) ~= 0
|
||||
local old_lb = (previous_buttons & 0x0100) ~= 0
|
||||
if rb and not old_rb and gear < #GEAR_RATIOS then
|
||||
gear = gear + 1
|
||||
shift_timer = SHIFT_TIME
|
||||
elseif lb and not old_lb and gear > -1 then
|
||||
gear = gear - 1
|
||||
shift_timer = SHIFT_TIME
|
||||
end
|
||||
previous_buttons = buttons
|
||||
shift_timer = math.max(0, shift_timer - dt)
|
||||
|
||||
local brake_amount = clamp(
|
||||
(tonumber(triggers[1]) or 0) + (down("KeyS") and 1 or 0),
|
||||
0,
|
||||
1
|
||||
)
|
||||
local foot_braking = brake_amount > 0.01 or down("Space")
|
||||
or ((buttons & 0x1000) ~= 0)
|
||||
local handbrake = down("ShiftLeft") or ((buttons & 0x2000) ~= 0)
|
||||
local ratio = gear_ratio()
|
||||
local wheel_rpm = driven_wheel_omega() * 60 / TWO_PI
|
||||
local raw_rpm = ratio and wheel_rpm * ratio or 0
|
||||
local rpm = ratio and clamp(raw_rpm, IDLE_RPM, REDLINE_RPM) or IDLE_RPM
|
||||
-- Keep the velocity servo out of the torque curve. Engine torque is capped
|
||||
-- separately at redline; targeting redline directly would taper motor force
|
||||
-- thousands of RPM early as velocity error shrinks.
|
||||
local target_omega = ratio and REDLINE_RPM * 1.5 * TWO_PI / 60 / ratio or 0
|
||||
if gear == -1 then target_omega = -target_omega end
|
||||
local on_limiter = raw_rpm >= REDLINE_RPM
|
||||
local total_axle_torque = ratio
|
||||
and engine_torque(rpm) * ratio * DRIVELINE_EFFICIENCY * JOINT_TORQUE_CALIBRATION
|
||||
* (gear > 0 and GEAR_TORQUE_COMPENSATION[gear] or 1)
|
||||
or 0
|
||||
local steering = steering_targets(steer, car.speed or 0)
|
||||
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
if wheel.steer then
|
||||
steer_to(wheel, steering[wheel.key])
|
||||
end
|
||||
if wheel.driven then
|
||||
if foot_braking or (handbrake and not wheel.steer) then
|
||||
sdk.physics.joint_motor(axle_key(wheel), {
|
||||
mode = "velocity",
|
||||
target_velocity = 0,
|
||||
factor = 120,
|
||||
max_force = handbrake and not wheel.steer and 12000 or 9000,
|
||||
})
|
||||
elseif handbrake then
|
||||
stop_motor(axle_key(wheel))
|
||||
elseif throttle > 0.01 and shift_timer <= 0 and ratio and not on_limiter then
|
||||
-- ATTESA closes toward even AWD for a straight launch, then releases
|
||||
-- the front axle during cornering so throttle can rotate the rear.
|
||||
local drive_share = 0.25
|
||||
if math.abs(steer) > 0.15 then
|
||||
drive_share = wheel.steer and 0.05 or 0.45
|
||||
end
|
||||
sdk.physics.joint_motor(axle_key(wheel), {
|
||||
mode = "velocity",
|
||||
target_velocity = target_omega,
|
||||
factor = 10000,
|
||||
max_force = total_axle_torque * drive_share * throttle,
|
||||
})
|
||||
else
|
||||
stop_motor(axle_key(wheel))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if sdk.settings.show_debug ~= false then
|
||||
local contacts = 0
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
if wheel_contact(wheel.key) then contacts = contacts + 1 end
|
||||
end
|
||||
sdk.ui.text("skyline_status", string.format(
|
||||
"Skyline physics | %.0f km/h | gear %s | %.0f rpm | tires %d/4 | throttle %.0f%% | steer %.0f%%",
|
||||
(car.speed or 0) * 3.6,
|
||||
gear_label(),
|
||||
rpm,
|
||||
contacts,
|
||||
throttle * 100,
|
||||
steer * 100
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
on_load = function()
|
||||
sdk.ui.text("skyline_help", "Skyline — F10 spawn · Y/E enter · RT throttle · LT/A brake · B/Shift handbrake · RB/LB shift · R/R3 reset")
|
||||
end,
|
||||
|
||||
on_unload = function()
|
||||
remove_rig()
|
||||
end,
|
||||
|
||||
on_fixed_update = function(event)
|
||||
elapsed = elapsed + (event and event.dt or 1 / 120)
|
||||
|
||||
if pressed("F10") then
|
||||
request_respawn()
|
||||
return
|
||||
end
|
||||
if pending_spawn then
|
||||
pending_spawn = false
|
||||
spawn_rig()
|
||||
return
|
||||
end
|
||||
|
||||
local car = sdk.physics.read(CHASSIS)
|
||||
if not car then return end
|
||||
|
||||
local buttons = math.floor(tonumber(pad().buttons) or 0)
|
||||
local reset_down = (buttons & 0x0080) ~= 0
|
||||
local reset_pressed = reset_down and not reset_was_down
|
||||
reset_was_down = reset_down
|
||||
if pressed("KeyR") or reset_pressed then
|
||||
request_reset(car)
|
||||
return
|
||||
end
|
||||
|
||||
local enter_pressed = pressed("KeyE")
|
||||
local controller_enter = y_pressed()
|
||||
if enter_pressed or controller_enter then
|
||||
if occupied then
|
||||
sdk.player.detach()
|
||||
sdk.camera.clear_follow()
|
||||
occupied = false
|
||||
elseif elapsed >= enter_after then
|
||||
local player = sdk.player.read()
|
||||
local dx = car.position[1] - player.position[1]
|
||||
local dz = car.position[3] - player.position[3]
|
||||
if dx * dx + dz * dz <= ENTER_RANGE * ENTER_RANGE then
|
||||
sdk.player.attach(CHASSIS, SEAT)
|
||||
sdk.camera.follow(CHASSIS, CAMERA)
|
||||
occupied = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if occupied then
|
||||
drive(car, event and event.dt or 1 / 120)
|
||||
else
|
||||
for _, wheel in ipairs(WHEELS) do
|
||||
if wheel.driven then stop_motor(axle_key(wheel)) end
|
||||
if wheel.steer then
|
||||
steer_to(wheel, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
on_event = function(event)
|
||||
if event.name == "world_changed" then
|
||||
spawned = false
|
||||
occupied = false
|
||||
pending_spawn = false
|
||||
previous = {}
|
||||
previous_buttons = 0
|
||||
previous_y = false
|
||||
gear = 1
|
||||
shift_timer = 0
|
||||
pending_pose = nil
|
||||
pending_reenter = false
|
||||
reset_was_down = false
|
||||
end
|
||||
end,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "examples.skyline",
|
||||
"api": 2,
|
||||
"name": "Skyline Physics",
|
||||
"version": "1.0.0",
|
||||
"author": "Skate3clone",
|
||||
"description": "GLB rigid-body Skyline: physical suspension, steering hinges, axle motors, and Rapier tire contact.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"tire_friction": {
|
||||
"type": "number",
|
||||
"label": "Tire friction",
|
||||
"description": "Rapier friction coefficient on the four GLB wheel hulls.",
|
||||
"default": 1.3,
|
||||
"min": 0.2,
|
||||
"max": 2,
|
||||
"step": 0.05
|
||||
},
|
||||
"show_debug": {
|
||||
"type": "boolean",
|
||||
"label": "Physics HUD",
|
||||
"description": "Show wheel contacts, speed, gear, engine RPM, and steering.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+202
-160
@@ -1,191 +1,233 @@
|
||||
---@meta
|
||||
-- SDK 1 language-server declarations. Add this directory as a Lua workspace library.
|
||||
-- This file is not executed. Runtime reference: docs/lua-modding.md.
|
||||
---@alias Vec3 number[] Three finite components, 1-based, Y up, metres.
|
||||
---@alias SettingValue boolean|number|string
|
||||
---@class GrindSnapshot
|
||||
---@field active boolean
|
||||
---@field name string
|
||||
---@field kind integer
|
||||
---@field distance number
|
||||
-- SDK 2 language-server declarations. Not executed at runtime.
|
||||
---@alias Vec3 number[]
|
||||
---@alias Quat number[] xyzw
|
||||
---@class BodyDesc
|
||||
---@field shape {type:'box'|'sphere'|'capsule'|'convex'|'mesh', half_extents?:Vec3, radius?:number, half_height?:number, points?:Vec3[], path?:string, object?:string}
|
||||
---@field body_type 'dynamic'|'kinematic'|'static'
|
||||
---@field mass? number
|
||||
---@field position? Vec3
|
||||
---@field heading? number
|
||||
---@field friction? number
|
||||
---@field ccd? boolean
|
||||
---@field sensor? boolean
|
||||
---@field membership? integer
|
||||
---@field filter? integer
|
||||
---@field center_of_mass? Vec3 body-local COM (weight transfer under corner loads)
|
||||
---@field collider_offset? Vec3 chassis-local collider translation (legacy vehicle.json)
|
||||
---@field inertia_half_extents? Vec3 optional inertia box; with COM sets MassProperties
|
||||
---@field linear_damping? number default 0.08
|
||||
---@field angular_damping? number default 0.5
|
||||
---@class SpringRayOpts
|
||||
---@field local_origin Vec3
|
||||
---@field local_direction? Vec3 default {0,-1,0}
|
||||
---@field rest_length number
|
||||
---@field max_travel? number
|
||||
---@field contact_radius? number
|
||||
---@field stiffness number
|
||||
---@field compression? number
|
||||
---@field relaxation? number
|
||||
---@field damping? number alias for compression/relaxation
|
||||
---@field max_force? number
|
||||
---@field dt number
|
||||
---@class SpringRayHit
|
||||
---@field in_contact boolean
|
||||
---@field point Vec3
|
||||
---@field normal Vec3
|
||||
---@field hard_point Vec3
|
||||
---@field direction_ws Vec3
|
||||
---@field suspension_length number
|
||||
---@field load number
|
||||
---@field relative_velocity number
|
||||
---@class BodySnapshot
|
||||
---@field position Vec3
|
||||
---@field rotation Quat
|
||||
---@field linvel Vec3
|
||||
---@field angvel Vec3
|
||||
---@field force Vec3 last sdk.physics.force this tick (zeros if none); Rapier user forces are cleared each tick
|
||||
---@field torque Vec3 last sdk.physics.torque this tick
|
||||
---@field mass number
|
||||
---@field speed number |linvel|
|
||||
---@class PlayerSnapshot
|
||||
---@field position Vec3 Animation root position.
|
||||
---@field velocity Vec3 Physical board velocity, including when offboard.
|
||||
---@field heading number Animation-root yaw in radians, zero faces +Z.
|
||||
---@field position Vec3
|
||||
---@field velocity Vec3
|
||||
---@field heading number
|
||||
---@field on_board boolean
|
||||
---@field state integer Native state identifier, not a trick name.
|
||||
---@field state integer
|
||||
---@field category integer
|
||||
---@field bailing boolean
|
||||
---@field grind GrindSnapshot
|
||||
---@class MapSnapshot
|
||||
---@field name string
|
||||
---@field generation integer
|
||||
---@class AnimationInfo
|
||||
---@field bone_names string[]
|
||||
---@field slots table<string, {fps:number,frame_count:integer}>
|
||||
---@class ContactEvent
|
||||
---@field a string|nil body key or `"ground"` when the other side is owned
|
||||
---@field b string|nil body key or `"ground"`
|
||||
---@field started boolean true on pair begin, false on end
|
||||
---@class TouchingPair
|
||||
---@field a string|nil body key or `"ground"`
|
||||
---@field b string|nil body key or `"ground"`
|
||||
---@class PadSnapshot
|
||||
---@field buttons integer XInput button bits
|
||||
---@field triggers number[] LT, RT in 0..1
|
||||
---@field left number[] stick XY
|
||||
---@field right number[] stick XY
|
||||
---@class NetworkInfo
|
||||
---@field active boolean
|
||||
---@field local_id string
|
||||
---@field is_host boolean
|
||||
---@field states table<string, table<string, table<string, any>>> mod_id → peer_id → key → value
|
||||
---@field status string
|
||||
---@class SDKSnapshot
|
||||
---@field player PlayerSnapshot
|
||||
---@field map MapSnapshot
|
||||
---@field animation AnimationInfo
|
||||
---@field network NetworkInfo
|
||||
---@field map {name:string, generation:integer}
|
||||
---@field tick integer
|
||||
---@field keys table<string,boolean>
|
||||
---@field actions number[] 18 values; Lua index 1 corresponds to native action 64.
|
||||
---@field actions number[]
|
||||
---@field pad PadSnapshot
|
||||
---@field paused boolean
|
||||
---@field replay boolean
|
||||
---@field attach? {body:string, owner:string}
|
||||
---@field physics {bodies:table<string,BodySnapshot>, contacts:ContactEvent[], touching:TouchingPair[]}
|
||||
---@field network? NetworkInfo
|
||||
---@class ModCallbacks
|
||||
---@field on_load? fun()
|
||||
---@field on_unload? fun() Commands discarded; host always cleans up.
|
||||
---@field on_update? fun(event:{dt:number}) Active render time; max dt 0.25 seconds.
|
||||
---@field on_fixed_update? fun(event:{dt:number}) After native physics, before next input publication.
|
||||
---@field on_settings? fun(event:{key:string,value:SettingValue}) sdk.settings already updated.
|
||||
---@field on_event? fun(event:ModEvent)
|
||||
---@class ModEvent
|
||||
---@field name 'world_changed'|'bail_changed'|'player_state_changed'|'grind_changed'
|
||||
---@field map? MapSnapshot world_changed only
|
||||
---@field bailing? boolean bail_changed only
|
||||
---@field previous? integer player_state_changed only
|
||||
---@field state? integer player_state_changed only
|
||||
---@field grind? GrindSnapshot grind_changed only
|
||||
---@field on_unload? fun()
|
||||
---@field on_update? fun(event:{dt:number})
|
||||
---@field on_fixed_update? fun(event:{dt:number})
|
||||
---@field on_settings? fun(event:{key:string,value:any})
|
||||
---@field on_event? fun(event:{name:string})
|
||||
sdk = {
|
||||
api_version = 1,
|
||||
---@type table<string,SettingValue>
|
||||
api_version = 2,
|
||||
---@type string
|
||||
mod_id = "",
|
||||
---@type table<string, any>
|
||||
settings = {},
|
||||
---@type SDKSnapshot
|
||||
snapshot = {},
|
||||
player = {}, input = {}, ui = {}, scene = {}, animation = {},
|
||||
time = {elapsed = 0},
|
||||
physics = {}, graphics = {}, player = {}, camera = {}, input = {}, ui = {}, net = {}, assets = {},
|
||||
time = { elapsed = 0 },
|
||||
}
|
||||
---@param text string At most 2048 UTF-8 bytes; queues labelled INFO output.
|
||||
---@param text string
|
||||
function sdk.log(text) end
|
||||
---@param path string Relative within mod root; max 256 KiB, UTF-8 only; errors on failure.
|
||||
---@param path string
|
||||
---@return string
|
||||
function sdk.read_text(path) end
|
||||
---@param path string package-relative .glb
|
||||
---@return {nodes:string[], meshes:string[]}
|
||||
function sdk.assets.objects(path) end
|
||||
---@param key string
|
||||
---@param body BodyDesc
|
||||
function sdk.physics.spawn(key, body) end
|
||||
---@param key string
|
||||
function sdk.physics.remove(key) end
|
||||
---@param key string
|
||||
---@param opts {shape:{type:'mesh'|'convex', path?:string, object?:string, points?:Vec3[]}, position?:Vec3, friction?:number}
|
||||
function sdk.physics.add_collider(key, opts) end
|
||||
---@param key string
|
||||
---@param force Vec3
|
||||
---@param point? Vec3
|
||||
function sdk.physics.force(key, force, point) end
|
||||
---@param key string
|
||||
---@param impulse Vec3
|
||||
---@param point? Vec3
|
||||
function sdk.physics.impulse(key, impulse, point) end
|
||||
---@param key string
|
||||
---@param torque Vec3
|
||||
function sdk.physics.torque(key, torque) end
|
||||
---@param key string
|
||||
---@param torque Vec3
|
||||
function sdk.physics.torque_impulse(key, torque) end
|
||||
---@param origin Vec3
|
||||
---@param direction Vec3
|
||||
---@param opts? {max_distance?:number, filter?:'all'|'ground', exclude?:string[]}
|
||||
---@return {body:string|nil, point:Vec3, normal:Vec3, toi:number}|nil
|
||||
function sdk.physics.raycast(origin, direction, opts) end
|
||||
---@param key string
|
||||
---@param point Vec3
|
||||
---@return Vec3|nil
|
||||
function sdk.physics.velocity_at(key, point) end
|
||||
---@param key string
|
||||
---@param point Vec3
|
||||
---@param direction Vec3
|
||||
---@return number|nil
|
||||
function sdk.physics.effective_inv_mass(key, point, direction) end
|
||||
---@param key string
|
||||
---@param opts SpringRayOpts
|
||||
---@return SpringRayHit|nil
|
||||
function sdk.physics.spring_ray(key, opts) end
|
||||
---@param key string
|
||||
---@param local_accel Vec3
|
||||
---@param dt? number
|
||||
---@return Vec3|nil
|
||||
function sdk.physics.local_ang_accel_impulse(key, local_accel, dt) end
|
||||
---@param key string
|
||||
---@param linvel Vec3
|
||||
function sdk.physics.set_linvel(key, linvel) end
|
||||
---@param key string
|
||||
---@param angvel Vec3
|
||||
function sdk.physics.set_angvel(key, angvel) end
|
||||
---@param key string
|
||||
---@param position Vec3
|
||||
---@param rotation Quat
|
||||
function sdk.physics.set_pose(key, position, rotation) end
|
||||
---@param key string
|
||||
---@param joint {body_a:string, body_b:string, anchor_a:Vec3, anchor_b:Vec3, axis?:Vec3, limits?:[number,number], contacts_enabled?:boolean}
|
||||
function sdk.physics.revolute(key, joint) end
|
||||
---@param key string
|
||||
---@param joint {body_a:string, body_b:string, anchor_a?:Vec3, anchor_b?:Vec3, axis?:Vec3, limits?:[number,number], contacts_enabled?:boolean}
|
||||
function sdk.physics.prismatic(key, joint) end
|
||||
---@param key string
|
||||
---@param motor {mode:'velocity', target_velocity:number, factor?:number, max_force?:number}|{mode:'position', target_position:number, stiffness:number, damping:number, max_force?:number}
|
||||
function sdk.physics.joint_motor(key, motor) end
|
||||
---@param key string
|
||||
---@param spring {position?:number, target_position?:number, stiffness:number, damping:number, max_force?:number}
|
||||
function sdk.physics.joint_spring(key, spring) end
|
||||
---@param key string
|
||||
function sdk.physics.remove_joint(key) end
|
||||
---@param key string
|
||||
---@return BodySnapshot|nil
|
||||
function sdk.physics.read(key) end
|
||||
---@return ContactEvent[] edge events from the previous Rapier step
|
||||
function sdk.physics.contacts() end
|
||||
---@return TouchingPair[] pairs in contact after the previous Rapier step (`ground` included)
|
||||
function sdk.physics.touching() end
|
||||
---@param key string
|
||||
---@param opts {path?:string, body?:string, position?:Vec3, rotation?:Quat, scale?:Vec3, color?:Vec3, visible?:boolean}
|
||||
function sdk.graphics.mesh(key, opts) end
|
||||
---@param key string
|
||||
function sdk.graphics.remove(key) end
|
||||
---@param key string
|
||||
---@param visible boolean
|
||||
function sdk.graphics.set_visible(key, visible) end
|
||||
---@return PlayerSnapshot
|
||||
function sdk.player.read() end
|
||||
---@param position Vec3 Components within ±100000 metres; author supplies a valid destination.
|
||||
---@param heading number Radians around world Y; zero faces +Z.
|
||||
---@param on_board boolean
|
||||
function sdk.player.teleport(position, heading, on_board) end
|
||||
---@param key string Bevy physical KeyCode name, e.g. KeyJ, F6, Space. Unknown returns false.
|
||||
---@param body string
|
||||
---@param offset? Vec3
|
||||
function sdk.player.attach(body, offset) end
|
||||
function sdk.player.detach() end
|
||||
---@return string|nil
|
||||
function sdk.player.attached() end
|
||||
---@param body? string
|
||||
---@param offset? Vec3
|
||||
function sdk.camera.follow(body, offset) end
|
||||
function sdk.camera.clear_follow() end
|
||||
---@param position Vec3
|
||||
---@param look_at? Vec3
|
||||
function sdk.camera.set(position, look_at) end
|
||||
---@param key string
|
||||
---@return boolean
|
||||
function sdk.input.down(key) end
|
||||
---@param id integer Native published gameplay action 64..81; invalid ID raises an error.
|
||||
---@param id integer
|
||||
---@return number
|
||||
function sdk.input.action(id) end
|
||||
---@param key string Owner-local stable visual key, 1..64 lowercase ASCII letters/digits/._-
|
||||
---@param text string At most 1024 UTF-8 bytes.
|
||||
---@return PadSnapshot
|
||||
function sdk.input.pad() end
|
||||
---@param key string
|
||||
---@param text string
|
||||
function sdk.ui.text(key, text) end
|
||||
---@param key string Same namespace as sdk.ui.text; repeated key updates/replaces.
|
||||
---@param position Vec3
|
||||
---@param size Vec3 Each component >0 and <=100 metres.
|
||||
---@param color Vec3 Each sRGB component in 0..1.
|
||||
function sdk.scene.cube(key, position, size, color) end
|
||||
---@param key string Remove owner's text or cube; absent is a no-op.
|
||||
function sdk.scene.remove(key) end
|
||||
---@param key string Timer key, 1..64 bytes. Existing timer is replaced.
|
||||
---@param seconds number Finite delay 0..86400 active Update seconds.
|
||||
---@param callback function One-shot callback, part of the on_update transaction/quota.
|
||||
function sdk.time.after(key, seconds, callback) end
|
||||
---@param key string Absent is a no-op.
|
||||
function sdk.time.cancel(key) end
|
||||
---@return AnimationInfo
|
||||
function sdk.animation.info() end
|
||||
---@param path string Package-relative original body-clip JSON, <=4 MiB; host validates supported slots and exact timing.
|
||||
function sdk.animation.replace(path) end
|
||||
|
||||
---@class TrainerTuning Numeric fields default to 1; multipliers of current stock settings.
|
||||
---@field pop? number 0.25..4, native ground pop heights.
|
||||
---@field grind_pop? number 0.25..4, native grind launch height.
|
||||
---@field push_speed? number 0.25..4, push target and maximum pushable speed.
|
||||
---@field push_power? number 0.25..4, native push velocity-change limits.
|
||||
---@field braking? number 0.25..4, foot and tail brake force.
|
||||
---@field steering? number 0.25..4, general steering scalar.
|
||||
---@field wobble? number 0..2, speed wobble amplitude.
|
||||
---@field offboard_jump? number 0.25..4, native biped launch height.
|
||||
---@field grip? number 0.25..4, wheel static/dynamic and sideways slide friction.
|
||||
---@field turn_power? number 0.25..4, native heading turn strength.
|
||||
---@field hold_fakie? boolean Defaults false; suppress automatic fakie stance switching.
|
||||
---@field manual_drag? number 0.25..4, manual balance linear drag.
|
||||
sdk.trainer = {}
|
||||
---@param tuning TrainerTuning Owned, reversible native tuning. Conflicting owner is rejected.
|
||||
function sdk.trainer.apply(tuning) end
|
||||
|
||||
---@class VehicleControls
|
||||
---@field pitch? number -1..1; airborne nose up to nose down (left stick vertical)
|
||||
---@field throttle? number -1..1, reverse to forward
|
||||
---@field steering? number -1..1, right to left
|
||||
---@field brake? number 0..1
|
||||
---@field handbrake? boolean
|
||||
---@class VehicleTuning
|
||||
---@field engine_volume? number 0..1; requires engine_audio.enabled in definition
|
||||
---@field engine_force? number 0..100000 N
|
||||
---@field max_speed? number 1..100 m/s, engine limit
|
||||
---@field brake_impulse? number 0..10000
|
||||
---@field steering_angle? number 0.01..1.2 radians
|
||||
---@field tire_grip? number 0.1..20
|
||||
---@class VehicleSnapshot
|
||||
---@field position number[] metres
|
||||
---@field rotation number[] quaternion xyzw
|
||||
---@field heading number radians
|
||||
---@field speed number signed m/s
|
||||
---@field phase 'parked'|'entering'|'driving'|'exiting'
|
||||
---@field occupied boolean
|
||||
---@field ready boolean
|
||||
sdk.vehicle = {}
|
||||
---@param key string Owner-local key
|
||||
---@param definition string Package-relative vehicle JSON
|
||||
---@param position number[] World position
|
||||
---@param heading? number Radians
|
||||
function sdk.vehicle.spawn(key,definition,position,heading) end
|
||||
---@param key string
|
||||
---@return VehicleSnapshot?
|
||||
function sdk.vehicle.read(key) end
|
||||
---@param key string
|
||||
---@param controls VehicleControls Refresh every fixed tick
|
||||
function sdk.vehicle.control(key,controls) end
|
||||
---@param key string
|
||||
---@param tuning VehicleTuning
|
||||
function sdk.vehicle.tune(key,tuning) end
|
||||
---@param key string
|
||||
function sdk.vehicle.enter(key) end
|
||||
---@param key string
|
||||
function sdk.vehicle.exit(key) end
|
||||
---@param key string
|
||||
---@param position number[]
|
||||
---@param heading? number
|
||||
function sdk.vehicle.reset(key,position,heading) end
|
||||
---@param key string
|
||||
function sdk.vehicle.remove(key) end
|
||||
---@return table Normalized keyboard/controller axes plus interact and pad_buttons
|
||||
function sdk.vehicle.input() end
|
||||
|
||||
---@class VehicleBailEvent
|
||||
---@field name 'vehicle_bailed'
|
||||
---@field owner string
|
||||
---@field key string
|
||||
---@field reason 'crash'|'rider_impact'|'inverted'
|
||||
---@field position number[] World seat position at detection, metres
|
||||
---@field velocity number[] Carried world velocity plus launch lift, m/s
|
||||
---@field angular_velocity number[] World angular velocity, rad/s
|
||||
-- Configure rider_safety in the vehicle definition; see docs/vehicle-sdk.md.
|
||||
|
||||
---@class NetworkInfo
|
||||
---@field active boolean
|
||||
---@field local_id string Keep 64-bit player IDs as strings. Offline: "0".
|
||||
---@field is_host boolean
|
||||
---@field states table<string,table<string,table<string,any>>> Mod ID -> peer ID -> state keys.
|
||||
---@field status string Matching/limit diagnostics.
|
||||
sdk.net = {}
|
||||
---@return NetworkInfo
|
||||
function sdk.net.info() end
|
||||
---@param key string Stable owner-local key, 1..64 characters.
|
||||
---@param value any JSON-compatible value, at most 512 encoded bytes; nil clears.
|
||||
function sdk.net.publish(key,value) end
|
||||
---@param peer string Player ID from sdk.net.info(), never converted to a number.
|
||||
---@param key string
|
||||
---@return any Latest value for this mod/peer/key, or nil.
|
||||
function sdk.net.read(peer,key) end
|
||||
-- State coalesces; this is not an exactly-once event channel.
|
||||
-- World cubes and owned vehicles replicate automatically for matching enabled mods.
|
||||
-- See docs/multiplayer-mods.md for ownership, collision and late-join behavior.
|
||||
---@param value any JSON-compatible; at most 512 encoded bytes; nil clears
|
||||
function sdk.net.publish(key, value) end
|
||||
---@param peer string|number peer id from info().local_id / remote peers
|
||||
---@param key string
|
||||
---@return any|nil
|
||||
function sdk.net.read(peer, key) end
|
||||
Reference in new issue
Block a user