mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Bundle only Skyline in the installed mods and fresh releases
This commit is contained in:
1 parent
dfd3e543c0
commit
60efdef866
17 files changed
+9
-1498
No files matched your search
@@ -1,185 +0,0 @@
|
||||
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,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# Broken Bones
|
||||
|
||||
Enable **Broken Bones** in Mods on the matching generalized-API build. A hard
|
||||
limb impact releases that joint's angular limits and the limb's animation and
|
||||
board-holding drives. The limb stays attached at its linear joint anchor.
|
||||
|
||||
Use **Skater mods > Broken bones > Heal all injuries** to restore this mod's
|
||||
overrides. Turning off Enable injuries also heals. Map changes/unload restore
|
||||
controls. Heal on teleport is enabled by default. Normal bail recovery alone
|
||||
does not heal injuries.
|
||||
|
||||
Settings expose impulse threshold (default 80 N s), minimum closing speed
|
||||
(3 m/s), cooldown, bail-only detection and HUD visibility. Thresholds are gameplay
|
||||
settings, not medical fracture estimates. Resting support force alone cannot
|
||||
cause an injury. Joint names and indices come from the loaded native rig.
|
||||
|
||||
All injury logic is in `main.lua`. The host only exposes body/contact observations,
|
||||
joint and drive controls, menu actions, networking and command results. A failed
|
||||
override is reported instead of falsely marking a joint injured. Owned overrides
|
||||
are restored without clearing another mod's direct joint settings.
|
||||
|
||||
With the mod enabled on peers, the HUD displays remote injury counts; native
|
||||
physical poses use the existing multiplayer pose stream. There are no detached
|
||||
limb meshes or blood effects. Headless Lua/native integration tests cover the
|
||||
rules and constraint controls; injury feel still needs gameplay evaluation.
|
||||
@@ -1,3 +0,0 @@
|
||||
5d30691ecb4b3865c462680685a3c146fe7fb6e8fd0b41d5e951fb93fe6a20ae main.lua
|
||||
1461c828f1fad08bb6f7f96ddde56368af499caca6deb7a709d3ad34bdcf8378 mod.json
|
||||
6b2ae2e24202b97ca88173cca3412e0c6ba1245fbf2896067e98bbdf2d523599 README.md
|
||||
@@ -1,94 +0,0 @@
|
||||
-- Injury decisions belong to Lua; the engine exposes generic bodies/constraints.
|
||||
local broken, pending, last_hit, recent = {}, {}, {}, {}
|
||||
local last_tick, last_mode, was_enabled, status = nil, nil, nil, ""
|
||||
local function cfg(k,d) local v=sdk.settings[k];if v==nil then return d end;return v end
|
||||
local function length(v) v=v or {};return math.sqrt((v[1] or 0)^2+(v[2] or 0)^2+(v[3] or 0)^2) end
|
||||
local function publish()
|
||||
local ids={};for i in pairs(broken) do ids[#ids+1]=i end;table.sort(ids)
|
||||
sdk.net.publish("injuries",{v=1,j=ids})
|
||||
end
|
||||
local function heal()
|
||||
for i in pairs(broken) do sdk.rig.reset_joint(i) end
|
||||
for i in pairs(pending) do sdk.rig.reset_joint(i) end
|
||||
broken,pending,recent,last_hit={},{},{},{};status="Joints restored";publish()
|
||||
end
|
||||
local function menu()
|
||||
sdk.ui.menu("bones",{title="Broken bones",section="Skater mods",items={
|
||||
{id="heal",label="Heal all injuries",description="Restore this mod's joint and drive overrides."},
|
||||
}})
|
||||
end
|
||||
local function eligible(j)
|
||||
return j and (j.name:find("LEFT",1,true) or j.name:find("RIGHT",1,true))
|
||||
end
|
||||
local function draw()
|
||||
if not cfg("show_hud",true) then sdk.ui.text("bones","");return end
|
||||
local labels={};for _,j in pairs(broken) do labels[#labels+1]=j.name:gsub("JOINT_",""):gsub("_"," ") end;table.sort(labels)
|
||||
local text="BROKEN BONES | "..(#labels==0 and "No injuries" or table.concat(labels,", "))
|
||||
for _,id in ipairs(sdk.net.players()) do
|
||||
if tostring(id)~=tostring(sdk.net.info().local_id) then
|
||||
local p=sdk.net.read(id,"injuries")
|
||||
if type(p)=="table" and p.v==1 and type(p.j)=="table" and #p.j>0 then
|
||||
local skater=sdk.player.skater(id);text=text.."\n"..(skater and skater.name or tostring(id))..": "..#p.j.." injured joints"
|
||||
end
|
||||
end
|
||||
end
|
||||
if status~="" then text=text.."\n"..status end
|
||||
sdk.ui.text("bones",text:sub(1,1000))
|
||||
end
|
||||
local function fixed()
|
||||
local rig=sdk.rig.read({"tick","ragdoll","contacts"});local player=sdk.player.read();local now=sdk.time.elapsed
|
||||
if cfg("heal_on_teleport",true) and player.mode=="teleport" and last_mode~="teleport" then heal() end
|
||||
last_mode=player.mode
|
||||
local enabled=cfg("enabled",true)
|
||||
if was_enabled and not enabled then heal() end;was_enabled=enabled
|
||||
for i,j in pairs(pending) do
|
||||
local result=sdk.commands.result("joint_"..i)
|
||||
if result then
|
||||
pending[i]=nil
|
||||
if result.ok then broken[i]=j;status="Impact injured "..j.name:gsub("JOINT_",""):gsub("_"," ");publish()
|
||||
else status="Joint unchanged: "..tostring(result.error) end
|
||||
end
|
||||
end
|
||||
if not enabled or last_tick==rig.tick or sdk.player.attached() or player.suspended then draw();return end
|
||||
last_tick=rig.tick
|
||||
local joints
|
||||
local strongest={}
|
||||
for _,c in ipairs(rig.contacts or {}) do
|
||||
if c.phase~="end" and (c.closing_speed or 0)>=cfg("closing_speed",3) then
|
||||
if not joints then
|
||||
joints={};for _,j in ipairs(sdk.rig.joints()) do joints[j.child]=j end
|
||||
end
|
||||
for _,body in ipairs({c.a,c.b}) do
|
||||
if body and body.kind=="skater" then
|
||||
local j=joints[body.index];local impulse=length(c.impulse)
|
||||
if eligible(j) and impulse>=cfg("impact_impulse",80) and not broken[j.index] and not pending[j.index] and
|
||||
(not j.override_owner or j.override_owner==sdk.mod_id) and now-(last_hit[j.index] or -100)>=cfg("cooldown",0.75) then
|
||||
if not strongest[j.index] or impulse>strongest[j.index].impulse then strongest[j.index]={joint=j,impulse=impulse,at=now} end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for i,hit in pairs(strongest) do recent[i]=hit end
|
||||
for i,hit in pairs(recent) do
|
||||
if now-hit.at>0.3 then recent[i]=nil
|
||||
elseif not cfg("only_bails",false) or player.bailing or rig.ragdoll then
|
||||
sdk.commands.request("joint_"..i,{kind="player_joint",joint=i,options={free_swing=true,free_twist=true,
|
||||
drive_enabled=false,possession_enabled=false,descendants=true}})
|
||||
pending[i]=hit.joint;last_hit[i]=now;recent[i]=nil
|
||||
end
|
||||
end
|
||||
draw()
|
||||
end
|
||||
return {
|
||||
on_load=function()
|
||||
if (sdk.capabilities.player_physics or 0)<2 or not sdk.capabilities.command_results then error("Broken Bones requires the generalized native rig API build") end
|
||||
menu();publish();draw()
|
||||
end,
|
||||
on_fixed_update=fixed,
|
||||
on_event=function(e)
|
||||
if e.name=="world_changed" then broken,pending,recent,last_hit={},{},{},{};last_tick=nil;status="";publish();menu()
|
||||
elseif e.name=="menu_action" and e.menu=="bones" and e.item=="heal" then heal();draw() end
|
||||
end,
|
||||
on_unload=function() heal();sdk.net.publish("injuries",nil);sdk.ui.remove_menu("bones");sdk.ui.text("bones","") end,
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"id": "examples.broken-bones",
|
||||
"name": "Broken Bones",
|
||||
"version": "1.0.0",
|
||||
"api": 2,
|
||||
"author": "Skate3clone",
|
||||
"description": "Impact-based limb injuries using native contacts, joint topology and reversible constraint overrides. All damage rules run in Lua.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"impact_impulse": {
|
||||
"type": "number",
|
||||
"label": "Impact impulse threshold",
|
||||
"description": "Gameplay threshold in N s; tune for your preferred injury frequency.",
|
||||
"default": 80,
|
||||
"min": 5,
|
||||
"max": 1000,
|
||||
"step": 5
|
||||
},
|
||||
"closing_speed": {
|
||||
"type": "number",
|
||||
"label": "Minimum impact speed",
|
||||
"description": "Minimum pre-solve closing speed in metres/second; excludes resting support.",
|
||||
"default": 3,
|
||||
"min": 0.5,
|
||||
"max": 20,
|
||||
"step": 0.5
|
||||
},
|
||||
"cooldown": {
|
||||
"type": "number",
|
||||
"label": "Impact cooldown",
|
||||
"description": "Seconds between counted impacts on the same joint.",
|
||||
"default": 0.75,
|
||||
"min": 0.1,
|
||||
"max": 5,
|
||||
"step": 0.05
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"label": "Enable injuries",
|
||||
"description": "Process native impacts while enabled.",
|
||||
"default": true
|
||||
},
|
||||
"only_bails": {
|
||||
"type": "boolean",
|
||||
"label": "Only injure during bails",
|
||||
"description": "Retain recent impacts briefly to catch the transition into a bail.",
|
||||
"default": false
|
||||
},
|
||||
"show_hud": {
|
||||
"type": "boolean",
|
||||
"label": "Show injuries",
|
||||
"description": "Display local and remote injury summaries.",
|
||||
"default": true
|
||||
},
|
||||
"heal_on_teleport": {
|
||||
"type": "boolean",
|
||||
"label": "Heal on teleport",
|
||||
"description": "Restore joints when a teleport/reset state is observed.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
-- All match rules and turn authority live here; the SDK is game-mode agnostic.
|
||||
local WORD = "SKATE"
|
||||
local game, running, serial = nil, false, 0
|
||||
local received, received_host
|
||||
local applied, released, ack, restore = nil, nil, nil, nil
|
||||
local armed_at, last_publish, menu_stamp = 0, -1, nil
|
||||
local function info() return sdk.net.info() end
|
||||
local function me() return tostring(info().local_id or "0") end
|
||||
local function host() return not info().active or info().is_host end
|
||||
local function roster()
|
||||
local ids, have = {me()}, {[me()]=true}
|
||||
for _, peer in ipairs(sdk.net.players()) do
|
||||
local id=tostring(peer)
|
||||
if not have[id] then ids[#ids+1],have[id]=id,true end
|
||||
end
|
||||
table.sort(ids)
|
||||
-- The transport host starts, regardless of numeric/lexicographic peer order.
|
||||
local first=host() and me() or tostring(info().host_id)
|
||||
for i,id in ipairs(ids) do if id==first then table.remove(ids,i);table.insert(ids,1,id);break end end
|
||||
return ids
|
||||
end
|
||||
local function name(id)
|
||||
if id==me() then return "You" end
|
||||
local s=sdk.player.skater(id)
|
||||
return s and s.name or ("Player "..tostring(id))
|
||||
end
|
||||
local function canon(s) return tostring(s or ""):lower():gsub("[^%w]", "") end
|
||||
local function allowed(t) local c=canon(t);return c~="" and (sdk.settings.allow_ollie or c~="ollie" and c~="nollie") end
|
||||
local function index(ids,id) for i,p in ipairs(ids) do if p==id then return i end end return 0 end
|
||||
local function count(s) local n=0;for _,id in ipairs(s.ids) do if (s.l[id] or 0)<5 then n=n+1 end end return n end
|
||||
local function next_setter(s)
|
||||
local at=index(s.ids,s.a)
|
||||
for step=1,#s.ids do local id=s.ids[(at+step-1)%#s.ids+1];if (s.l[id] or 0)<5 then return id end end
|
||||
return s.ids[1]
|
||||
end
|
||||
local function next_copier(s)
|
||||
for _,id in ipairs(s.ids) do
|
||||
if (s.solo or id~=s.a) and (s.l[id] or 0)<5 and not s.d[id] and not s.f[id] then return id end
|
||||
end
|
||||
end
|
||||
local function publish(s)
|
||||
local l,d,f={},{},{}
|
||||
for _,id in ipairs(s.ids) do
|
||||
l[#l+1]=tostring(s.l[id] or 0);d[#d+1]=s.d[id] and "1" or "0";f[#f+1]=s.f[id] and "1" or "0"
|
||||
end
|
||||
-- Split immutable roster/start pose from turn state; each value stays <512B.
|
||||
sdk.net.publish("origin",{e=s.e,v=s.v,ids=s.ids,p=s.origin.position,h=s.origin.heading})
|
||||
sdk.net.publish("skate",{e=s.e,v=s.v,q=s.q,p=s.p,n=s.n,a=index(s.ids,s.a),c=index(s.ids,s.c),
|
||||
t=s.t,w=index(s.ids,s.w),g=s.g,r=math.ceil(math.max(0,(s.u or 0)-sdk.time.elapsed)),
|
||||
l=table.concat(l),d=table.concat(d),f=table.concat(f),error=s.error})
|
||||
last_publish=sdk.time.elapsed
|
||||
end
|
||||
local function read_game()
|
||||
if host() then return game end
|
||||
local peer=tostring(info().host_id)
|
||||
if received_host~=peer then received,received_host=nil,peer end
|
||||
local w,o=sdk.net.read(peer,"skate"),sdk.net.read(peer,"origin")
|
||||
if type(w)~="table" or type(o)~="table" then received=nil;return nil end
|
||||
if w.e~=o.e or w.v~=o.v then return received end
|
||||
if type(o.ids)~="table" or type(w.l)~="string" then return nil end
|
||||
local s={e=w.e,q=w.q,p=w.p,n=w.n,ids=o.ids,a=o.ids[w.a],c=o.ids[w.c],t=w.t,w=o.ids[w.w],g=w.g,r=w.r,error=w.error,
|
||||
origin={position=o.p,heading=o.h,velocity={0,0,0}},l={},d={},f={}}
|
||||
for i,id in ipairs(s.ids) do s.l[id]=tonumber(w.l:sub(i,i)) or 0;s.d[id]=tostring(w.d):sub(i,i)=="1";s.f[id]=tostring(w.f):sub(i,i)=="1" end
|
||||
received=s
|
||||
return s
|
||||
end
|
||||
local function release()
|
||||
if applied then
|
||||
sdk.player.suspend(false);sdk.camera.watch(nil)
|
||||
if restore then sdk.player.teleport(restore) end
|
||||
sdk.net.publish("ack",nil)
|
||||
end
|
||||
applied,released,ack,restore=nil,nil,nil,nil
|
||||
end
|
||||
local function control(s, fixed)
|
||||
if not s or s.p=="i" or s.p=="w" then release();return end
|
||||
local token=tostring(s.e)..":"..tostring(s.q)
|
||||
if applied~=token then
|
||||
if not restore then local p=sdk.player.read();restore={position=p.position,heading=p.heading,velocity={0,0,0}} end
|
||||
applied,released,ack=token,nil,nil
|
||||
sdk.player.suspend(true)
|
||||
sdk.camera.watch(s.c~=me() and s.c or nil)
|
||||
end
|
||||
if fixed and not ack then
|
||||
-- Acknowledge the suspension on the next command frame.
|
||||
ack={e=s.e,q=s.q,stage=0}
|
||||
sdk.net.publish("ack",ack)
|
||||
end
|
||||
if s.g and s.c==me() and released~=token then
|
||||
sdk.player.teleport(s.origin)
|
||||
sdk.player.suspend(false)
|
||||
sdk.camera.watch(nil)
|
||||
released,armed_at=token,sdk.time.elapsed+0.35
|
||||
end
|
||||
if fixed and released==token and ack and ack.stage==0 and sdk.time.elapsed>=armed_at then
|
||||
local p=sdk.player.read()
|
||||
ack={e=s.e,q=s.q,stage=1,l=p.landing_seq or 0,b=p.bail_seq or 0}
|
||||
sdk.net.publish("ack",ack)
|
||||
end
|
||||
end
|
||||
local function turn(s,id,phase)
|
||||
s.c,s.p,s.q,s.g,s.base=id,phase,s.q+1,false,nil
|
||||
s.u=sdk.time.elapsed+15
|
||||
end
|
||||
local function begin_set(s,id)
|
||||
s.a,s.t,s.d,s.f=id,"",{},{}
|
||||
turn(s,id,"s")
|
||||
end
|
||||
local function result(s)
|
||||
local id=next_copier(s)
|
||||
if id then turn(s,id,"c") else s.p,s.g,s.u="r",false,sdk.time.elapsed+2;s.q=s.q+1 end
|
||||
end
|
||||
local function read_ack(id) if id==me() then return ack end return sdk.net.read(id,"ack") end
|
||||
local function matches(a,s) return type(a)=="table" and a.e==s.e and a.q==s.q end
|
||||
local function host_tick()
|
||||
local s,now=game,sdk.time.elapsed
|
||||
if not s or not running then return end
|
||||
local present={};for _,id in ipairs(roster()) do present[id]=true end
|
||||
local changed=false
|
||||
for i=#s.ids,1,-1 do if not present[s.ids[i]] then table.remove(s.ids,i);changed=true end end
|
||||
if changed then
|
||||
s.v=s.v+1
|
||||
if not present[s.a] then begin_set(s,next_setter(s))
|
||||
elseif not present[s.c] and s.p=="c" then result(s) end
|
||||
publish(s)
|
||||
end
|
||||
if s.p=="r" and now>=s.u then
|
||||
if not s.solo and count(s)<=1 then
|
||||
s.p,s.g="w",false
|
||||
for _,id in ipairs(s.ids) do if (s.l[id] or 0)<5 then s.w=id end end
|
||||
running=false
|
||||
else s.n=s.n+1;begin_set(s,next_setter(s)) end
|
||||
publish(s);return
|
||||
end
|
||||
if s.p=="s" or s.p=="c" then
|
||||
if not s.g then
|
||||
local all=true
|
||||
for _,id in ipairs(s.ids) do if not matches(read_ack(id),s) then all=false end end
|
||||
if all then s.g=true;s.u=now+15;publish(s)
|
||||
elseif now>=s.u then s.p,s.error,running="i","A player did not acknowledge the turn. Enable the same mod on every player and restart.",false;publish(s) end
|
||||
return
|
||||
end
|
||||
if not s.base then
|
||||
local a=read_ack(s.c)
|
||||
if matches(a,s) and a.stage==1 then s.base={landing=a.l,bail=a.b};s.u=now+(s.p=="s" and 90 or (sdk.settings.copy_seconds or 45));publish(s)
|
||||
elseif now>=s.u then s.p,s.error,running="i","Active player did not finish resetting. Restart the session.",false;publish(s) end
|
||||
return
|
||||
end
|
||||
local p=sdk.player.skater(s.c)
|
||||
if p then
|
||||
local landed,bail=p.landing_seq or 0,p.bail_seq or 0
|
||||
local failed=bail>s.base.bail or now>=s.u
|
||||
local trick=landed>s.base.landing and not p.bailing and tostring(p.landed_trick or "") or nil
|
||||
if s.p=="s" then
|
||||
if failed then begin_set(s,next_setter(s));publish(s)
|
||||
elseif trick and allowed(trick) then s.t=trick;result(s);publish(s)
|
||||
elseif trick then s.base.landing=landed end
|
||||
elseif failed or trick then
|
||||
if not failed and canon(trick)==canon(s.t) then s.d[s.c]=true
|
||||
else s.f[s.c]=true;s.l[s.c]=math.min(5,(s.l[s.c] or 0)+1) end
|
||||
result(s);publish(s)
|
||||
end
|
||||
elseif now>=s.u then s.p,s.error,running="i","Active player observations are missing. Restart the session.",false;publish(s) end
|
||||
end
|
||||
if now-last_publish>=0.5 then publish(s) end
|
||||
end
|
||||
local function menu(force)
|
||||
local stamp=tostring(host())..tostring(running)
|
||||
if not force and stamp==menu_stamp then return end
|
||||
menu_stamp=stamp
|
||||
sdk.ui.menu("session",{title="SKATE",section="Gamemodes",items={
|
||||
{id="start",label="Start session",enabled=host() and not running,description=host() and "Host starts here. Other players take turns from this position." or "Only the multiplayer host can start this session."},
|
||||
{id="stop",label="Stop session",enabled=host() and running,description="End the match and restore everyone's skater and camera."},
|
||||
}})
|
||||
end
|
||||
local function overlay(s)
|
||||
local status,board="No host match received. Ask the host to start SKATE; all players must enable the same mod version.",{}
|
||||
if s then
|
||||
for _,id in ipairs(s.ids) do local l=WORD:sub(1,s.l[id] or 0);board[#board+1]=name(id)..(id==s.c and " > " or " ")..(l~="" and l or "-") end
|
||||
if s.p=="i" then status=s.error or "Session stopped. Open Gamemodes > SKATE to start."
|
||||
elseif s.p=="w" then status=s.w and name(s.w).." wins!" or "Session finished."
|
||||
elseif s.p=="r" then status="Round complete: "..s.t
|
||||
elseif not s.g then status="Preparing "..name(s.c).."'s turn; synchronizing players."
|
||||
elseif s.p=="s" then status=name(s.c)..": land a trick to set it."
|
||||
else status=name(s.c)..": copy "..s.t.." ("..tostring(s.r or math.ceil(math.max(0,s.u-sdk.time.elapsed))).."s)" end
|
||||
if s.c~=me() and s.p~="i" and s.p~="w" then status=status.." Spectating." end
|
||||
end
|
||||
sdk.ui.text("skate_title","GAME OF SKATE")
|
||||
sdk.ui.text("skate_status",status)
|
||||
sdk.ui.text("skate_board",table.concat(board," "))
|
||||
sdk.ui.text("skate_last","Last landed trick: "..tostring(sdk.player.read().landed_trick or ""))
|
||||
sdk.ui.text("skate_net",tostring(info().status or ""))
|
||||
end
|
||||
return {
|
||||
on_load=function()
|
||||
assert((sdk.capabilities.player_control or 0)>=1 and (sdk.capabilities.camera or 0)>=3,"Update the game: player suspension and peer camera streaming are required")
|
||||
menu(true)
|
||||
end,
|
||||
on_ui_update=function()
|
||||
if not host() then running=false;game=nil end
|
||||
local s=read_game();control(s,false);overlay(s);menu()
|
||||
end,
|
||||
on_fixed_update=function()
|
||||
local s=read_game();control(s,true)
|
||||
if host() then host_tick() end
|
||||
s=read_game();control(s,false);overlay(s);menu()
|
||||
end,
|
||||
on_event=function(event)
|
||||
if event.name=="world_changed" then release();received,received_host=nil,nil;game,running=nil,false;sdk.net.publish("skate",nil);sdk.net.publish("origin",nil);menu(true)
|
||||
elseif event.name=="menu_action" and event.menu=="session" and host() then
|
||||
if event.item=="start" and not running then
|
||||
release();serial=serial+1
|
||||
local p=sdk.player.read()
|
||||
game={e=math.floor(sdk.time.elapsed*1000)*1000+serial,v=1,q=0,n=1,ids=roster(),l={},w=nil,
|
||||
origin={position=p.position,heading=p.heading or 0,velocity={0,0,0}}}
|
||||
game.solo=#game.ids==1;running=true;begin_set(game,me());publish(game);control(game,false);menu()
|
||||
elseif event.item=="stop" and game then game.p,game.g,running="i",false,false;publish(game);release();menu() end
|
||||
end
|
||||
end,
|
||||
on_unload=function()
|
||||
release();sdk.net.publish("skate",nil);sdk.net.publish("origin",nil);sdk.ui.remove_menu("session")
|
||||
for _,key in ipairs({"skate_title","skate_status","skate_board","skate_last","skate_net"}) do sdk.ui.text(key,"") end
|
||||
end,
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"id": "examples.game-of-skate",
|
||||
"api": 2,
|
||||
"name": "Game of SKATE",
|
||||
"version": "0.3.0",
|
||||
"author": "Skate3clone",
|
||||
"description": "Host-first sequential turns from a shared start. Waiting players are suspended and watch the active player's camera. Match the trick or take a letter; last player without SKATE wins.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"copy_seconds": {
|
||||
"type": "number",
|
||||
"label": "Copy window (seconds)",
|
||||
"description": "How long copiers have to match the set trick before taking a letter.",
|
||||
"default": 45,
|
||||
"min": 15,
|
||||
"max": 120,
|
||||
"step": 5
|
||||
},
|
||||
"allow_ollie": {
|
||||
"type": "boolean",
|
||||
"label": "Allow ollie as a set",
|
||||
"description": "If off, ollies and nollies cannot start a round.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
# Simon Says 2.0
|
||||
|
||||
Enable this version on every player, using the updated game build. The host
|
||||
starts/stops from **Gamemodes > Simon Says**. Everyone races to land the called
|
||||
trick; the first confirmed clean ride-away observed by the host wins the round.
|
||||
Others lose a life. Solo plays as survival against the clock. Category filters,
|
||||
round timing, escalation, sudden death, hints and the call card remain settings.
|
||||
Eliminated and late-joining players spectate a surviving player. Stop, match end,
|
||||
world changes and unloading release the camera and player suspension.
|
||||
|
||||
Each player acknowledges a new round before its results can count. The host
|
||||
reads replicated engine landing/bail counters and settled scoring metadata;
|
||||
there is no stick-gesture win detector or client-submitted win claim. Body spins
|
||||
are separate from board shuvit rotation. A bail during ride-away cancels credit.
|
||||
All peers need this version and the new engine observation fields.
|
||||
|
||||
Caveman and Acid Drop guide entries are excluded from random calls because the
|
||||
current extracted scoring catalog has no distinct settled labels for them.
|
||||
The duplicate FS Fastplant card is de-duplicated. Ground tricks must finish and
|
||||
produce a confirmed scoring result; merely entering a manual/plant does not win.
|
||||
|
||||
Start/Stop now use the game menu instead of the old ready/reset hotkeys. The
|
||||
obsolete stick-detector minimum-airtime setting has been removed.
|
||||
@@ -1,3 +0,0 @@
|
||||
a3f3c066b97ab4318fa58dc4f31d897f498c5cfd9da716c126d836f2ed4069ad main.lua
|
||||
da06ca442a0270968341e9bb54cd1eec34987046f9434e8f09254ae2c074d7d9 mod.json
|
||||
b0fe8ba660657ac6ca84e98735fc362ca7ab6c41dc22d73ed2ac50fbe1911e2e README.md
|
||||
@@ -1,280 +0,0 @@
|
||||
-- Host-authoritative race to copy; only settled engine results count.
|
||||
local POOL = {
|
||||
{ name = "Kickflip", cat = "flip", hint = "RS down, flick UP+RIGHT", ghint = "RS down, flick UP+LEFT", color = { 0.23, 0.79, 0.98, 1 } },
|
||||
{ name = "Heelflip", cat = "flip", hint = "RS down, flick UP+LEFT", ghint = "RS down, flick UP+RIGHT", color = { 1.00, 0.62, 0.15, 1 } },
|
||||
{ name = "Pop Shuvit", cat = "shuvit", hint = "RS scoop RIGHT side", ghint = "RS scoop LEFT side", color = { 0.70, 0.50, 1.00, 1 } },
|
||||
{ name = "360 Flip", cat = "flip", hint = "RS scoop L -> DOWN -> UR", ghint = "RS scoop R -> DOWN -> UL", color = { 1.00, 0.84, 0.20, 1 } },
|
||||
{ name = "Nose Grab", cat = "grab", hint = "AIR: hold LT + RS DOWN", ghint = "AIR: hold RT + RS DOWN", color = { 0.35, 0.95, 0.45, 1 } },
|
||||
{ name = "Ollie", cat = "ollie", hint = "RS down, pop straight UP", ghint = "RS down, pop straight UP", color = { 0.75, 0.85, 1.00, 1 } },
|
||||
{ name = "Seatbelt", cat = "grab", hint = "AIR: hold LT + RS UP", ghint = "AIR: hold RT + RS UP", color = { 1.00, 0.45, 0.75, 1 } },
|
||||
{ name = "Nollie", cat = "ollie", hint = "RS tap UP then DOWN", ghint = "RS tap UP then DOWN", color = { 0.80, 0.85, 0.90, 1 } },
|
||||
{ name = "Varial Kickflip", cat = "flip", hint = "RS down-LEFT to UP+RIGHT", ghint = "RS down-RIGHT to UP+LEFT", color = { 0.20, 0.90, 0.80, 1 } },
|
||||
{ name = "Varial Heelflip", cat = "flip", hint = "RS down-RIGHT to UP+LEFT", ghint = "RS down-LEFT to UP+RIGHT", color = { 1.00, 0.45, 0.30, 1 } },
|
||||
{ name = "360 Pop Shuvit", cat = "shuvit", hint = "RS scoop L,DOWN,RIGHT", ghint = "RS scoop R,DOWN,LEFT", color = { 0.85, 0.70, 1.00, 1 } },
|
||||
{ name = "FS Pop Shuvit", cat = "shuvit", hint = "RS scoop LEFT side", ghint = "RS scoop RIGHT side", color = { 0.55, 0.65, 1.00, 1 } },
|
||||
{ name = "Double Kickflip", cat = "flip", hint = "Flick, hold to keep flipping", ghint = "Flick, hold to keep flipping", color = { 0.30, 0.70, 1.00, 1 } },
|
||||
{ name = "Double Heelflip", cat = "flip", hint = "Flick, hold to keep flipping", ghint = "Flick, hold to keep flipping", color = { 1.00, 0.55, 0.20, 1 } },
|
||||
{ name = "Nollie Kickflip", cat = "flip", hint = "Nollie: flick to heelside", ghint = "Nollie: flick to heelside", color = { 0.45, 0.85, 0.90, 1 } },
|
||||
{ name = "Nollie Heelflip", cat = "flip", hint = "Nollie: flick to toeside", ghint = "Nollie: flick to toeside", color = { 0.90, 0.60, 0.35, 1 } },
|
||||
{ name = "Hardflip", cat = "flip", hint = "FS shuvit + kickflip. Awkward!", ghint = "FS shuvit + kickflip. Awkward!", color = { 1.00, 0.30, 0.45, 1 } },
|
||||
{ name = "Laser Flip", cat = "flip", hint = "FS 360 shuvit + heelflip", ghint = "FS 360 shuvit + heelflip", color = { 0.60, 1.00, 0.40, 1 } },
|
||||
{ name = "Inward Heelflip", cat = "flip", hint = "Shuvit + heelflip", ghint = "Shuvit + heelflip", color = { 1.00, 0.50, 0.60, 1 } },
|
||||
{ name = "360 Hardflip", cat = "flip", hint = "FS 360 shuvit + kickflip", ghint = "FS 360 shuvit + kickflip", color = { 0.95, 0.35, 0.55, 1 } },
|
||||
{ name = "Melon", cat = "grab", hint = "Grab BS, push board forwards", ghint = "Grab BS, push board forwards", color = { 0.40, 0.90, 0.50, 1 } },
|
||||
{ name = "Method", cat = "grab", hint = "Grab and tweak it out", ghint = "Grab and tweak it out", color = { 0.50, 1.00, 0.60, 1 } },
|
||||
{ name = "Mute", cat = "grab", hint = "Board sideways, front grab", ghint = "Board sideways, front grab", color = { 0.65, 0.95, 0.45, 1 } },
|
||||
{ name = "Stale", cat = "grab", hint = "Board sideways, back grab", ghint = "Board sideways, back grab", color = { 0.75, 0.90, 0.40, 1 } },
|
||||
{ name = "Crail", cat = "grab", hint = "Nose reach, watch the hand", ghint = "Nose reach, watch the hand", color = { 0.45, 0.85, 0.55, 1 } },
|
||||
{ name = "Tail Grab", cat = "grab", hint = "Push forward, back-hand grab", ghint = "Push forward, back-hand grab", color = { 0.55, 0.80, 0.50, 1 } },
|
||||
{ name = "50-50", cat = "grind", fam = true, hint = "Both trucks. Line up rail", ghint = "Both trucks. Line up rail", color = { 0.90, 0.90, 0.95, 1 } },
|
||||
{ name = "Smith Grind", cat = "grind", fam = true, hint = "Line up Smith and grind", ghint = "Line up Smith and grind", color = { 0.88, 0.86, 0.90, 1 } },
|
||||
{ name = "Krooked", cat = "grind", fam = true, hint = "Nose into it, crooked grind", ghint = "Nose into it, crooked grind", color = { 0.86, 0.80, 0.92, 1 } },
|
||||
{ name = "Nose Blunt", cat = "grind", fam = true, hint = "Nose up onto the blunt", ghint = "Nose up onto the blunt", color = { 0.84, 0.82, 0.94, 1 } },
|
||||
{ name = "Feeble", cat = "grind", fam = true, hint = "Front truck hangs feeble", ghint = "Front truck hangs feeble", color = { 0.82, 0.86, 0.92, 1 } },
|
||||
{ name = "Salad", cat = "grind", fam = true, hint = "Salad grind, hold it", ghint = "Salad grind, hold it", color = { 0.80, 0.90, 0.88, 1 } },
|
||||
{ name = "Blunt Slide", cat = "grind", fam = true, hint = "Up on the blunt, slide it", ghint = "Up on the blunt, slide it", color = { 0.87, 0.83, 0.93, 1 } },
|
||||
{ name = "Willie", cat = "grind", fam = true, hint = "Willie grind, steezy", ghint = "Willie grind, steezy", color = { 0.83, 0.89, 0.91, 1 } },
|
||||
{ name = "Over-Krooks", cat = "grind", fam = true, also = { "overkrook", "overkrooked", "krooks" }, hint = "Over the krooks", ghint = "Over the krooks", color = { 0.85, 0.81, 0.93, 1 } },
|
||||
{ name = "Nose Manual", ground = true, cat = "manual", fam = true, hint = "Push partway, sweet spot", ghint = "Push partway, sweet spot", color = { 0.70, 0.95, 0.85, 1 } },
|
||||
{ name = "BS Fastplant", ground = true, cat = "manual", fam = true, hint = "BS grab, push off back foot", ghint = "BS grab, push off back foot", color = { 0.95, 0.75, 0.45, 1 } },
|
||||
{ name = "BS Handplant", ground = true, cat = "manual", fam = true, hint = "Ramp angle, hold at edge", ghint = "Ramp angle, hold at edge", color = { 0.93, 0.70, 0.50, 1 } },
|
||||
{ name = "FS 360 Pop Shuvit", cat = "shuvit", hint = "Find the start spot", ghint = "Find the start spot", color = { 0.65, 0.60, 1.00, 1 } },
|
||||
{ name = "Nollie 360 Flip", cat = "flip", hint = "Nollie shuvit + kickflip", ghint = "Nollie shuvit + kickflip", color = { 0.50, 0.80, 0.95, 1 } },
|
||||
{ name = "FS Fastplant", ground = true, cat = "manual", fam = true, hint = "FS grab, push off front foot", ghint = "FS grab, push off front foot", color = { 0.94, 0.78, 0.42, 1 } },
|
||||
{ name = "Triple Kickflip", cat = "flip", hint = "Flick, hold to keep flipping", ghint = "Flick, hold to keep flipping", color = { 0.25, 0.65, 1.00, 1 } },
|
||||
{ name = "Triple Heelflip", cat = "flip", hint = "Flick, hold to keep flipping", ghint = "Flick, hold to keep flipping", color = { 1.00, 0.50, 0.25, 1 } },
|
||||
{ name = "360 Spin", cat = "spin", spin = 360, hint = "Spin 360 either way, stomp it", ghint = "Spin 360 either way, stomp it", color = { 0.40, 1.00, 0.70, 1 } },
|
||||
{ name = "540 Spin", cat = "spin", spin = 540, hint = "Spin 540 either way, stomp it", ghint = "Spin 540 either way, stomp it", color = { 0.45, 0.95, 0.75, 1 } },
|
||||
{ name = "720 Spin", cat = "spin", spin = 720, hint = "Spin 720 either way, stomp it", ghint = "Spin 720 either way, stomp it", color = { 0.50, 0.90, 0.80, 1 } },
|
||||
{ name = "Kickflip 180", cat = "combo", combo = { flip = "KICKFLIP", pool = 1, spin = 180 }, hint = "Kickflip + spin 180 either way", ghint = "Kickflip + spin 180 either way", color = { 0.30, 0.75, 1.00, 1 } },
|
||||
{ name = "Kickflip 360", cat = "combo", combo = { flip = "KICKFLIP", pool = 1, spin = 360 }, hint = "Kickflip + spin 360 either way", ghint = "Kickflip + spin 360 either way", color = { 0.28, 0.72, 1.00, 1 } },
|
||||
{ name = "Heelflip 180", cat = "combo", combo = { flip = "HEELFLIP", pool = 2, spin = 180 }, hint = "Heelflip + spin 180 either way", ghint = "Heelflip + spin 180 either way", color = { 1.00, 0.58, 0.22, 1 } },
|
||||
{ name = "Heelflip 360", cat = "combo", combo = { flip = "HEELFLIP", pool = 2, spin = 360 }, hint = "Heelflip + spin 360 either way", ghint = "Heelflip + spin 360 either way", color = { 1.00, 0.55, 0.20, 1 } },
|
||||
{ name = "Ollie 180", cat = "combo", combo = { flip = "OLLIE", pool = 6, spin = 180 }, hint = "Ollie + spin 180 either way", ghint = "Ollie + spin 180 either way", color = { 0.72, 0.82, 1.00, 1 } },
|
||||
{ name = "Ollie 360", cat = "combo", combo = { flip = "OLLIE", pool = 6, spin = 360 }, hint = "Ollie + spin 360 either way", ghint = "Ollie + spin 360 either way", color = { 0.70, 0.80, 1.00, 1 } },
|
||||
{ name = "360 Flip 180", cat = "combo", combo = { flip = "360_FLIP", pool = 4, spin = 180 }, hint = "Treflip + spin 180 either way", ghint = "Treflip + spin 180 either way", color = { 0.95, 0.80, 0.25, 1 } },
|
||||
{ name = "Melon 360", cat = "combo", combo = { flip = "MELON", pool = 21, spin = 360 }, hint = "Melon + spin 360 either way", ghint = "Melon + spin 360 either way", color = { 0.42, 0.88, 0.52, 1 } },
|
||||
{ name = "Nose Grab 360", cat = "combo", combo = { flip = "NOSE_GRAB", pool = 5, spin = 360 }, hint = "Nose grab + spin 360 either way", ghint = "Nose grab + spin 360 either way", color = { 0.38, 0.92, 0.48, 1 } },
|
||||
{ name = "Method 360", cat = "combo", combo = { flip = "METHOD", pool = 22, spin = 360 }, hint = "Method + spin 360 either way", ghint = "Method + spin 360 either way", color = { 0.52, 0.98, 0.62, 1 } },
|
||||
{ name = "Superdude", cat = "grab", hint = "Grab + superman out huge", ghint = "Grab + superman out huge", color = { 0.60, 0.90, 0.50, 1 } },
|
||||
{ name = "Miracle Whip", cat = "grab", hint = "Whip it. It's a grab.", ghint = "Whip it. It's a grab.", color = { 0.65, 0.88, 0.48, 1 } },
|
||||
{ name = "Late Kickflip", cat = "flip", hint = "Ollie first, flip late", ghint = "Ollie first, flip late", color = { 0.32, 0.72, 1.00, 1 } },
|
||||
{ name = "Late Heelflip", cat = "flip", hint = "Ollie first, flip late", ghint = "Ollie first, flip late", color = { 1.00, 0.57, 0.23, 1 } },
|
||||
{ name = "Late Shuvit", cat = "shuvit", hint = "Ollie first, shuvit late", ghint = "Ollie first, shuvit late", color = { 0.72, 0.52, 1.00, 1 } },
|
||||
{ name = "Kickflip Underflip", cat = "flip", hint = "Flip it, then flip it back", ghint = "Flip it, then flip it back", color = { 0.34, 0.70, 1.00, 1 } },
|
||||
{ name = "Heelflip Underflip", cat = "flip", hint = "Flip it, then flip it back", ghint = "Flip it, then flip it back", color = { 1.00, 0.53, 0.25, 1 } },
|
||||
{ name = "Nollie Pop Shuvit", cat = "shuvit", hint = "Nollie stance shuvit", ghint = "Nollie stance shuvit", color = { 0.68, 0.62, 1.00, 1 } },
|
||||
{ name = "FS Fastplant", ground = true, cat = "manual", fam = true, hint = "FS grab, push front foot", ghint = "FS grab, push front foot", color = { 0.94, 0.76, 0.44, 1 } },
|
||||
{ name = "Airwalk", cat = "grab", hint = "Airwalk it out", ghint = "Airwalk it out", color = { 0.62, 0.89, 0.49, 1 } },
|
||||
{ name = "Coffin", ground = true, cat = "grab", hint = "Lay back, feet up. Coffin", ghint = "Lay back, feet up. Coffin", color = { 0.58, 0.86, 0.52, 1 } },
|
||||
{ name = "Cross Bone", cat = "grab", hint = "Cross-bone grab", ghint = "Cross-bone grab", color = { 0.66, 0.87, 0.47, 1 } },
|
||||
{ name = "Caveman", ground = true, cat = "ollie", hint = "Step off, jump back on", ghint = "Step off, jump back on", color = { 0.74, 0.84, 1.00, 1 } },
|
||||
{ name = "Acid Drop", ground = true, cat = "ollie", hint = "Ride off a drop", ghint = "Ride off a drop", color = { 0.73, 0.83, 1.00, 1 } },
|
||||
}
|
||||
|
||||
local game, received, epoch = nil, nil, 0
|
||||
local last_publish, menu_stamp, watching = -1, nil, false
|
||||
local deck = {}
|
||||
local acknowledged
|
||||
local function option(k,d) local v=sdk.settings[k];if v==nil then return d end;return v end
|
||||
local function net() return sdk.net.info() end
|
||||
local function me() return tostring(net().local_id or "0") end
|
||||
local function host() return not net().active or net().is_host end
|
||||
local function now() return sdk.time.elapsed end
|
||||
local function canon(s)
|
||||
s=tostring(s or ""):lower():gsub("id_trick_",""):gsub("_"," ")
|
||||
for _,category in ipairs({"ground","trick","flip","grab","grind","authentic","air","metrics"}) do
|
||||
s=s:gsub("%f[%w]"..category.."%f[%W]","")
|
||||
end
|
||||
s=s:gsub("%f[%w]switch%f[%W]",""):gsub("%f[%w]fakie%f[%W]","")
|
||||
return (s:gsub("[^%w]",""))
|
||||
end
|
||||
local aliases={stalegrab="stale",stalefish="stale",crailgrab="crail",seatbeltgrab="seatbelt",
|
||||
mutegrab="mute",superman="superdude",ngairwalk="airwalk",crossbone="crossbone",
|
||||
crooked="krooked",crookedgrind="krooked",krooks="krooked",nosebluntslide="noseblunt",
|
||||
popshoveit="popshuvit",shuvit="popshuvit",["360shuvit"]="360popshuvit"}
|
||||
local function key(s) local k=canon(s);return aliases[k] or k end
|
||||
local function matches(label,t,p)
|
||||
-- The scorer appends body rotation after the board/grab name. A leading
|
||||
-- 360 in "360 Flip" is a board trick, never evidence of a body spin.
|
||||
local spin=tonumber(tostring(label):match("%s(%d+)%s*$"))
|
||||
local base=tostring(label):gsub("%s%d+%s*$","")
|
||||
if p.landed_spin_degrees~=nil then spin=math.abs(p.landed_spin_degrees) end
|
||||
local raw=p.landed_trick_base or ""
|
||||
if base:lower():find("halfcab",1,true) then spin=180;base=base:gsub("[FfBb][Ss] ?[Hh]alf[Cc]ab","") end
|
||||
if t.spin then
|
||||
return spin==t.spin
|
||||
end
|
||||
if t.combo then
|
||||
return spin==t.combo.spin and (key(base:gsub("^[FfBb][Ss] ",""))==key(POOL[t.combo.pool].name) or raw~="" and key(raw)==key(POOL[t.combo.pool].name))
|
||||
end
|
||||
if t.cat=="grind" or t.cat=="grab" then base=base:gsub("^[FfBb][Ss] ","") end
|
||||
local k=key(base)
|
||||
if k==key(t.name) or raw~="" and key(raw)==key(t.name) then return true end
|
||||
for _,a in ipairs(t.also or {}) do if k==key(a) then return true end end
|
||||
return false
|
||||
end
|
||||
local function enabled(t)
|
||||
-- The extracted scoring catalog has no settled Caveman/Acid Drop labels.
|
||||
-- Keep their guide entries, but never issue an unconfirmable challenge.
|
||||
if t.name=="Caveman" or t.name=="Acid Drop" then return false end
|
||||
local keys={flip="allow_flips",shuvit="allow_shuvits",ollie="allow_ollies",grab="allow_grabs",grind="allow_grinds",manual="allow_manuals_plants",spin="allow_spins"}
|
||||
if t.combo then return option("allow_spins",true) and enabled(POOL[t.combo.pool]) end
|
||||
return option(keys[t.cat],true)
|
||||
end
|
||||
local function draw()
|
||||
if #deck==0 then
|
||||
local seen={}
|
||||
for i,t in ipairs(POOL) do if enabled(t) and not seen[t.name] then deck[#deck+1]=i;seen[t.name]=true end end
|
||||
for i=#deck,2,-1 do local j=math.random(i);deck[i],deck[j]=deck[j],deck[i] end
|
||||
end
|
||||
return table.remove(deck)
|
||||
end
|
||||
local function publish()
|
||||
if not game then sdk.net.publish("match",nil);return end
|
||||
local l={};for _,id in ipairs(game.ids) do l[#l+1]=tostring(game.l[id] or 0) end
|
||||
sdk.net.publish("match",{e=game.e,p=game.p,n=game.n,c=game.c,ids=game.ids,l=table.concat(l),
|
||||
r=math.ceil(math.max(0,game.until_at-now())),w=game.w or 0})
|
||||
last_publish=now()
|
||||
end
|
||||
local function read()
|
||||
if host() then return game end
|
||||
local w=sdk.net.read(tostring(net().host_id),"match")
|
||||
if type(w)~="table" or type(w.ids)~="table" or type(w.l)~="string" then received=nil;return nil end
|
||||
local s={e=w.e,p=w.p,n=w.n,c=w.c,ids=w.ids,l={},r=w.r,w=w.w}
|
||||
for i,id in ipairs(w.ids) do s.l[id]=tonumber(w.l:sub(i,i)) or 0 end
|
||||
received=s;return s
|
||||
end
|
||||
local function release()
|
||||
if watching then sdk.player.suspend(false);sdk.camera.watch(nil);watching=false end
|
||||
end
|
||||
local function controls(s)
|
||||
if s and s.p=="copy" then
|
||||
local token=tostring(s.e)..":"..s.n
|
||||
if acknowledged~=token then
|
||||
local p=sdk.player.read()
|
||||
sdk.net.publish("round",{e=s.e,n=s.n,l=p.landing_seq or 0,b=p.bail_seq or 0})
|
||||
acknowledged=token
|
||||
end
|
||||
end
|
||||
local out=s and s.p~="done" and s.p~="stopped" and (s.l[me()] or 0)<=0
|
||||
if not out then release();return end
|
||||
sdk.player.suspend(true);watching=true
|
||||
local target
|
||||
if option("spec_cam",true) then for _,id in ipairs(s.ids) do if (s.l[id] or 0)>0 then target=id;break end end end
|
||||
sdk.camera.watch(target)
|
||||
end
|
||||
local function menu()
|
||||
local s=read();local active=s and s.p~="done" and s.p~="stopped"
|
||||
local stamp=tostring(host())..tostring(active)
|
||||
if stamp==menu_stamp then return end;menu_stamp=stamp
|
||||
sdk.ui.menu("challenge",{title="Simon Says",section="Gamemodes",items={
|
||||
{id="start",label="Start match",enabled=host() and not active,description="Everyone races to land the called trick. First clean ride-away wins."},
|
||||
{id="stop",label="Stop match",enabled=host() and active},
|
||||
}})
|
||||
end
|
||||
local function next_round()
|
||||
game.n=game.n+1;game.c=draw()
|
||||
if not game.c then game.p="stopped";publish();return end
|
||||
game.p="countdown";game.until_at=now()+option("countdown_secs",3);game.w=0
|
||||
game.base={};game.pending={};publish()
|
||||
end
|
||||
local function start()
|
||||
epoch=epoch+1;deck={}
|
||||
local ids,have={me()},{[me()]=true}
|
||||
for _,id in ipairs(sdk.net.players()) do id=tostring(id);if not have[id] then ids[#ids+1]=id;have[id]=true end end
|
||||
game={e=tostring(me())..":"..epoch..":"..now(),ids=ids,l={},n=0,until_at=now()}
|
||||
for _,id in ipairs(ids) do game.l[id]=option("lives",3) end
|
||||
next_round()
|
||||
end
|
||||
local function resolve(winner)
|
||||
game.w=winner or 0
|
||||
local cost=option("sudden_on",false) and game.n>=option("sudden_round",6) and option("sudden_cost",2) or 1
|
||||
local alive=0
|
||||
for i,id in ipairs(game.ids) do
|
||||
if i~=winner then game.l[id]=math.max(0,game.l[id]-cost) end
|
||||
if game.l[id]>0 then alive=alive+1 end
|
||||
end
|
||||
game.p=(alive==0 or #game.ids>1 and alive<=1) and "done" or "break"
|
||||
game.until_at=now()+math.max(1,option("intermission_base",4)*option("esc_dur",0.85)^(game.n-1));publish()
|
||||
end
|
||||
local function advance()
|
||||
if not game or game.p=="done" or game.p=="stopped" then return end
|
||||
local connected={[me()]=true};for _,id in ipairs(sdk.net.players()) do connected[tostring(id)]=true end
|
||||
for _,id in ipairs(game.ids) do if not connected[id] then game.l[id]=0 end end
|
||||
if game.p=="countdown" and now()>=game.until_at then
|
||||
game.p="copy";game.until_at=now()+math.max(5,option("round_time",45)*option("esc_dur",0.85)^(game.n-1))
|
||||
game.base={}
|
||||
publish()
|
||||
elseif game.p=="break" and now()>=game.until_at then next_round()
|
||||
elseif game.p=="copy" then
|
||||
local ride=math.max(0.5,option("ride_away",2)-option("esc_ride",0.2)*(game.n-1))
|
||||
for i,id in ipairs(game.ids) do
|
||||
local p=sdk.player.skater(id);local b=game.base[id]
|
||||
if not b then
|
||||
local ack=sdk.net.read(id,"round")
|
||||
if type(ack)=="table" and ack.e==game.e and ack.n==game.n and type(ack.l)=="number" and type(ack.b)=="number" then
|
||||
b={l=ack.l,b=ack.b};game.base[id]=b
|
||||
end
|
||||
end
|
||||
if p and b and game.l[id]>0 then
|
||||
local landed,bail=p.landing_seq or 0,p.bail_seq or 0
|
||||
if bail~=b.b or p.bailing or p.on_board==false then game.pending[id]=nil end
|
||||
if landed>b.l and bail==b.b and not p.bailing and (p.landed_clean==nil and p.clean~=false or p.landed_clean==true) and not (p.landed_sketchy or p.sketchy) and matches(p.landed_trick,POOL[game.c],p) then
|
||||
game.pending[id]={at=now()+ride,b=bail}
|
||||
end
|
||||
b.l,b.b=landed,bail
|
||||
local pending=game.pending[id]
|
||||
if pending and now()>=pending.at and bail==pending.b and p.on_board~=false and not p.bailing then resolve(i);return end
|
||||
else game.pending[id]=nil end
|
||||
end
|
||||
if now()>=game.until_at then resolve(nil);return end
|
||||
end
|
||||
if now()-last_publish>=0.25 then publish() end
|
||||
end
|
||||
local card_token, card_at = nil, 0
|
||||
local function card(s)
|
||||
local t=s and POOL[s.c]
|
||||
local token=s and tostring(s.e)..":"..s.n or "idle"
|
||||
if token~=card_token then card_token,card_at=token,now() end
|
||||
local accent=t and t.color or {0.23,0.79,0.98,1}
|
||||
local pop=option("flash",true) and 1+math.max(0,0.15-(now()-card_at)*0.3) or 1
|
||||
local hint=t and (option("stance","regular")=="goofy" and t.ghint or t.hint) or "First clean ride-away wins the round."
|
||||
local remaining=s and (s.r or math.ceil(math.max(0,s.until_at-now()))) or 0
|
||||
local items={
|
||||
{key="back",type="rect",position={0,0},size={420,220},color={0.04,0.05,0.06,0.94}},
|
||||
{key="accent",type="rect",position={0,0},size={420,6},color=accent},
|
||||
{key="title",type="text",position={16,14},size={388,24},text="SIMON SAYS"..(s and " | ROUND "..s.n or ""),font_size=16,color=accent},
|
||||
{key="call",type="text",position={16,46},size={388,48},text=t and t.name or "Ready to play",font_size=28,color={1,1,1,1}},
|
||||
{key="hint",type="text",position={16,96},size={388,38},text=hint,font_size=14,color={0.85,0.9,0.95,1}},
|
||||
{key="state",type="text",position={16,140},size={388,24},text=s and (s.p.." | "..remaining.."s | Lives: "..(s.l[me()] or 0)) or "Open Gamemodes > Simon Says",font_size=17,color=accent},
|
||||
{key="help",type="text",position={16,182},size={388,22},text="Start / Stop: Gamemodes > Simon Says",font_size=13,color={0.6,0.65,0.7,1}},
|
||||
}
|
||||
sdk.ui.canvas("simon_card",{anchor="top_right",offset={24,24},size={420,220},scale=option("card_scale",1)*pop,visible=true,items=items})
|
||||
end
|
||||
local function hud()
|
||||
local s=read();controls(s);menu();card(s)
|
||||
if not option("show_hud",true) then sdk.ui.text("simon","");return end
|
||||
if not s then sdk.ui.text("simon","SIMON SAYS | Open Gamemodes > Simon Says. All players must enable this mod.");return end
|
||||
local t=POOL[s.c];local lines={"SIMON SAYS | Round "..s.n.." | "..s.p}
|
||||
if t then
|
||||
lines[#lines+1]="Call: "..t.name.." | "..tostring(s.r or math.ceil(math.max(0,s.until_at-now()))).."s"
|
||||
lines[#lines+1]=option("stance","regular")=="goofy" and t.ghint or t.hint
|
||||
end
|
||||
for i,id in ipairs(s.ids) do local p=sdk.player.skater(id);lines[#lines+1]=(id==me() and "You" or p and p.name or id)..": "..(s.l[id] or 0).." lives"..(s.w==i and " - round winner" or "") end
|
||||
if s.p=="done" then lines[#lines+1]="Match ended. Start again from Gamemodes > Simon Says." end
|
||||
sdk.ui.text("simon",table.concat(lines,"\n"))
|
||||
end
|
||||
return {
|
||||
on_load=function() menu();hud() end,
|
||||
on_fixed_update=function() if host() then advance() end;hud() end,
|
||||
on_event=function(e)
|
||||
if e.name=="world_changed" then release();game,received=nil,nil;publish();menu()
|
||||
elseif e.name=="menu_action" and e.menu=="challenge" and host() then
|
||||
if e.item=="start" and (not game or game.p=="done" or game.p=="stopped") then start()
|
||||
elseif e.item=="stop" and game then game.p="stopped";publish();release() end
|
||||
hud()
|
||||
end
|
||||
end,
|
||||
on_unload=function() release();sdk.net.publish("match",nil);sdk.ui.remove_menu("challenge");sdk.ui.remove("simon_card");sdk.ui.text("simon","") end,
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
{
|
||||
"id": "simon.says",
|
||||
"api": 2,
|
||||
"name": "Simon Says",
|
||||
"version": "2.0.0",
|
||||
"author": "you",
|
||||
"description": "Race to copy confirmed landed tricks, with host authority, lives, ride-away checks and eliminated-player spectating.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"lives": {
|
||||
"type": "number",
|
||||
"label": "Lives",
|
||||
"description": "Lives before elimination.",
|
||||
"default": 3,
|
||||
"min": 1,
|
||||
"max": 5,
|
||||
"step": 1
|
||||
},
|
||||
"round_time": {
|
||||
"type": "number",
|
||||
"label": "Round time (s)",
|
||||
"description": "Seconds to land the called trick and ride away.",
|
||||
"default": 45,
|
||||
"min": 15,
|
||||
"max": 120,
|
||||
"step": 5
|
||||
},
|
||||
"ride_away": {
|
||||
"type": "number",
|
||||
"label": "Ride-away clean (s)",
|
||||
"description": "Clean landing window for v1: on_board + not bailing.",
|
||||
"default": 2.0,
|
||||
"min": 1.0,
|
||||
"max": 4.0,
|
||||
"step": 0.5
|
||||
},
|
||||
"stance": {
|
||||
"type": "choice",
|
||||
"label": "Your stance",
|
||||
"description": "Match your skater: goofy mirrors left/right flicks and hints.",
|
||||
"default": "regular",
|
||||
"choices": [
|
||||
"regular",
|
||||
"goofy"
|
||||
]
|
||||
},
|
||||
"show_hud": {
|
||||
"type": "boolean",
|
||||
"label": "Top-left text HUD",
|
||||
"description": "Show additional top-left call and lives text. The call card remains visible.",
|
||||
"default": true
|
||||
},
|
||||
"countdown_secs": {
|
||||
"type": "number",
|
||||
"label": "Countdown (s)",
|
||||
"description": "Get-ready countdown before round 1.",
|
||||
"default": 3,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"step": 0.5
|
||||
},
|
||||
"intermission_base": {
|
||||
"type": "number",
|
||||
"label": "Break between rounds (s)",
|
||||
"description": "Round 1 breather. Shrinks as rounds climb.",
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 8,
|
||||
"step": 0.5
|
||||
},
|
||||
"sudden_on": {
|
||||
"type": "boolean",
|
||||
"label": "Sudden death",
|
||||
"description": "Late rounds hit harder. Off keeps it chill.",
|
||||
"default": false
|
||||
},
|
||||
"sudden_round": {
|
||||
"type": "number",
|
||||
"label": "Sudden death starts round",
|
||||
"description": "Fail costs double from here.",
|
||||
"default": 6,
|
||||
"min": 3,
|
||||
"max": 12,
|
||||
"step": 1
|
||||
},
|
||||
"sudden_cost": {
|
||||
"type": "number",
|
||||
"label": "Sudden death cost",
|
||||
"description": "Lives lost per fail in sudden death.",
|
||||
"default": 2,
|
||||
"min": 1,
|
||||
"max": 3,
|
||||
"step": 1
|
||||
},
|
||||
"esc_dur": {
|
||||
"type": "number",
|
||||
"label": "Timer squeeze / round",
|
||||
"description": "Round time multiplier each round. Lower = meaner.",
|
||||
"default": 0.85,
|
||||
"min": 0.7,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
"esc_ride": {
|
||||
"type": "number",
|
||||
"label": "Ride-out shrink / round (s)",
|
||||
"description": "Ride-away loses this much each round.",
|
||||
"default": 0.2,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.05
|
||||
},
|
||||
"spec_cam": {
|
||||
"type": "boolean",
|
||||
"label": "Spectate camera",
|
||||
"description": "Eliminated riders watch through a surviving player camera.",
|
||||
"default": true
|
||||
},
|
||||
"allow_flips": {
|
||||
"type": "boolean",
|
||||
"label": "Flip tricks",
|
||||
"description": "Kickflips, heelflips, varials, hardflips, lasers, doubles, nollie flips.",
|
||||
"default": true
|
||||
},
|
||||
"allow_shuvits": {
|
||||
"type": "boolean",
|
||||
"label": "Shuv-its",
|
||||
"description": "Pop shuvits, FS and 360 variants.",
|
||||
"default": true
|
||||
},
|
||||
"allow_ollies": {
|
||||
"type": "boolean",
|
||||
"label": "Ollies",
|
||||
"description": "Ollie and nollie calls.",
|
||||
"default": true
|
||||
},
|
||||
"allow_grabs": {
|
||||
"type": "boolean",
|
||||
"label": "Grabs",
|
||||
"description": "Nose, seatbelt, melon, method, mute, stale, crail, tail.",
|
||||
"default": true
|
||||
},
|
||||
"allow_grinds": {
|
||||
"type": "boolean",
|
||||
"label": "Grinds",
|
||||
"description": "50-50, smith, krooked, blunts, feeble, salad, willie.",
|
||||
"default": true
|
||||
},
|
||||
"allow_manuals_plants": {
|
||||
"type": "boolean",
|
||||
"label": "Manuals and plants",
|
||||
"description": "Nose manual, fastplants, handplants.",
|
||||
"default": true
|
||||
},
|
||||
"allow_spins": {
|
||||
"type": "boolean",
|
||||
"label": "Spins and combos",
|
||||
"description": "Pure spins plus flip-plus-spin combos. Off removes both.",
|
||||
"default": true
|
||||
},
|
||||
"card_scale": {
|
||||
"type": "number",
|
||||
"label": "Card size",
|
||||
"description": "Top-right card scale.",
|
||||
"default": 1,
|
||||
"min": 0.65,
|
||||
"max": 1.5,
|
||||
"step": 0.05
|
||||
},
|
||||
"flash": {
|
||||
"type": "boolean",
|
||||
"label": "Call flash",
|
||||
"description": "Card pops big on each new call.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
# Wipeout Challenge 3.1
|
||||
|
||||
Requires the updated game build with `player_overlap` capability. Enable the
|
||||
mod, then use **Gamemodes > Wipeout**: Start, Restart at start pad, or Stop and
|
||||
return. T starts an idle run; R restarts. Each local run is a personal survival
|
||||
challenge, not a shared multiplayer race or leaderboard.
|
||||
|
||||
Start builds the supplied four-pad sky course 40m above the saved starting
|
||||
location. Restart reuses that anchor rather than lifting another 40m. Stop and
|
||||
unload restore the original position and remove all course bodies and meshes.
|
||||
A map change removes the course without teleporting into the previous map.
|
||||
|
||||
Hazards are persistent kinematic solids moved every fixed tick, not repeatedly
|
||||
removed/respawned static boxes. Hits use enabled native board/skater collision
|
||||
shapes through `sdk.physics.read(key).player_overlapping`. The first hazard
|
||||
entry is forgiven; the next separate entry ends the run. Sustained contact does
|
||||
not count repeatedly. One second of recovery groups simultaneous impacts.
|
||||
Water sensors and falling off the course end the run immediately. Water volumes
|
||||
extend below their original surface to catch fast falls. Timed mode clears after
|
||||
the selected waves; survival repeats them. The obsolete safe-marker settings
|
||||
were removed because they never provided a physical safe area.
|
||||
@@ -1,3 +0,0 @@
|
||||
b2cf588b2b14b818d574d62f6e8c5d9f76ae256d9171871ec3b0b80b16f3b44b main.lua
|
||||
c18fa0fe6f9259b7310c9dc063784ca265889a4dbfb77e95286550702eda6cc1 mod.json
|
||||
e79c9177d3a4a37ae52ac23c8b95e02c7a122398706925b8de2416fb8d28f4af README.md
|
||||
@@ -1,305 +0,0 @@
|
||||
local WAVE_LEN = 40
|
||||
local MAXZ = 24
|
||||
local SKY_LIFT = 40
|
||||
local FALL_MARGIN = 12
|
||||
|
||||
local RED = { 1, 0.12, 0.12 }
|
||||
local WATER = { 0.1, 0.55, 1 }
|
||||
local SPIKE = { 1, 0.5, 0.05 }
|
||||
local DECK = { 0.55, 0.6, 0.7 }
|
||||
local STARTPAD = { 0.15, 0.9, 0.35 }
|
||||
|
||||
local run = {
|
||||
started = false, over = false,
|
||||
bail_used = false, hits = 0,
|
||||
t0 = 0,
|
||||
ax = 0, ay = 0, az = 0,
|
||||
built = false, grace_until = 0,
|
||||
}
|
||||
local prev = { r = false, t = false }
|
||||
local slots = {}
|
||||
for i = 1, MAXZ do slots[i] = { live = false, x = 0, y = 0, z = 0, hx = 0, hy = 0, hz = 0, kind = 0 } end
|
||||
local scratch = {}
|
||||
|
||||
local function fmt_time(s)
|
||||
if s < 0 then s = 0 end
|
||||
local m = math.floor(s / 60)
|
||||
local sec = math.floor(s % 60)
|
||||
local ms = math.floor((s * 1000) % 1000)
|
||||
return string.format("%d:%02d.%03d", m, sec, ms)
|
||||
end
|
||||
|
||||
local function setting(key, fallback)
|
||||
local v = sdk.settings[key]
|
||||
if v == nil then return fallback end
|
||||
return v
|
||||
end
|
||||
|
||||
local function intensity_scale()
|
||||
local h = setting("hazard_intensity", "hardcore")
|
||||
if h == "easy" then return 0.5 end
|
||||
if h == "normal" then return 0.75 end
|
||||
return 1
|
||||
end
|
||||
|
||||
local function wave_count()
|
||||
local w = math.floor(setting("wave_count", 4))
|
||||
if w < 1 then w = 1 end
|
||||
if w > 10 then w = 10 end
|
||||
return w
|
||||
end
|
||||
|
||||
local function prand(seed)
|
||||
local x = math.sin(seed * 127.1) * 43758.5
|
||||
return x - math.floor(x)
|
||||
end
|
||||
|
||||
local function slot_key(i)
|
||||
return string.format("wz%02d", i)
|
||||
end
|
||||
|
||||
local function zone_color(kind)
|
||||
if kind == 2 then return WATER end
|
||||
if kind == 3 then return SPIKE end
|
||||
return RED
|
||||
end
|
||||
|
||||
-- Course pieces, anchor-relative. Water keeps its original surface height
|
||||
-- with depth below it so a falling skater cannot skip a thin sampled sensor.
|
||||
-- kind 9 = deck platform (no hit rules).
|
||||
local function course_pieces()
|
||||
return {
|
||||
{ key = "wo.p1", x = 0, y = 0, z = 0, hx = 7, hy = 0.5, hz = 7, kind = 9, color = STARTPAD },
|
||||
{ key = "wo.p2", x = 17, y = 1, z = 0, hx = 5, hy = 0.5, hz = 5, kind = 9, color = DECK },
|
||||
{ key = "wo.p3", x = 17, y = 2, z = 17, hx = 5, hy = 0.5, hz = 5, kind = 9, color = DECK },
|
||||
{ key = "wo.p4", x = 0, y = 3, z = 17, hx = 7, hy = 0.5, hz = 7, kind = 9, color = DECK },
|
||||
{ key = "wo.w1", x = 8.5, y = -1.22, z = 0, hx = 2.5, hy = 2.0, hz = 6, kind = 2, sensor = true },
|
||||
{ key = "wo.w2", x = 17, y = -0.22, z = 8.5, hx = 4, hy = 2.0, hz = 2.5, kind = 2, sensor = true },
|
||||
{ key = "wo.w3", x = 8.5, y = 0.78, z = 17, hx = 2.5, hy = 2.0, hz = 6, kind = 2, sensor = true },
|
||||
}
|
||||
end
|
||||
|
||||
local function build_course()
|
||||
local ax, ay, az = run.ax, run.ay, run.az
|
||||
for _, pc in ipairs(course_pieces()) do
|
||||
local desc = {
|
||||
shape = { type = "box", half_extents = { pc.hx, pc.hy, pc.hz } },
|
||||
body_type = "static",
|
||||
position = { ax + pc.x, ay + pc.y, az + pc.z },
|
||||
friction = 0.9,
|
||||
}
|
||||
if pc.sensor then desc.sensor = true end
|
||||
sdk.physics.spawn(pc.key, desc)
|
||||
sdk.graphics.mesh(pc.key .. "v", {
|
||||
body = pc.key,
|
||||
scale = { pc.hx * 2, pc.hy * 2, pc.hz * 2 },
|
||||
color = pc.color or zone_color(pc.kind),
|
||||
})
|
||||
end
|
||||
run.built = true
|
||||
end
|
||||
|
||||
local function clear_course()
|
||||
for _, pc in ipairs(course_pieces()) do
|
||||
sdk.physics.remove(pc.key)
|
||||
sdk.graphics.remove(pc.key .. "v")
|
||||
end
|
||||
run.built = false
|
||||
end
|
||||
|
||||
local function start_pad_top()
|
||||
-- Pad corner, clear of the wave-1 orbit ring.
|
||||
return { run.ax - 5, run.ay + 1.6, run.az - 5 }
|
||||
end
|
||||
|
||||
local function teleport_to_start()
|
||||
local s = start_pad_top()
|
||||
sdk.player.teleport({position=s, heading=0, velocity={0,0,0}})
|
||||
end
|
||||
|
||||
-- Moving hazards, anchor-relative. kind 1=red 2=water 3=spike.
|
||||
local function gen_zones(wave, t, out)
|
||||
local ax, ay, az = run.ax, run.ay, run.az
|
||||
local n = 0
|
||||
local scale = intensity_scale()
|
||||
local function add(x, y, z, hx, hy, hz, kind)
|
||||
n = n + 1
|
||||
if n > MAXZ then return end
|
||||
out[n] = out[n] or {}
|
||||
local zn = out[n]
|
||||
zn.x, zn.y, zn.z, zn.hx, zn.hy, zn.hz, zn.kind = x, y, z, hx, hy, hz, kind
|
||||
end
|
||||
if wave == 1 then
|
||||
local count = math.floor(6 * scale + 0.5)
|
||||
for i = 1, count do
|
||||
local a = (i - 1) * math.pi * 2 / count + t * 0.35
|
||||
local r = 4.5 + math.sin(t * 1.5 + i) * 1
|
||||
add(ax + math.cos(a) * r, ay + 1.1, az + math.sin(a) * r, 1.0, 0.6, 1.0, 1)
|
||||
end
|
||||
elseif wave == 2 then
|
||||
local rc = math.floor(6 * scale + 0.5)
|
||||
for i = 1, rc do
|
||||
local a = (i - 1) * math.pi * 2 / rc - t * 0.7
|
||||
local r = 3.2 + math.sin(t * 2 + i * 2) * 0.8
|
||||
add(ax + 17 + math.cos(a) * r, ay + 2.1, az + math.sin(a) * r, 0.9, 0.6, 0.9, 1)
|
||||
end
|
||||
local qc = math.floor(4 * scale + 0.5)
|
||||
for i = 1, qc do
|
||||
local ox = (prand(i * 31 + wave) - 0.5) * 10
|
||||
local oz = (prand(i * 57 + wave * 3) - 0.5) * 10
|
||||
if math.abs(ox) + math.abs(oz) > 4 then
|
||||
add(ax + ox, ay + 1.1, az + oz, 1.0, 0.6, 1.0, 1)
|
||||
end
|
||||
end
|
||||
elseif wave == 3 then
|
||||
local sc = math.floor(8 * scale + 0.5)
|
||||
for i = 1, sc do
|
||||
local a = (i - 1) * math.pi * 2 / sc + t * 1.2
|
||||
local r = 3 + math.sin(t * 2.5 + i) * 1
|
||||
add(ax + 17 + math.cos(a) * r, ay + 3.4, az + 17 + math.sin(a) * r, 0.85, 0.85, 0.85, 3)
|
||||
end
|
||||
else
|
||||
local cc = math.floor(16 * scale + 0.5)
|
||||
local hubs = { { 0, 0.5, 0 }, { 17, 1.5, 0 }, { 17, 2.5, 17 }, { 0, 3.5, 17 } }
|
||||
for i = 1, cc do
|
||||
local hub = hubs[((i - 1) % 4) + 1]
|
||||
local a = (i - 1) * math.pi * 2 / cc + t * (0.6 + (i % 3) * 0.4)
|
||||
local r = 3.5 + math.sin(t * 3 + i * 1.7) * 1.5
|
||||
local kind = 1
|
||||
local h = 0.6
|
||||
if i % 5 == 0 then kind = 3 h = 0.85 end
|
||||
add(ax + hub[1] + math.cos(a) * r, ay + hub[2] + h, az + hub[3] + math.sin(a) * r,
|
||||
1.0, h, 1.0, kind)
|
||||
end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function spawn_zone(i, zn)
|
||||
local key = slot_key(i)
|
||||
local desc = {
|
||||
shape = { type = "box", half_extents = { zn.hx, zn.hy, zn.hz } },
|
||||
body_type = "kinematic",
|
||||
position = { zn.x, zn.y, zn.z },
|
||||
friction = 0.7,
|
||||
}
|
||||
if zn.kind == 2 then desc.sensor = true end
|
||||
sdk.physics.spawn(key, desc)
|
||||
sdk.graphics.mesh(key .. "v", {
|
||||
body = key,
|
||||
scale = { zn.hx * 2, zn.hy * 2, zn.hz * 2 },
|
||||
color = zone_color(zn.kind),
|
||||
})
|
||||
local s = slots[i]
|
||||
s.touching = false
|
||||
s.live = true
|
||||
s.x, s.y, s.z, s.hx, s.hy, s.hz, s.kind = zn.x, zn.y, zn.z, zn.hx, zn.hy, zn.hz, zn.kind
|
||||
end
|
||||
|
||||
local function remove_zone(i)
|
||||
local s = slots[i]
|
||||
if not s.live then return end
|
||||
local key = slot_key(i)
|
||||
sdk.physics.remove(key)
|
||||
sdk.graphics.remove(key .. "v")
|
||||
s.live = false
|
||||
end
|
||||
|
||||
|
||||
local restore, phase, last_wave = nil, "idle", 0
|
||||
local function menu()
|
||||
sdk.ui.menu("challenge", {title="Wipeout", section="Gamemodes", items={
|
||||
{id="start",label="Start course",enabled=phase=="idle"},
|
||||
{id="restart",label="Restart at start pad",enabled=phase~="idle"},
|
||||
{id="stop",label="Stop and return",enabled=phase~="idle"},
|
||||
}})
|
||||
end
|
||||
local function clear_slots() for i=1,MAXZ do remove_zone(i) end end
|
||||
local function stop(return_player)
|
||||
if return_player and restore then sdk.player.teleport(restore) end
|
||||
clear_slots();clear_course();restore=nil;phase="idle";run.started=false
|
||||
sdk.ui.text("wo.status", "Wipeout: open Gamemodes > Wipeout to start.")
|
||||
menu()
|
||||
end
|
||||
local function begin()
|
||||
if not sdk.capabilities.player_overlap then
|
||||
sdk.ui.text("wo.status","Wipeout needs the updated player-overlap API. Update the game build.");return
|
||||
end
|
||||
if not restore then
|
||||
local p=sdk.player.read();restore={position=p.position,heading=p.heading,velocity={0,0,0}}
|
||||
run.ax,run.ay,run.az=p.position[1],p.position[2]+SKY_LIFT,p.position[3]
|
||||
build_course()
|
||||
end
|
||||
clear_slots();last_wave=0
|
||||
run.started,run.over,run.bail_used,run.hits=true,false,false,0
|
||||
run.t0=sdk.time.elapsed+2;run.grace_until=run.t0;run.hit_until=run.t0
|
||||
phase="running";teleport_to_start();menu()
|
||||
end
|
||||
local function finish(won,elapsed)
|
||||
phase=won and "cleared" or "eliminated";run.over=true
|
||||
clear_slots()
|
||||
sdk.ui.text("wo.status",string.upper(phase).." at "..fmt_time(elapsed)..". Restart or Stop in Gamemodes > Wipeout.")
|
||||
menu()
|
||||
end
|
||||
local function overlapping(key)
|
||||
local body=sdk.physics.read(key)
|
||||
return body and body.player_overlapping or false
|
||||
end
|
||||
local function fixed(ev)
|
||||
local t,r=sdk.input.down("KeyT"),sdk.input.down("KeyR")
|
||||
if (t and not prev.t and phase=="idle") or (r and not prev.r) then begin() end
|
||||
prev.t,prev.r=t,r
|
||||
if phase~="running" then return end
|
||||
local now=sdk.time.elapsed
|
||||
local elapsed=math.max(0,now-run.t0)
|
||||
local wave=math.floor(elapsed/WAVE_LEN)+1
|
||||
local total=wave_count()
|
||||
if setting("game_mode","survival")=="timed" and wave>total then finish(true,elapsed);return end
|
||||
wave=(wave-1)%total+1
|
||||
if wave~=last_wave then clear_slots();last_wave=wave end
|
||||
local dt=math.max(0.001,ev.dt or 1/60)
|
||||
local count=gen_zones(wave,elapsed+dt,scratch)
|
||||
for i=1,MAXZ do
|
||||
if i<=count then
|
||||
local z,s=scratch[i],slots[i]
|
||||
if not s.live then spawn_zone(i,z)
|
||||
else
|
||||
local b=sdk.physics.read(slot_key(i))
|
||||
if b then sdk.physics.set_linvel(slot_key(i),{(z.x-b.position[1])/dt,(z.y-b.position[2])/dt,(z.z-b.position[3])/dt}) end
|
||||
end
|
||||
else remove_zone(i) end
|
||||
end
|
||||
if now>=run.grace_until then
|
||||
for _,pc in ipairs(course_pieces()) do
|
||||
if pc.sensor and overlapping(pc.key) then finish(false,elapsed);return end
|
||||
end
|
||||
local p=sdk.player.read()
|
||||
if p.position[2]<run.ay-FALL_MARGIN then finish(false,elapsed);return end
|
||||
-- Count entries, not every frame spent touching one hazard. One-second
|
||||
-- recovery also groups simultaneous impacts from the same collision.
|
||||
for i=1,count do
|
||||
local s=slots[i];local hit=overlapping(slot_key(i))
|
||||
if hit and not s.touching and now>=run.hit_until then
|
||||
run.hits=run.hits+1;run.hit_until=now+1
|
||||
if run.bail_used then finish(false,elapsed);return end
|
||||
run.bail_used=true
|
||||
end
|
||||
s.touching=hit
|
||||
end
|
||||
end
|
||||
local timer=setting("enable_timer",true) and (" | "..fmt_time(elapsed)) or ""
|
||||
sdk.ui.text("wo.status","WIPEOUT | Wave "..wave.."/"..total..timer.." | "..
|
||||
(now<run.grace_until and "Get ready" or run.bail_used and "Next hazard hit ends the run" or "One hazard hit forgiven"))
|
||||
end
|
||||
return {
|
||||
on_load=function() menu();sdk.ui.text("wo.status","Wipeout: open Gamemodes > Wipeout to start.") end,
|
||||
on_fixed_update=fixed,
|
||||
on_event=function(e)
|
||||
if e.name=="world_changed" then stop(false)
|
||||
elseif e.name=="menu_action" and e.menu=="challenge" then
|
||||
if e.item=="start" and phase=="idle" or e.item=="restart" then begin()
|
||||
elseif e.item=="stop" then stop(true) end
|
||||
end
|
||||
end,
|
||||
on_unload=function() stop(true);sdk.ui.remove_menu("challenge");sdk.ui.text("wo.status","") end,
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"id": "project-wipeout-challenge",
|
||||
"api": 2,
|
||||
"name": "Wipeout Challenge",
|
||||
"version": "3.1.0",
|
||||
"author": "opencode",
|
||||
"description": "Floating platform survival with moving solid hazards, water sensors, one forgiven hit, and restart/return controls.",
|
||||
"entry": "main.lua",
|
||||
"settings": {
|
||||
"game_mode": {
|
||||
"type": "choice",
|
||||
"label": "Game Mode",
|
||||
"description": "Timed ends after all waves, survival loops them.",
|
||||
"default": "survival",
|
||||
"choices": [
|
||||
"survival",
|
||||
"timed"
|
||||
]
|
||||
},
|
||||
"wave_count": {
|
||||
"type": "number",
|
||||
"label": "Wave Count",
|
||||
"description": "Number of hazard waves.",
|
||||
"default": 4,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"step": 1
|
||||
},
|
||||
"hazard_intensity": {
|
||||
"type": "choice",
|
||||
"label": "Hazard Intensity",
|
||||
"description": "Obstacle danger level.",
|
||||
"default": "hardcore",
|
||||
"choices": [
|
||||
"easy",
|
||||
"normal",
|
||||
"hardcore"
|
||||
]
|
||||
},
|
||||
"enable_timer": {
|
||||
"type": "boolean",
|
||||
"label": "Enable Timer",
|
||||
"description": "Show countdown timer.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,15 @@ try {
|
||||
pipeline_equivalence = $pipelineEquivalence
|
||||
} | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath "$stage/release.json" -Encoding utf8
|
||||
Copy-Item -LiteralPath "$stage/release.json" -Destination (Join-Path $ProjectRoot 'target/release.json')
|
||||
# Seed fresh downloads with Skyline only. Mods stay outside release.json:
|
||||
# the updater deliberately never owns or overwrites a user's mod packages.
|
||||
$bundledModFiles = @(& git ls-files -- 'mods/Skyline_Drive_Mod/')
|
||||
if ($LASTEXITCODE -ne 0 -or $bundledModFiles.Count -eq 0) { throw 'Tracked Skyline package missing' }
|
||||
foreach ($relative in $bundledModFiles) {
|
||||
$destination = Join-Path $stage $relative
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectRoot $relative) -Destination $destination
|
||||
}
|
||||
$zip = Join-Path $ProjectRoot 'target/skate3rust-windows-x64.zip'
|
||||
Compress-Archive -LiteralPath $stage -DestinationPath $zip -Force
|
||||
(Get-Sha256Hex $zip) + ' skate3rust-windows-x64.zip' |
|
||||
|
||||
Reference in new issue
Block a user