Bundle only Skyline in the installed mods and fresh releases

This commit is contained in:
Ethanw05 committed 2026-09-29 20:25:49 -06:00
1 parent dfd3e543c0
commit 60efdef866
17 files changed
+9 -1498

No files matched your search

-185
View File
@@ -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,
}
-20
View File
@@ -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
}
}
}
-25
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
5d30691ecb4b3865c462680685a3c146fe7fb6e8fd0b41d5e951fb93fe6a20ae main.lua
1461c828f1fad08bb6f7f96ddde56368af499caca6deb7a709d3ad34bdcf8378 mod.json
6b2ae2e24202b97ca88173cca3412e0c6ba1245fbf2896067e98bbdf2d523599 README.md
-94
View File
@@ -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,
}
-62
View File
@@ -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
}
}
}
-224
View File
@@ -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,
}
-26
View File
@@ -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
}
}
}
-23
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
a3f3c066b97ab4318fa58dc4f31d897f498c5cfd9da716c126d836f2ed4069ad main.lua
da06ca442a0270968341e9bb54cd1eec34987046f9434e8f09254ae2c074d7d9 mod.json
b0fe8ba660657ac6ca84e98735fc362ca7ab6c41dc22d73ed2ac50fbe1911e2e README.md
-280
View File
@@ -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,
}
-177
View File
@@ -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
}
}
}
-21
View File
@@ -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.
-3
View File
@@ -1,3 +0,0 @@
b2cf588b2b14b818d574d62f6e8c5d9f76ae256d9171871ec3b0b80b16f3b44b main.lua
c18fa0fe6f9259b7310c9dc063784ca265889a4dbfb77e95286550702eda6cc1 mod.json
e79c9177d3a4a37ae52ac23c8b95e02c7a122398706925b8de2416fb8d28f4af README.md
-305
View File
@@ -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,
}
-47
View File
@@ -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
}
}
}
+9
View File
@@ -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' |