@cimplify/sdk 0.108.0 → 0.110.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/advanced.d.mts +4 -3
- package/dist/advanced.d.ts +4 -3
- package/dist/advanced.js +24 -24
- package/dist/advanced.mjs +5 -5
- package/dist/animation-4EXBXRVX.js +27 -0
- package/dist/animation-6DICNWUP.mjs +6 -0
- package/dist/audio-DhvyjqdA.d.mts +249 -0
- package/dist/audio-m0w8cnxq.d.ts +249 -0
- package/dist/avatars.d.mts +110 -0
- package/dist/avatars.d.ts +110 -0
- package/dist/avatars.js +82 -0
- package/dist/avatars.mjs +5 -0
- package/dist/backend-gUCzs5Kg.d.mts +3866 -0
- package/dist/backend-gUCzs5Kg.d.ts +3866 -0
- package/dist/{catalogue-B4eLL8Mi.d.ts → catalogue-CPTMyBUz.d.mts} +109 -221
- package/dist/catalogue-DzrTXP3v.d.mts +541 -0
- package/dist/catalogue-DzrTXP3v.d.ts +541 -0
- package/dist/{catalogue-OT89_Ifs.d.mts → catalogue-OTuS5zp-.d.ts} +109 -221
- package/dist/chunk-22BD3RHN.mjs +1809 -0
- package/dist/chunk-25T4JHEA.mjs +21 -0
- package/dist/chunk-2AQ237Y4.js +508 -0
- package/dist/chunk-2IC4H6OV.mjs +48 -0
- package/dist/chunk-2LHLG4OB.mjs +71 -0
- package/dist/chunk-2O2CG465.js +1709 -0
- package/dist/chunk-2Q4DTHXK.js +316 -0
- package/dist/chunk-2QTQ6BUH.mjs +46 -0
- package/dist/chunk-2UN4SFLR.mjs +100 -0
- package/dist/chunk-2V75I7K3.js +454 -0
- package/dist/chunk-2V7IL4HC.js +10 -0
- package/dist/chunk-34XBXUMW.js +296 -0
- package/dist/chunk-3C4C654I.js +71 -0
- package/dist/chunk-3DAD2LDF.mjs +72 -0
- package/dist/chunk-3GXH2NFD.mjs +4 -0
- package/dist/chunk-3O47ZFW3.js +961 -0
- package/dist/chunk-3OY3VYNF.mjs +11 -0
- package/dist/chunk-3S6WPBZL.js +2936 -0
- package/dist/chunk-3TUOUP5T.js +399 -0
- package/dist/chunk-3VEH23I5.mjs +1380 -0
- package/dist/chunk-3ZYSAW2T.mjs +263 -0
- package/dist/chunk-4ANVF274.js +103 -0
- package/dist/chunk-4C7I3ELN.js +388 -0
- package/dist/chunk-4DYKAYFO.mjs +8 -0
- package/dist/chunk-4ZDR72SG.js +329 -0
- package/dist/chunk-52YS45RR.js +1066 -0
- package/dist/chunk-5325XSCH.js +107 -0
- package/dist/chunk-5AD5X6AV.js +13 -0
- package/dist/chunk-5BYDGGWO.mjs +197 -0
- package/dist/chunk-5C3C7VDM.mjs +55 -0
- package/dist/chunk-5HFMY3D7.js +660 -0
- package/dist/chunk-5LH2QN3T.js +1862 -0
- package/dist/chunk-5PJEXSOW.js +44 -0
- package/dist/chunk-5SXDH3Y7.js +90 -0
- package/dist/chunk-5YFBGGCK.mjs +502 -0
- package/dist/chunk-5YWN6H5Q.mjs +133 -0
- package/dist/chunk-66QP3SMN.js +402 -0
- package/dist/chunk-6DAEZKIG.mjs +385 -0
- package/dist/chunk-6DATW7TB.mjs +420 -0
- package/dist/{chunk-JRGK3NXR.mjs → chunk-6ETDAU62.mjs} +72 -212
- package/dist/chunk-6H254EVP.mjs +178 -0
- package/dist/chunk-6PFNJPBJ.mjs +820 -0
- package/dist/chunk-6PJWXIFX.mjs +357 -0
- package/dist/chunk-6SWKWCMS.js +80 -0
- package/dist/chunk-6VVXXX7S.js +808 -0
- package/dist/chunk-6WUBSMTX.js +1403 -0
- package/dist/chunk-73SGZLAQ.mjs +35 -0
- package/dist/chunk-76L2V6KD.mjs +4 -0
- package/dist/chunk-7AAIAVAA.js +1531 -0
- package/dist/chunk-7ARSJPV3.mjs +93 -0
- package/dist/chunk-7BHW5GTC.mjs +1234 -0
- package/dist/chunk-7DQSDKDU.js +6 -0
- package/dist/chunk-7IQQQENP.mjs +401 -0
- package/dist/chunk-7KFTYENS.js +11357 -0
- package/dist/chunk-7SM6XSPZ.js +3171 -0
- package/dist/chunk-7SXOHAIR.mjs +28 -0
- package/dist/chunk-A2MAX6S4.js +90 -0
- package/dist/chunk-A33OORT7.mjs +1530 -0
- package/dist/{chunk-U6ACF76H.mjs → chunk-A36JRXXH.mjs} +1075 -1845
- package/dist/chunk-A7BU3BK4.js +52 -0
- package/dist/chunk-A7MQSN2V.mjs +77 -0
- package/dist/chunk-A7V4L2IT.mjs +416 -0
- package/dist/chunk-AABEK6XU.js +226 -0
- package/dist/chunk-AGV33FWC.mjs +5005 -0
- package/dist/chunk-AHNR3BXI.js +108 -0
- package/dist/chunk-AQ5RLBU2.mjs +286 -0
- package/dist/chunk-AQOE6WCG.js +1270 -0
- package/dist/chunk-ATAWRACV.mjs +62 -0
- package/dist/chunk-ATCURNUV.js +14 -0
- package/dist/chunk-AWPV33OO.js +6 -0
- package/dist/chunk-AYJPHKYV.js +337 -0
- package/dist/chunk-AZ5BGWHB.js +162 -0
- package/dist/chunk-AZAXD44P.js +1373 -0
- package/dist/chunk-BBGKQIJJ.mjs +406 -0
- package/dist/chunk-BP4JLSUA.js +1453 -0
- package/dist/{chunk-SUCE7FWA.mjs → chunk-BTUSVHIE.mjs} +539 -978
- package/dist/chunk-BU6OK542.mjs +818 -0
- package/dist/chunk-CVSRWM6W.js +418 -0
- package/dist/chunk-CYBMFMJD.mjs +247 -0
- package/dist/chunk-DDJLDOUX.mjs +217 -0
- package/dist/{chunk-V32BCG7M.js → chunk-DFZWHNRG.js} +75 -215
- package/dist/chunk-DJ2C7AVG.js +644 -0
- package/dist/chunk-DS2VU4WW.mjs +102 -0
- package/dist/chunk-DSAG7T5P.js +7290 -0
- package/dist/chunk-E2QPPH7I.js +327 -0
- package/dist/chunk-E65WCTIG.mjs +1187 -0
- package/dist/{chunk-33VCYWVL.mjs → chunk-E6QQKDNE.mjs} +30 -42
- package/dist/chunk-ECDK6QBF.mjs +981 -0
- package/dist/chunk-EIB4OWVQ.mjs +620 -0
- package/dist/chunk-EQUZYCGO.js +250 -0
- package/dist/chunk-F3UJ6ZRM.mjs +313 -0
- package/dist/chunk-F63N2ZTQ.js +7023 -0
- package/dist/chunk-F6XRYEM3.mjs +26 -0
- package/dist/chunk-FE6VCE4K.js +5030 -0
- package/dist/chunk-FLWD6GRR.js +1372 -0
- package/dist/chunk-FQBFWJBS.mjs +110 -0
- package/dist/chunk-FXVJ2HFU.js +26 -0
- package/dist/chunk-FZMQMQBW.js +312 -0
- package/dist/chunk-G3EEC44R.js +53 -0
- package/dist/{chunk-KG3WT4PR.mjs → chunk-GCKWKLZF.mjs} +73 -104
- package/dist/chunk-GCX7TY4D.mjs +6826 -0
- package/dist/chunk-GDB6TCSD.js +450 -0
- package/dist/chunk-GDCV4IWO.mjs +21 -0
- package/dist/chunk-GDZT5LZ5.js +170 -0
- package/dist/chunk-GHPUYD4I.mjs +213 -0
- package/dist/chunk-GHS6MIL6.mjs +1432 -0
- package/dist/chunk-GPS2THCL.mjs +11 -0
- package/dist/{chunk-EV2EVOYS.js → chunk-GTJWQAGU.js} +31 -42
- package/dist/chunk-GVTI6UJQ.mjs +130 -0
- package/dist/chunk-GXKI64OT.mjs +1138 -0
- package/dist/chunk-GYDYF2WX.js +275 -0
- package/dist/chunk-GYLP6UG4.mjs +705 -0
- package/dist/chunk-H3OQKK6X.js +1158 -0
- package/dist/chunk-H5DFSCJ6.js +40 -0
- package/dist/chunk-HAGEIC2L.mjs +186 -0
- package/dist/chunk-HAJ4JRFO.js +293 -0
- package/dist/chunk-HDR72GB6.js +188 -0
- package/dist/chunk-HF5HPS4T.mjs +40 -0
- package/dist/chunk-HK6LOFDS.js +218 -0
- package/dist/chunk-HMWYTXCY.mjs +143 -0
- package/dist/chunk-HPCSRCMA.js +89 -0
- package/dist/chunk-HSEO6TPU.js +247 -0
- package/dist/chunk-I5FOHCLH.js +132 -0
- package/dist/chunk-IDLW4NWA.mjs +85 -0
- package/dist/chunk-IIDZ5WUX.js +61 -0
- package/dist/chunk-IR74FJHU.js +2332 -0
- package/dist/chunk-IRVFYAKP.mjs +100 -0
- package/dist/{chunk-XBL3TMPC.mjs → chunk-IZDWAL53.mjs} +1795 -3287
- package/dist/chunk-J2KZXIU3.js +51 -0
- package/dist/chunk-J2U6PJSO.js +91 -0
- package/dist/chunk-J5AJNY2N.js +283 -0
- package/dist/chunk-J6KQZ4FO.mjs +821 -0
- package/dist/{chunk-OWW5GUSB.js → chunk-JGBCIKJV.js} +7 -6
- package/dist/chunk-JKGTBMU7.mjs +25 -0
- package/dist/chunk-JR3TNEQC.js +422 -0
- package/dist/chunk-JURRXY2J.mjs +29 -0
- package/dist/chunk-K3HW5I2A.mjs +317 -0
- package/dist/chunk-K46SMZ66.mjs +103 -0
- package/dist/chunk-K4UDGE3Z.mjs +6 -0
- package/dist/chunk-KDQOR4QQ.mjs +350 -0
- package/dist/chunk-KDTVOLHI.mjs +288 -0
- package/dist/chunk-KGHJH5KJ.mjs +35 -0
- package/dist/chunk-KIZOA5LX.mjs +140 -0
- package/dist/chunk-KK6HYVBI.mjs +242 -0
- package/dist/chunk-KMKWGXMK.js +856 -0
- package/dist/chunk-KPA6K5IB.js +80 -0
- package/dist/chunk-KU7O4IXU.mjs +260 -0
- package/dist/chunk-KVFNRJOR.mjs +11142 -0
- package/dist/chunk-KZJRSXHK.js +2432 -0
- package/dist/chunk-LHWPRUJL.js +79 -0
- package/dist/chunk-LIV3KXJG.mjs +369 -0
- package/dist/chunk-LKV4LACG.js +21 -0
- package/dist/chunk-LUY45X3U.mjs +263 -0
- package/dist/chunk-LYEZZEEF.mjs +17 -0
- package/dist/chunk-M5EXQZYY.mjs +1519 -0
- package/dist/chunk-ME6HGG4I.mjs +1338 -0
- package/dist/chunk-MEXP6DA6.js +31 -0
- package/dist/chunk-MG52AW3H.js +902 -0
- package/dist/chunk-MIPO6F72.js +334 -0
- package/dist/{chunk-5QW7CQBA.js → chunk-MKIJQL5Q.js} +211 -321
- package/dist/chunk-MN3YETVN.mjs +17 -0
- package/dist/chunk-MOBSBXVE.mjs +2391 -0
- package/dist/chunk-MOMOZEUM.mjs +88 -0
- package/dist/chunk-MRFUNRAP.js +62 -0
- package/dist/chunk-MTIHEW2M.js +433 -0
- package/dist/chunk-N4RGGDEL.mjs +73 -0
- package/dist/chunk-NH654HAT.js +10 -0
- package/dist/chunk-NHJUGZUR.mjs +253 -0
- package/dist/chunk-NMQT4HG3.mjs +757 -0
- package/dist/chunk-NPSYB6DC.mjs +906 -0
- package/dist/chunk-NW2GPMSO.js +857 -0
- package/dist/chunk-NXKQOQ6L.js +76 -0
- package/dist/chunk-NXLAZIJZ.js +309 -0
- package/dist/chunk-NYWNA5CF.js +148 -0
- package/dist/chunk-NZTKLY2W.js +1442 -0
- package/dist/chunk-O2ICABUD.js +36 -0
- package/dist/chunk-OIVVQQ22.js +862 -0
- package/dist/chunk-OKC2B4O3.mjs +312 -0
- package/dist/chunk-OLWFPDBK.js +2220 -0
- package/dist/chunk-OMTVH334.js +3779 -0
- package/dist/chunk-OQRVEM56.mjs +362 -0
- package/dist/chunk-OXMIBOD4.js +337 -0
- package/dist/chunk-PBL6YL5L.mjs +288 -0
- package/dist/chunk-PDOELNNQ.js +97 -0
- package/dist/chunk-PDQJY2GZ.js +136 -0
- package/dist/chunk-PKOZXD2Q.js +1274 -0
- package/dist/chunk-PPF2BYFL.mjs +2191 -0
- package/dist/chunk-PQTQKNKD.js +539 -0
- package/dist/chunk-PRSR4FAV.mjs +81 -0
- package/dist/chunk-PTRNO5UD.js +95 -0
- package/dist/chunk-Q2Y3VMKH.js +2000 -0
- package/dist/chunk-QCZOE65N.js +96 -0
- package/dist/chunk-QDYFLATW.mjs +52 -0
- package/dist/chunk-QQABGCMM.js +22 -0
- package/dist/chunk-QS6TRHMR.js +345 -0
- package/dist/chunk-R56P64LB.mjs +208 -0
- package/dist/chunk-RCQ2MPBV.js +428 -0
- package/dist/chunk-RFVRPYR2.mjs +856 -0
- package/dist/chunk-RKBLPGPV.js +11521 -0
- package/dist/chunk-RM2V4IND.mjs +327 -0
- package/dist/chunk-RQXN5A34.mjs +613 -0
- package/dist/chunk-RUVGVNOF.mjs +2687 -0
- package/dist/chunk-SAFJIGYB.mjs +165 -0
- package/dist/chunk-SKQFLBDD.js +263 -0
- package/dist/chunk-SMK6JU2F.mjs +2092 -0
- package/dist/{chunk-UVRPC2P7.js → chunk-SNNEOS5Z.js} +1984 -3476
- package/dist/chunk-SPCRQ6NO.mjs +1246 -0
- package/dist/chunk-SWX4SALA.mjs +19 -0
- package/dist/{chunk-JVODCGFP.mjs → chunk-SYEO2WV4.mjs} +128 -238
- package/dist/chunk-T2QQMX2N.js +10 -0
- package/dist/chunk-T3AFF3L7.mjs +57 -0
- package/dist/chunk-T6Z2EXYL.js +712 -0
- package/dist/chunk-T7LOXGYX.js +118 -0
- package/dist/chunk-TJCAE7RC.mjs +1315 -0
- package/dist/chunk-TJI3K6RP.js +137 -0
- package/dist/chunk-TL2SVO4P.mjs +55 -0
- package/dist/chunk-TM63CYGN.mjs +399 -0
- package/dist/chunk-TU5RYTYU.mjs +13 -0
- package/dist/chunk-TVJZ4GPS.js +1553 -0
- package/dist/chunk-TWM6DLYF.js +371 -0
- package/dist/chunk-U63OC4TQ.js +30 -0
- package/dist/{chunk-L5GKVQVJ.js → chunk-U6BVSJZ6.js} +25 -57
- package/dist/chunk-U6UVWEAK.mjs +792 -0
- package/dist/chunk-UCEPNO6N.mjs +11048 -0
- package/dist/{chunk-ANT574FD.js → chunk-UCR5HRYM.js} +74 -105
- package/dist/chunk-UG5BTOTF.mjs +334 -0
- package/dist/chunk-UGH37BQW.mjs +157 -0
- package/dist/chunk-V7ZRH3PY.mjs +271 -0
- package/dist/chunk-VBGHBL3E.mjs +1394 -0
- package/dist/chunk-VRBDAS5I.js +920 -0
- package/dist/chunk-VVE3Y76O.js +230 -0
- package/dist/chunk-W65JIRI4.mjs +1182 -0
- package/dist/chunk-WHYKAT7G.js +62 -0
- package/dist/{chunk-YOIVZ6OA.mjs → chunk-WIZRUIZB.mjs} +25 -57
- package/dist/chunk-WKWTDZS7.js +385 -0
- package/dist/chunk-WLMINRC4.mjs +78 -0
- package/dist/chunk-WOF3AD3J.js +22 -0
- package/dist/chunk-WWKMOA76.js +1262 -0
- package/dist/chunk-X5A5FOWW.js +615 -0
- package/dist/chunk-X6CN6KQF.mjs +192 -0
- package/dist/chunk-XD2PMH46.mjs +517 -0
- package/dist/chunk-XJA2NGTI.mjs +88 -0
- package/dist/chunk-XJIHN3BY.mjs +8 -0
- package/dist/chunk-XK6UDWJS.js +203 -0
- package/dist/chunk-XPG3N5CF.mjs +415 -0
- package/dist/chunk-XVZ2NKSU.mjs +74 -0
- package/dist/chunk-XYP45OLE.js +117 -0
- package/dist/chunk-XZSDGZX6.mjs +301 -0
- package/dist/chunk-Y2EMGN4T.mjs +120 -0
- package/dist/chunk-Y3XWP5XW.mjs +298 -0
- package/dist/chunk-Y4K22JL6.js +228 -0
- package/dist/chunk-Y65LXQGA.mjs +3164 -0
- package/dist/chunk-Y66WVMO5.mjs +87 -0
- package/dist/chunk-Y7JWLB25.mjs +7280 -0
- package/dist/chunk-YCDXAJKU.js +175 -0
- package/dist/chunk-YRDQCCIS.js +152 -0
- package/dist/chunk-YV52DGSA.js +111 -0
- package/dist/chunk-YXGZQPUD.js +94 -0
- package/dist/chunk-Z3A3AFVU.mjs +102 -0
- package/dist/chunk-ZCY6YKKP.mjs +475 -0
- package/dist/chunk-ZEQHXVU5.js +215 -0
- package/dist/chunk-ZF7JCXCI.js +284 -0
- package/dist/chunk-ZJBRUH6V.js +29 -0
- package/dist/chunk-ZMIL4ZOA.js +365 -0
- package/dist/chunk-ZR5ULMN2.mjs +337 -0
- package/dist/chunk-ZXREOYOW.js +87 -0
- package/dist/{client-BSP9U1O1.d.mts → client-CX_WR0-J.d.mts} +2 -2
- package/dist/client-Dytx_ZTC.d.ts +230 -0
- package/dist/{client-B878xeAP.d.ts → client-dqOhP9Nc.d.ts} +2 -2
- package/dist/client-hTKbCuhG.d.mts +230 -0
- package/dist/clock-B-UAjkKa.d.mts +5 -0
- package/dist/clock-B-UAjkKa.d.ts +5 -0
- package/dist/config-BlXQluJR.d.mts +36 -0
- package/dist/config-BlXQluJR.d.ts +36 -0
- package/dist/courier-Cnn-edJ4.d.ts +95 -0
- package/dist/courier-FmGfkvIN.d.mts +95 -0
- package/dist/crowd-DdHFbqKK.d.ts +186 -0
- package/dist/crowd-hOVSAmA3.d.mts +186 -0
- package/dist/danfo-BEBapFlX.d.mts +146 -0
- package/dist/danfo-UQ_PTIIx.d.ts +146 -0
- package/dist/{elements-Bty6Qs_N.d.mts → elements-DdsRig-2.d.mts} +1548 -257
- package/dist/{elements-Bty6Qs_N.d.ts → elements-DdsRig-2.d.ts} +1548 -257
- package/dist/engines/godot.d.mts +56 -0
- package/dist/engines/godot.d.ts +56 -0
- package/dist/engines/godot.js +28 -0
- package/dist/engines/godot.mjs +3 -0
- package/dist/engines/wasm.d.mts +11 -0
- package/dist/engines/wasm.d.ts +11 -0
- package/dist/engines/wasm.js +178 -0
- package/dist/engines/wasm.mjs +176 -0
- package/dist/fallback-CrG_lTFk.d.mts +270 -0
- package/dist/fallback-CrG_lTFk.d.ts +270 -0
- package/dist/fallback-KDB4Y2CE.mjs +6 -0
- package/dist/fallback-Y26TQENE.js +19 -0
- package/dist/features-CZB4rmcD.d.mts +118 -0
- package/dist/features-CZB4rmcD.d.ts +118 -0
- package/dist/fit-BWiE7rgA.d.mts +44 -0
- package/dist/fit-DK0oT7zO.d.ts +44 -0
- package/dist/five-a-side-Bv1AOUy3.d.mts +115 -0
- package/dist/five-a-side-EC51SJY1.d.ts +115 -0
- package/dist/fluid-ARr0qUIL.d.ts +939 -0
- package/dist/fluid-DLlEj2cs.d.mts +939 -0
- package/dist/game/conformance.d.mts +84 -0
- package/dist/game/conformance.d.ts +84 -0
- package/dist/game/conformance.js +1025 -0
- package/dist/game/conformance.mjs +994 -0
- package/dist/game/heap.d.mts +40 -0
- package/dist/game/heap.d.ts +40 -0
- package/dist/game/heap.js +48 -0
- package/dist/game/heap.mjs +7 -0
- package/dist/game/kit/canvas.d.mts +240 -0
- package/dist/game/kit/canvas.d.ts +240 -0
- package/dist/game/kit/canvas.js +3566 -0
- package/dist/game/kit/canvas.mjs +1039 -0
- package/dist/game/kit/courier.d.mts +17 -0
- package/dist/game/kit/courier.d.ts +17 -0
- package/dist/game/kit/courier.js +66 -0
- package/dist/game/kit/courier.mjs +49 -0
- package/dist/game/kit/danfo.d.mts +11 -0
- package/dist/game/kit/danfo.d.ts +11 -0
- package/dist/game/kit/danfo.js +109 -0
- package/dist/game/kit/danfo.mjs +44 -0
- package/dist/game/kit/engine.d.mts +29 -0
- package/dist/game/kit/engine.d.ts +29 -0
- package/dist/game/kit/engine.js +183 -0
- package/dist/game/kit/engine.mjs +30 -0
- package/dist/game/kit/five-a-side.d.mts +11 -0
- package/dist/game/kit/five-a-side.d.ts +11 -0
- package/dist/game/kit/five-a-side.js +86 -0
- package/dist/game/kit/five-a-side.mjs +41 -0
- package/dist/game/kit/frontier.d.mts +344 -0
- package/dist/game/kit/frontier.d.ts +344 -0
- package/dist/game/kit/frontier.js +435 -0
- package/dist/game/kit/frontier.mjs +14 -0
- package/dist/game/kit/genesis.d.mts +752 -0
- package/dist/game/kit/genesis.d.ts +752 -0
- package/dist/game/kit/genesis.js +380 -0
- package/dist/game/kit/genesis.mjs +15 -0
- package/dist/game/kit/ludo.d.mts +15 -0
- package/dist/game/kit/ludo.d.ts +15 -0
- package/dist/game/kit/ludo.js +87 -0
- package/dist/game/kit/ludo.mjs +42 -0
- package/dist/game/kit/runner.d.mts +12 -0
- package/dist/game/kit/runner.d.ts +12 -0
- package/dist/game/kit/runner.js +83 -0
- package/dist/game/kit/runner.mjs +46 -0
- package/dist/game/kit/shootout.d.mts +13 -0
- package/dist/game/kit/shootout.d.ts +13 -0
- package/dist/game/kit/shootout.js +122 -0
- package/dist/game/kit/shootout.mjs +57 -0
- package/dist/game/kit/slots.d.mts +392 -0
- package/dist/game/kit/slots.d.ts +392 -0
- package/dist/game/kit/slots.js +215 -0
- package/dist/game/kit/slots.mjs +6 -0
- package/dist/game/kit/tycoon.d.mts +16 -0
- package/dist/game/kit/tycoon.d.ts +16 -0
- package/dist/game/kit/tycoon.js +145 -0
- package/dist/game/kit/tycoon.mjs +44 -0
- package/dist/game/kit/word.d.mts +11 -0
- package/dist/game/kit/word.d.ts +11 -0
- package/dist/game/kit/word.js +78 -0
- package/dist/game/kit/word.mjs +41 -0
- package/dist/game/kit/worlds.d.mts +18 -0
- package/dist/game/kit/worlds.d.ts +18 -0
- package/dist/game/kit/worlds.js +74 -0
- package/dist/game/kit/worlds.mjs +49 -0
- package/dist/game/kit.d.mts +53 -0
- package/dist/game/kit.d.ts +53 -0
- package/dist/game/kit.js +2610 -0
- package/dist/game/kit.mjs +101 -0
- package/dist/game/prepared.d.mts +14 -0
- package/dist/game/prepared.d.ts +14 -0
- package/dist/game/prepared.js +81 -0
- package/dist/game/prepared.mjs +32 -0
- package/dist/game-Bx8onkhX.d.mts +1451 -0
- package/dist/game-fgQy2YIm.d.ts +1451 -0
- package/dist/game-theme-CBTw1b_d.d.mts +35 -0
- package/dist/game-theme-Dg-ict5A.d.ts +35 -0
- package/dist/game.d.mts +3546 -0
- package/dist/game.d.ts +3546 -0
- package/dist/game.js +4455 -0
- package/dist/game.mjs +50 -0
- package/dist/graphics-C6YjWYvP.d.ts +5 -0
- package/dist/graphics-Dk2yQnYN.d.mts +5 -0
- package/dist/guest-DrsA00XD.d.mts +732 -0
- package/dist/guest-wIdfoT05.d.ts +732 -0
- package/dist/hit-stop-BKkVwTq7.d.mts +43 -0
- package/dist/hit-stop-D-8pPYnM.d.ts +43 -0
- package/dist/hot-Crn9XpHJ.d.ts +29 -0
- package/dist/hot-DH3nOhQD.d.mts +29 -0
- package/dist/{index-CQStGb3B.d.mts → index-6gkHMGAq.d.mts} +2 -2
- package/dist/index-B8ybCKJI.d.ts +41 -0
- package/dist/{index-DJwXmoaP.d.ts → index-B98Xac8e.d.ts} +2 -2
- package/dist/{index-B7oA_1XI.d.ts → index-B_ByTtIT.d.ts} +2 -2
- package/dist/index-DHjNHb1c.d.mts +41 -0
- package/dist/{index-DhUSTAhT.d.mts → index-DbAN_VQE.d.mts} +2 -2
- package/dist/index.d.mts +134 -24
- package/dist/index.d.ts +134 -24
- package/dist/index.js +407 -248
- package/dist/index.mjs +184 -58
- package/dist/interactives-authoring.d.mts +231 -0
- package/dist/interactives-authoring.d.ts +231 -0
- package/dist/interactives-authoring.js +1743 -0
- package/dist/interactives-authoring.mjs +1704 -0
- package/dist/interactives-canvas.global.js +148 -0
- package/dist/interactives-guest.d.mts +20 -0
- package/dist/interactives-guest.d.ts +20 -0
- package/dist/interactives-guest.js +63 -0
- package/dist/interactives-guest.json +1 -0
- package/dist/interactives-guest.mjs +61 -0
- package/dist/interactives-jsc.global.js +9357 -0
- package/dist/interactives-meters.d.mts +93 -0
- package/dist/interactives-meters.d.ts +93 -0
- package/dist/interactives-meters.js +252 -0
- package/dist/interactives-meters.mjs +68 -0
- package/dist/interactives-production.d.mts +430 -0
- package/dist/interactives-production.d.ts +430 -0
- package/dist/interactives-production.js +1231 -0
- package/dist/interactives-production.mjs +1170 -0
- package/dist/interactives-record.d.mts +112 -0
- package/dist/interactives-record.d.ts +112 -0
- package/dist/interactives-record.js +302 -0
- package/dist/interactives-record.mjs +295 -0
- package/dist/interactives-runtime.d.mts +61 -0
- package/dist/interactives-runtime.d.ts +61 -0
- package/dist/interactives-runtime.js +442 -0
- package/dist/interactives-runtime.mjs +25 -0
- package/dist/interactives-studio-kernel.global.js +1 -0
- package/dist/interactives-studio.d.mts +806 -0
- package/dist/interactives-studio.d.ts +806 -0
- package/dist/interactives-studio.js +5741 -0
- package/dist/interactives-studio.mjs +5651 -0
- package/dist/interactives-tick.d.mts +90 -0
- package/dist/interactives-tick.d.ts +90 -0
- package/dist/interactives-tick.js +203 -0
- package/dist/interactives-tick.mjs +6 -0
- package/dist/interactives-web.d.mts +762 -0
- package/dist/interactives-web.d.ts +762 -0
- package/dist/interactives-web.js +5000 -0
- package/dist/interactives-web.mjs +4628 -0
- package/dist/interactives.d.mts +2430 -0
- package/dist/interactives.d.ts +2430 -0
- package/dist/interactives.js +5723 -0
- package/dist/interactives.mjs +4333 -0
- package/dist/juice-CoHxlpKA.d.ts +42 -0
- package/dist/juice-DQztFS43.d.mts +42 -0
- package/dist/launch-T2f4_Di4.d.mts +151 -0
- package/dist/launch-d6oreMzR.d.ts +151 -0
- package/dist/library-DXYaMFhi.d.mts +69 -0
- package/dist/library-DXYaMFhi.d.ts +69 -0
- package/dist/link-BUTqGmyi.d.mts +38 -0
- package/dist/link-BUTqGmyi.d.ts +38 -0
- package/dist/lint.js +104 -204
- package/dist/lint.mjs +103 -203
- package/dist/local-D8_I6UeL.d.mts +6967 -0
- package/dist/local-VZkqsCzR.d.ts +6967 -0
- package/dist/lockstep-BLf6dglI.d.mts +321 -0
- package/dist/lockstep-D50Ia0JW.d.ts +321 -0
- package/dist/ludo-B8ClkDqr.d.ts +205 -0
- package/dist/ludo-BOuohQ-M.d.mts +205 -0
- package/dist/mock/cli.mjs +200 -56
- package/dist/mock/library.js +200 -56
- package/dist/mock/library.mjs +200 -56
- package/dist/mock/msw.js +200 -56
- package/dist/mock/msw.mjs +200 -56
- package/dist/native-BodxSh03.d.mts +16 -0
- package/dist/native-DUHMT8gA.d.ts +16 -0
- package/dist/network-ZXLD7l1b.d.mts +19 -0
- package/dist/network-ZXLD7l1b.d.ts +19 -0
- package/dist/online-XeymeM9s.d.ts +381 -0
- package/dist/online-d7W_Al9a.d.mts +381 -0
- package/dist/params-DxFb5G_U.d.mts +155 -0
- package/dist/params-DxFb5G_U.d.ts +155 -0
- package/dist/play-BRRlfLhX.d.ts +940 -0
- package/dist/play-ClrMd_e8.d.mts +940 -0
- package/dist/policy-CCw8tXLm.d.mts +3078 -0
- package/dist/policy-CMqOSQIu.d.ts +3078 -0
- package/dist/ports-B_0v_P3A.d.mts +13 -0
- package/dist/ports-CGU_3e_0.d.ts +13 -0
- package/dist/prefetch-BHnicOQj.d.ts +117 -0
- package/dist/prefetch-CBXgYacv.d.mts +117 -0
- package/dist/{price-CSztR02F.d.mts → price-BOzexBww.d.mts} +1 -1
- package/dist/{price-C_vasx9A.d.ts → price-C1h9sPxE.d.ts} +1 -1
- package/dist/protocol-C8qbpUbT.d.mts +242 -0
- package/dist/protocol-C8qbpUbT.d.ts +242 -0
- package/dist/rapier-AZHT4YI7.js +7886 -0
- package/dist/rapier-ODEPCQA6.mjs +7792 -0
- package/dist/react.d.mts +157 -15
- package/dist/react.d.ts +157 -15
- package/dist/react.js +5564 -5904
- package/dist/react.mjs +5451 -5808
- package/dist/runner-CQcLL7By.d.ts +189 -0
- package/dist/runner-D46Yj-wE.d.mts +189 -0
- package/dist/runtime-BRWfkf_q.d.mts +51 -0
- package/dist/runtime-DyuBMkKM.d.ts +51 -0
- package/dist/scheduling-D2ajXpMC.d.ts +224 -0
- package/dist/scheduling-DSRbbaZb.d.mts +224 -0
- package/dist/schema-BrWxzKp6.d.mts +63 -0
- package/dist/schema-BrWxzKp6.d.ts +63 -0
- package/dist/server/evict.js +34 -37
- package/dist/server/evict.mjs +34 -37
- package/dist/server/tanstack.d.mts +49 -3
- package/dist/server/tanstack.d.ts +49 -3
- package/dist/server/tanstack.js +169 -117
- package/dist/server/tanstack.mjs +166 -118
- package/dist/{server-Dckg4Vdr.d.ts → server-D29IIuDV.d.ts} +136 -130
- package/dist/{server-B6dW7-Lj.d.mts → server-DQMWPnc8.d.mts} +136 -130
- package/dist/server.d.mts +45 -5
- package/dist/server.d.ts +45 -5
- package/dist/server.js +171 -227
- package/dist/server.mjs +165 -225
- package/dist/session-grant-CJ27DwrT.d.mts +19 -0
- package/dist/session-grant-CJ27DwrT.d.ts +19 -0
- package/dist/shootout-BJZeX6FG.d.ts +140 -0
- package/dist/shootout-tcZd1Rxt.d.mts +140 -0
- package/dist/state-replica-stMtdkVD.d.mts +42 -0
- package/dist/state-replica-stMtdkVD.d.ts +42 -0
- package/dist/studio-CowqWqez.d.mts +14 -0
- package/dist/studio-CowqWqez.d.ts +14 -0
- package/dist/styles.css +1 -1
- package/dist/template-AhCdB1kW.d.mts +190 -0
- package/dist/template-BkEGVTQj.d.ts +190 -0
- package/dist/terrain-BMxEc6Oc.d.ts +429 -0
- package/dist/terrain-C-XC1Oui.d.mts +429 -0
- package/dist/testing/msw.d.mts +28 -4
- package/dist/testing/msw.d.ts +28 -4
- package/dist/testing/msw.js +15 -21
- package/dist/testing/msw.mjs +15 -21
- package/dist/testing/suite.d.mts +6 -5
- package/dist/testing/suite.d.ts +6 -5
- package/dist/testing/suite.js +89 -124
- package/dist/testing/suite.mjs +81 -116
- package/dist/testing.d.mts +27 -1227
- package/dist/testing.d.ts +27 -1227
- package/dist/testing.js +127 -131
- package/dist/testing.mjs +19 -23
- package/dist/tick-transport-C8QEh8Uw.d.mts +50 -0
- package/dist/tick-transport-UjHY3xK1.d.ts +50 -0
- package/dist/timeline-protocol-hp6e8zFB.d.mts +47 -0
- package/dist/timeline-protocol-hp6e8zFB.d.ts +47 -0
- package/dist/tokens-DhanQRQS.d.mts +21 -0
- package/dist/tokens-K8yxwd6G.d.ts +21 -0
- package/dist/tree-C-Io2Hbx.d.mts +35 -0
- package/dist/tree-CP_qSW4H.d.ts +35 -0
- package/dist/tycoon-B5KHk2Xx.d.mts +255 -0
- package/dist/tycoon-hmHo-3HX.d.ts +255 -0
- package/dist/types-BQkxPwZP.d.mts +219 -0
- package/dist/types-D_f6UUxM.d.ts +219 -0
- package/dist/ui-DwHicuFv.d.mts +563 -0
- package/dist/ui-hi7S2YcH.d.ts +563 -0
- package/dist/{sign-in-DQlIdqsi.d.mts → url-52ndUruG.d.mts} +15 -1
- package/dist/{sign-in-DQlIdqsi.d.ts → url-52ndUruG.d.ts} +15 -1
- package/dist/utils.d.mts +3 -3
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js +37 -37
- package/dist/utils.mjs +4 -4
- package/dist/vars-Dc_PrhBK.d.mts +511 -0
- package/dist/vars-Dc_PrhBK.d.ts +511 -0
- package/dist/vm-IUARFFAI.mjs +63 -0
- package/dist/vm-N7L6MGGH.js +65 -0
- package/dist/weave-delivery-CW-Kstut.d.mts +360 -0
- package/dist/weave-delivery-CW-Kstut.d.ts +360 -0
- package/dist/web-Dss74a7v.d.ts +883 -0
- package/dist/web-f9ZQPoyw.d.mts +883 -0
- package/dist/web-transport-C7MkBL0a.d.ts +19 -0
- package/dist/web-transport-aYEqXMd9.d.mts +19 -0
- package/dist/weight-CPGjsgjV.d.ts +92 -0
- package/dist/weight-tF1ODR1p.d.mts +92 -0
- package/dist/word-BcX9Jcmv.d.mts +70 -0
- package/dist/word-C7sVPLHm.d.ts +70 -0
- package/dist/world-PvILHDR1.d.ts +2285 -0
- package/dist/world-qekkXjJv.d.mts +2285 -0
- package/dist/worlds-Br2Pb0Cs.d.ts +109 -0
- package/dist/worlds-D5ydYr7d.d.mts +109 -0
- package/engines/c/cimplify.h +108 -0
- package/engines/c/cimplify.lib.js +11 -0
- package/engines/godot/addons/cimplify/cimplify.gd +97 -0
- package/engines/rust/interactives-ctx/Cargo.toml +9 -0
- package/engines/rust/interactives-ctx/src/lib.rs +189 -0
- package/package.json +189 -3
- package/vendor/rapier3d-compat/LICENSE +201 -0
- package/vendor/rapier3d-compat/PATCHES.md +61 -0
- package/dist/chunk-2P4KXFB6.js +0 -274
- package/dist/chunk-3G6RQLXK.mjs +0 -21
- package/dist/chunk-3V4DVXV3.mjs +0 -254
- package/dist/chunk-EDK3LNM6.js +0 -2436
- package/dist/chunk-F7J3HK27.js +0 -4549
- package/dist/chunk-U4MW6PYU.mjs +0 -23
- package/dist/chunk-XREIZ56K.js +0 -26
|
@@ -0,0 +1,1704 @@
|
|
|
1
|
+
import './chunk-25T4JHEA.mjs';
|
|
2
|
+
|
|
3
|
+
// src/interactives/authoring/threejs.ts
|
|
4
|
+
var THREE_GEOMETRIES = {
|
|
5
|
+
BoxGeometry: { fn: "boxGeometry", defaults: [1, 1, 1], simpleArgs: 3, extendedFn: "segmentedBoxGeometry" },
|
|
6
|
+
SphereGeometry: { fn: "sphereGeometry", defaults: [1, 32, 16], simpleArgs: 3, extendedFn: "sphereSectorGeometry" },
|
|
7
|
+
PlaneGeometry: { fn: "planeGeometry", defaults: [1, 1], simpleArgs: 2, extendedFn: "planeGridGeometry" },
|
|
8
|
+
CylinderGeometry: { fn: "cylinderGeometry", defaults: [1, 1, 1, 32], simpleArgs: 4, extendedFn: "cylinderShellGeometry" },
|
|
9
|
+
ConeGeometry: { fn: "coneGeometry", defaults: [1, 1, 32], simpleArgs: 7, extendedFn: null },
|
|
10
|
+
TorusGeometry: { fn: "torusGeometry", defaults: [1, 0.4, 12, 48], simpleArgs: 5, extendedFn: null },
|
|
11
|
+
TorusKnotGeometry: { fn: "torusKnotGeometry", defaults: [1, 0.4, 64, 8, 2, 3], simpleArgs: 6, extendedFn: null },
|
|
12
|
+
CapsuleGeometry: { fn: "capsuleGeometry", defaults: [1, 1, 4, 8], simpleArgs: 5, extendedFn: null },
|
|
13
|
+
CircleGeometry: { fn: "circleGeometry", defaults: [1, 32], simpleArgs: 4, extendedFn: null },
|
|
14
|
+
RingGeometry: { fn: "ringGeometry", defaults: [0.5, 1, 32, 1], simpleArgs: 6, extendedFn: null },
|
|
15
|
+
IcosahedronGeometry: { fn: "icosahedronGeometry", defaults: [1, 0], simpleArgs: 2, extendedFn: null },
|
|
16
|
+
OctahedronGeometry: { fn: "octahedronGeometry", defaults: [1, 0], simpleArgs: 2, extendedFn: null },
|
|
17
|
+
TetrahedronGeometry: { fn: "tetrahedronGeometry", defaults: [1, 0], simpleArgs: 2, extendedFn: null },
|
|
18
|
+
DodecahedronGeometry: { fn: "dodecahedronGeometry", defaults: [1, 0], simpleArgs: 2, extendedFn: null }
|
|
19
|
+
}, THREE_MATERIALS = {
|
|
20
|
+
MeshStandardMaterial: { kind: "standard", shininessToRoughness: false, roughness: null },
|
|
21
|
+
MeshPhysicalMaterial: { kind: "physical", shininessToRoughness: false, roughness: null },
|
|
22
|
+
MeshPhongMaterial: { kind: "standard", shininessToRoughness: true, roughness: null },
|
|
23
|
+
MeshLambertMaterial: { kind: "standard", shininessToRoughness: false, roughness: 1 },
|
|
24
|
+
MeshBasicMaterial: { kind: "unlit", shininessToRoughness: false, roughness: null },
|
|
25
|
+
MeshToonMaterial: { kind: "toon", shininessToRoughness: false, roughness: null },
|
|
26
|
+
MeshMatcapMaterial: { kind: "matcap", shininessToRoughness: false, roughness: null }
|
|
27
|
+
}, THREE_LIGHTS = {
|
|
28
|
+
DirectionalLight: { light: "directional", params: ["color", "intensity"] },
|
|
29
|
+
PointLight: { light: "point", params: ["color", "intensity", "distance", "decay"] },
|
|
30
|
+
SpotLight: { light: "spot", params: ["color", "intensity", "distance", "angle", "penumbra", "decay"] },
|
|
31
|
+
AmbientLight: { light: "ambient", params: ["color", "intensity"] },
|
|
32
|
+
HemisphereLight: { light: "hemisphere", params: ["color", "groundColor", "intensity"] },
|
|
33
|
+
RectAreaLight: { light: "rectArea", params: ["color", "intensity", "width", "height"] }
|
|
34
|
+
}, THREE_MATERIAL_PROPERTIES = [
|
|
35
|
+
"color",
|
|
36
|
+
"roughness",
|
|
37
|
+
"metalness",
|
|
38
|
+
"emissive",
|
|
39
|
+
"emissiveIntensity",
|
|
40
|
+
"map",
|
|
41
|
+
"normalMap",
|
|
42
|
+
"roughnessMap",
|
|
43
|
+
"metalnessMap",
|
|
44
|
+
"emissiveMap",
|
|
45
|
+
"aoMap",
|
|
46
|
+
"flatShading",
|
|
47
|
+
"transparent",
|
|
48
|
+
"opacity",
|
|
49
|
+
"side",
|
|
50
|
+
"wireframe",
|
|
51
|
+
"depthTest",
|
|
52
|
+
"depthWrite",
|
|
53
|
+
"alphaTest",
|
|
54
|
+
"vertexColors",
|
|
55
|
+
"clearcoat",
|
|
56
|
+
"clearcoatRoughness",
|
|
57
|
+
"transmission",
|
|
58
|
+
"ior",
|
|
59
|
+
"thickness",
|
|
60
|
+
"sheen",
|
|
61
|
+
"sheenColor",
|
|
62
|
+
"sheenRoughness",
|
|
63
|
+
"iridescence",
|
|
64
|
+
"anisotropy",
|
|
65
|
+
"gradientMap",
|
|
66
|
+
"matcap"
|
|
67
|
+
], THREE_TEXTURE_SLOTS = ["map", "normalMap", "roughnessMap", "metalnessMap", "emissiveMap", "aoMap", "gradientMap", "matcap"], THREE_SIDES = { FrontSide: "front", BackSide: "back", DoubleSide: "double" }, THREE_CONTROLS = {
|
|
68
|
+
OrbitControls: "OrbitControls",
|
|
69
|
+
MapControls: "MapControls",
|
|
70
|
+
PointerLockControls: "PointerLockControls",
|
|
71
|
+
FirstPersonControls: "FirstPersonControls",
|
|
72
|
+
FlyControls: "FlyControls",
|
|
73
|
+
TrackballControls: "TrackballControls",
|
|
74
|
+
DragControls: "DragControls"
|
|
75
|
+
}, THREE_MATH_HELPERS = {
|
|
76
|
+
clamp: "clamp",
|
|
77
|
+
lerp: "lerp",
|
|
78
|
+
damp: "damp",
|
|
79
|
+
degToRad: "degToRad",
|
|
80
|
+
radToDeg: "radToDeg",
|
|
81
|
+
smoothstep: "smoothstep",
|
|
82
|
+
euclideanModulo: "euclideanModulo"
|
|
83
|
+
}, THREE_VECTOR_METHODS = {
|
|
84
|
+
set: "set",
|
|
85
|
+
copy: "copy",
|
|
86
|
+
add: "add",
|
|
87
|
+
sub: "sub",
|
|
88
|
+
multiplyScalar: "scale",
|
|
89
|
+
addScaledVector: "addScaled",
|
|
90
|
+
dot: "dot",
|
|
91
|
+
cross: "cross",
|
|
92
|
+
length: "length",
|
|
93
|
+
lengthSq: "lengthSq",
|
|
94
|
+
normalize: "normalize",
|
|
95
|
+
lerp: "lerp",
|
|
96
|
+
distanceTo: "distanceTo",
|
|
97
|
+
distanceToSquared: "distanceToSquared",
|
|
98
|
+
negate: "negate",
|
|
99
|
+
min: "min",
|
|
100
|
+
max: "max",
|
|
101
|
+
clamp: "clamp",
|
|
102
|
+
clampLength: "clampLength",
|
|
103
|
+
subVectors: "subVectors",
|
|
104
|
+
addVectors: "addVectors",
|
|
105
|
+
applyQuaternion: "applyQuat",
|
|
106
|
+
angleTo: "angleTo",
|
|
107
|
+
equals: "equals"
|
|
108
|
+
};
|
|
109
|
+
function geometryRows() {
|
|
110
|
+
return Object.entries(THREE_GEOMETRIES).map(([three, rule]) => ({
|
|
111
|
+
three,
|
|
112
|
+
area: "geometry",
|
|
113
|
+
status: "auto",
|
|
114
|
+
sdk: `game.graphics.createGeometry(${rule.fn}(...))`,
|
|
115
|
+
note: rule.extendedFn === null ? `Same parameter order as Three.js; Three.js defaults (${rule.defaults.join(", ")}) are written out.` : `Same parameter order; more than ${rule.simpleArgs} arguments use ${rule.extendedFn}.`
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
function materialRows() {
|
|
119
|
+
return Object.entries(THREE_MATERIALS).map(([three, rule]) => ({
|
|
120
|
+
three,
|
|
121
|
+
area: "material",
|
|
122
|
+
status: "auto",
|
|
123
|
+
sdk: `game.graphics.createMaterial({ kind: "${rule.kind}", ... })`,
|
|
124
|
+
note: rule.shininessToRoughness ? "shininess becomes roughness = sqrt(2 / (shininess + 2)); specular is dropped." : rule.roughness !== null ? `Lambert shading is standard with roughness ${rule.roughness}.` : "Parameters keep their names; textures bind when they finish loading."
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
function lightRows() {
|
|
128
|
+
return Object.entries(THREE_LIGHTS).map(([three, rule]) => ({
|
|
129
|
+
three,
|
|
130
|
+
area: "light",
|
|
131
|
+
status: "auto",
|
|
132
|
+
sdk: `port.light("${rule.light}", { ${rule.params.join(", ")} })`,
|
|
133
|
+
note: "The light is an entity; directional and spot lights aim at light.target (the origin unless set)."
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
function controlRows() {
|
|
137
|
+
return Object.entries(THREE_CONTROLS).map(([three, kit]) => ({
|
|
138
|
+
three,
|
|
139
|
+
area: "controls",
|
|
140
|
+
status: "auto",
|
|
141
|
+
sdk: `new ${kit}(game.transforms, camera, game.input, options) from @cimplify/sdk/game/kit`,
|
|
142
|
+
note: three === "PointerLockControls" || three === "FirstPersonControls" ? "Property names match; update(dt) takes game.stepSeconds. For a walking game with phone controls prefer firstPerson(game, options)." : "Property names match; update(dt) takes game.stepSeconds. Touch drag, pinch and wheel work through game.input."
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
var MEMBER_ROWS = [
|
|
146
|
+
{ three: "Scene", area: "scene", status: "auto", sdk: "the game world (no object)", note: "scene.add(x) at setup is dropped; later adds show the entity; scene.remove(x) hides it." },
|
|
147
|
+
{ three: "scene.background = Color", area: "scene", status: "auto", sdk: "game.graphics.setEnvironment({ background: { color } })", note: "Merged with fog and shadows into one setEnvironment call." },
|
|
148
|
+
{ three: "Fog / FogExp2", area: "scene", status: "auto", sdk: 'setEnvironment({ fog: { mode: "linear" | "exp2", ... } })', note: "" },
|
|
149
|
+
{ three: "Object3D", area: "object", status: "auto", sdk: "port.object()", note: "Every Object3D becomes an Entity with a transform." },
|
|
150
|
+
{ three: "Group", area: "object", status: "auto", sdk: "port.group()", note: "group.add(child) parents with game.transforms.setParent." },
|
|
151
|
+
{ three: "Mesh", area: "object", status: "auto", sdk: "port.mesh(geometry, material, CAST_SHADOW | RECEIVE_SHADOW)", note: "castShadow and receiveShadow assignments next to the mesh fold into the mesh flags." },
|
|
152
|
+
{ three: "object.position / scale .set, .x, .y, .z, +=", area: "object", status: "auto", sdk: "game.transforms.setPosition / port.positionAxis / port.setPositionAxis / port.addPositionAxis", note: "Axis reads and writes go straight to the transform columns; no vector objects." },
|
|
153
|
+
{ three: "object.rotation .set, .x, .y, .z", area: "object", status: "auto", sdk: "port.setRotation / port.rotationAxis / port.setRotationAxis / port.addRotationAxis", note: "Euler XYZ angles are kept per entity and written as a quaternion." },
|
|
154
|
+
{ three: "object.lookAt", area: "object", status: "auto", sdk: "port.lookAt(entity, x, y, z) / port.lookAtEntity", note: "Cameras and lights face -Z, other objects +Z, as in Three.js." },
|
|
155
|
+
{ three: "a.position.distanceTo(b.position)", area: "object", status: "auto", sdk: "port.distance(a, b)", note: "distanceToSquared maps to port.distanceSquared." },
|
|
156
|
+
{ three: "a.position.copy(b.position)", area: "object", status: "auto", sdk: "port.copyPosition(a, b)", note: "" },
|
|
157
|
+
{ three: "object.visible", area: "object", status: "auto", sdk: "port.setVisible(entity, visible)", note: "" },
|
|
158
|
+
{ three: "object.userData", area: "object", status: "assisted", sdk: "typed arrays indexed by entityIndex(entity), or fields on your own class", note: "Reesa turns userData into columns." },
|
|
159
|
+
{ three: "object.rotateX/Y/Z, translateX/Y/Z", area: "object", status: "assisted", sdk: "Quat.multiply and game.transforms.translate", note: "Local-space operations need a quaternion; Reesa rewrites them." },
|
|
160
|
+
{ three: "PerspectiveCamera", area: "camera", status: "auto", sdk: "port.perspectiveCamera(fov, near, far)", note: "The aspect argument is dropped; the platform sizes the view." },
|
|
161
|
+
{ three: "OrthographicCamera", area: "camera", status: "auto", sdk: "port.orthographicCamera(left, right, top, bottom, near, far)", note: "" },
|
|
162
|
+
{ three: "camera.updateProjectionMatrix / aspect", area: "camera", status: "auto", sdk: "nothing (the platform resizes)", note: "Dropped." },
|
|
163
|
+
{ three: "WebGLRenderer", area: "renderer", status: "auto", sdk: "nothing (runCanvasGame picks the engine)", note: "setSize, setPixelRatio, domElement and render(scene, camera) are dropped; shadowMap.enabled and toneMapping move to setEnvironment." },
|
|
164
|
+
{ three: "window resize listener", area: "renderer", status: "auto", sdk: "nothing", note: "The platform handles resize and safe areas." },
|
|
165
|
+
{ three: "requestAnimationFrame(animate) / renderer.setAnimationLoop(animate)", area: "loop", status: "auto", sdk: "GameScene.fixedUpdate(game)", note: "The loop body runs at the fixed 60 Hz step, so frame-counted motion keeps its speed and replays deterministically." },
|
|
166
|
+
{ three: "Clock.getDelta()", area: "loop", status: "auto", sdk: "game.stepSeconds", note: "" },
|
|
167
|
+
{ three: "Clock.getElapsedTime() / clock.elapsedTime / performance.now()", area: "loop", status: "auto", sdk: "port.elapsedSeconds()", note: "" },
|
|
168
|
+
{ three: "keydown / keyup listeners", area: "input", status: "auto", sdk: "game.input.defineButton per key, replayed into your handler on justPressed and justReleased", note: "Every key code the handlers mention becomes an action; a touch path still needs adding (advisory residue)." },
|
|
169
|
+
{ three: "pointerdown / click / mousedown / touchstart", area: "input", status: "auto", sdk: 'game.input.defineButton("pointer", [{ device: "pointer" }])', note: "Handlers that read clientX or clientY are left for Reesa." },
|
|
170
|
+
{ three: "Raycaster", area: "picking", status: "assisted", sdk: "game.graphics.raycast(origin, direction, hit) or game.physics.raycast", note: "" },
|
|
171
|
+
{ three: "GLTFLoader.load(path, onLoad)", area: "loader", status: "auto", sdk: "port.model(path).then((gltf) => ...)", note: "gltf.scene becomes port.instantiate(gltf); the file must be in assets/ (ctx.assets)." },
|
|
172
|
+
{ three: "TextureLoader.load(path)", area: "loader", status: "auto", sdk: "port.texture(path)", note: "Materials bind the texture when it arrives with port.withTexture." },
|
|
173
|
+
{ three: "AnimationMixer / clipAction / play", area: "animation", status: "auto", sdk: "port.clipAction(root, clipName).play()", note: "mixer.update(dt) is dropped; the engine advances clips. gltf.animations[i] becomes gltf.clipNames[i]." },
|
|
174
|
+
{ three: "Vector3", area: "math", status: "auto", sdk: "Vec3 from @cimplify/sdk/game", note: "multiplyScalar is scale; a Vector3 created inside the loop is reported as a frame allocation." },
|
|
175
|
+
{ three: "Quaternion", area: "math", status: "auto", sdk: "Quat from @cimplify/sdk/game", note: "" },
|
|
176
|
+
{ three: "Color(hex)", area: "math", status: "auto", sdk: "a 0xrrggbb number", note: "CSS hex strings convert; other color strings are residue." },
|
|
177
|
+
{ three: "MathUtils.clamp/lerp/damp/degToRad/radToDeg/smoothstep", area: "math", status: "auto", sdk: "MathUtils.clamp/lerp/damp/degToRad/radToDeg/smoothstep/euclideanModulo from @cimplify/sdk/game/kit", note: "MathUtils.randFloat(a, b) becomes game.random.range(a, b)." },
|
|
178
|
+
{ three: "Math.random()", area: "math", status: "auto", sdk: "game.random.float()", note: "Seeded so replays and tests are deterministic." },
|
|
179
|
+
{ three: "document.getElementById(id).textContent = text", area: "dom", status: "auto", sdk: "game.ui.setText(game.ui.hudText(id), text)", note: "Each element becomes a HUD text slot." },
|
|
180
|
+
{ three: "other DOM and window access", area: "dom", status: "manual", sdk: "game.ui panels, ctx.ui", note: "The canvas profile has no page DOM." },
|
|
181
|
+
{ three: "Audio / PositionalAudio / AudioListener", area: "audio", status: "assisted", sdk: "game.audio (setListener, play, buses)", note: "" },
|
|
182
|
+
{ three: "ShaderMaterial / custom shaders", area: "material", status: "manual", sdk: 'createMaterial({ kind: "shader" }) with WGSL', note: "GLSL must be rewritten." },
|
|
183
|
+
{ three: "EffectComposer passes", area: "renderer", status: "assisted", sdk: "game.graphics.createPostChain({ effects })", note: "Bloom, FXAA, SMAA, SSAO and friends have built in effects." }
|
|
184
|
+
], THREEJS_PORT_MAP = [...MEMBER_ROWS, ...geometryRows(), ...materialRows(), ...lightRows(), ...controlRows()], THREEJS_PORT_TOPIC_ID = "porting-threejs", MAX_RESIDUE_IN_REQUEST = 80, STATUS_LABEL = { auto: "converted", assisted: "Reesa finishes", manual: "rewrite by hand" };
|
|
185
|
+
function mappingLine(row) {
|
|
186
|
+
let note = row.note.length > 0 ? ` ${row.note}` : "";
|
|
187
|
+
return `- ${row.three} -> ${row.sdk} (${STATUS_LABEL[row.status]}).${note}`;
|
|
188
|
+
}
|
|
189
|
+
function threejsPortGuide() {
|
|
190
|
+
return `cimplify port threejs <dir> converts a Three.js game into a game_api Interactive. It writes src/scene.ts (a GameScene class named after the game, for example CoinDashScene, built with the game API and ThreePort from @cimplify/sdk/game/kit), src/client.ts (runCanvasGame), src/conformance.ts and port-report.json. Primitive constants stay module constants; other module state becomes class fields (doubles in closure variables allocate on every write); functions become private methods; top-level setup runs in enter(game); the animate loop runs from fixedUpdate at 60 Hz; for...of over arrays becomes an indexed loop.
|
|
191
|
+
|
|
192
|
+
ThreePort (const port = new ThreePort(game, assets)) is the thin, allocation-free bridge for Three.js idioms: object(), group(), mesh(geometry, material, flags), light(kind, options), perspectiveCamera(fov, near, far), add(parent, child), setVisible, positionAxis/setPositionAxis/addPositionAxis with AXIS_X, AXIS_Y, AXIS_Z, the same for scale and rotation (Euler XYZ), setRotation, lookAt, lookAtEntity, copyPosition, distance, distanceSquared, model(path), instantiate(model), texture(path), withTexture(material, slot, texture), clipAction(root, clip), elapsedSeconds(). Prefer game.transforms and game.graphics directly in new code.
|
|
193
|
+
|
|
194
|
+
Finishing a port (residue items in port-report.json, each with file, line, construct, source and hint):
|
|
195
|
+
- unconverted items are Three.js code the converter left as it was; the project does not build until each is rewritten with the game API.
|
|
196
|
+
- advisory items build but miss the platform bar: add a touch path (a controls preset from the controls topic, TouchControls or virtual bindings) because phones have no keyboard, hoist allocations out of fixedUpdate and update, and move per-object data (userData) into typed arrays.
|
|
197
|
+
- Never import three or its addons. Keep the creator's gameplay, tuning numbers and names.
|
|
198
|
+
|
|
199
|
+
Mapping table:
|
|
200
|
+
${THREEJS_PORT_MAP.map(mappingLine).join(`
|
|
201
|
+
`)}`;
|
|
202
|
+
}
|
|
203
|
+
var THREEJS_PORT_TOPIC = {
|
|
204
|
+
id: THREEJS_PORT_TOPIC_ID,
|
|
205
|
+
title: "Porting from Three.js",
|
|
206
|
+
summary: "cimplify port threejs output, ThreePort helpers, the Three.js to game API mapping table and how to finish residue items.",
|
|
207
|
+
body: threejsPortGuide()
|
|
208
|
+
};
|
|
209
|
+
function residueLine(item) {
|
|
210
|
+
return `- [${item.id}] ${item.file}:${item.line}:${item.column} ${item.construct} (${item.area}). Source: ${item.source.replace(/\s+/g, " ").trim()}. Hint: ${item.hint}`;
|
|
211
|
+
}
|
|
212
|
+
function threejsPortRequest(report) {
|
|
213
|
+
let unconverted = report.residue.filter((r) => r.kind === "unconverted"), advisory = report.residue.filter((r) => r.kind === "advisory"), shown = [...unconverted, ...advisory].slice(0, (80)), hidden = report.residue.length - shown.length, sections = [
|
|
214
|
+
`Finish porting this Three.js game to the Cimplify game API. The converter already rewrote ${report.converted} Three.js constructs into src/scene.ts; read the ${THREEJS_PORT_TOPIC_ID} guide topic first, then src/scene.ts.`
|
|
215
|
+
];
|
|
216
|
+
return unconverted.length > 0 && sections.push(`Rewrite these ${unconverted.length} unconverted constructs with the game API (the project does not build until they are gone):
|
|
217
|
+
${shown.filter((r) => r.kind === "unconverted").map(residueLine).join(`
|
|
218
|
+
`)}`), advisory.length > 0 && sections.push(`Then address these ${advisory.length} advisory items:
|
|
219
|
+
${shown.filter((r) => r.kind === "advisory").map(residueLine).join(`
|
|
220
|
+
`)}`), hidden > 0 && sections.push(`${hidden} more items are listed in port-report.json; read it and finish them too.`), sections.push("Keep the creator's gameplay, numbers and names. Never import three. Allocate nothing in fixedUpdate or update. Call check when you are done."), sections.join(`
|
|
221
|
+
|
|
222
|
+
`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/interactives/authoring/topics.ts
|
|
226
|
+
var AUTHORING_RULES = `Cimplify Interactives authoring rules
|
|
227
|
+
|
|
228
|
+
Project layout. An Interactive is a folder: interactive.toml (manifest), cimplify.lock (pinned SDK, ctx and library versions; never edit it), package.json, tsconfig.json, src/client.ts (client entry, default export async function main(ctx: Ctx): Promise<void>), optional src/room.ts (shared rules), src/worker.ts (jobs) and src/tick.ts (an authoritative real-time tick room, [entries] tick; read the tick-rooms topic), src/conformance.ts for games (default export a frame hook factory used by the publication gates), tests/ (vitest), assets/ (models, textures, sounds, data).
|
|
229
|
+
|
|
230
|
+
Manifest (interactive.toml). Tables: [interactive] name, description, category ("games" or "everyday"), id (assigned on first publish, never invent one); [profile] kind ("native" for GenUI trees, "canvas" for games and creative surfaces), game ("game_api" for the Cimplify game API, "wasm" or "godot" for engine profiles), engine_features (list from: particles, raw_compute, raw_render, text, pick, post_shaders, audio, physics, models, images, ai, ui); [entries] client, room, worker, conformance, channels; [capabilities] each ctx scope with the reason shown to the player (for example share = "Share your score with friends"); [[providers]] every external destination with id, destination, data_use, purpose; [economy] currencies, items, odds, offers, rules; [ai] payer, budgets, characters, prompts, schemas, actions, styles; [rating] violence, sexuality, language, fear (each "none", "mild", "moderate" or "strong") and simulated_gambling (bool); [delivery] min_device_tier ("low", "mid", "high"), download_size (the brotli-compressed bytes a device transfers for what the package itself owns; the engine and SDK runtimes are shared platform code a device downloads once, and no package is billed for them), [[delivery.groups]] name, before_play, assets globs; [[content]] Workshop items (item, version, group).
|
|
231
|
+
|
|
232
|
+
Profiles. Native profile code emits GenUI trees through ctx.ui and has no DOM. Canvas profile code runs in an isolated iframe; games use the Cimplify game API (@cimplify/sdk/game and @cimplify/sdk/game/kit, with @cimplify/sdk/game/kit/canvas for runCanvasGame). A game_api game never imports three, phaser or @dimforge/rapier3d-compat directly and never imports engine backends; the platform picks the engine.
|
|
233
|
+
|
|
234
|
+
Allowed imports. Only @cimplify/sdk/interactives, @cimplify/sdk/game, @cimplify/sdk/game/kit, @cimplify/sdk/game/kit/canvas, @cimplify/sdk/game/kit/engine, @cimplify/sdk/game/kit/slots (typed template slots and remix manifests), the kit template entries (@cimplify/sdk/game/kit/shootout, runner, five-a-side, ludo, word, tycoon, danfo, courier, worlds, frontier, genesis), and relative imports inside the project. Node built-ins, URLs and unlisted npm packages are refused.
|
|
235
|
+
|
|
236
|
+
Admission refuses: eval, new Function, string timers, dynamic import(), require, fetch or XMLHttpRequest or WebSocket (network goes through ctx and declared providers only), raw DOM outside the canvas surface in the native profile, hard-coded URLs, minified or obfuscated code, binaries and prebuilt wasm, catastrophic regular expressions, text that collects passwords, card numbers or other credentials, payment wording outside ctx.wallet and host checkout, and any ctx scope used without a [capabilities] entry. TypeScript is checked in strict mode; every error blocks the build.
|
|
237
|
+
|
|
238
|
+
Performance doctrine. Frame and fixed-step code allocates nothing: create typed arrays, Vec3 and Quat scratch objects, materials, geometries and batches once in enter() or constructors, then reuse them. No array literals, closures, string building or object spreads inside fixedUpdate or update. Keep simulation in fixedUpdate (deterministic, fixed step) and presentation in update. Use game.random (seeded) for gameplay randomness, never Math.random. Use the detSin, detCos, detLog and detExp helpers inside simulation that must replay bit for bit. For hundreds or thousands of similar entities use an entity table (game.table, see the entity-tables topic): bulk op first, kernel second, cols or each third, cursor forEach last (on iOS, per-row JS above a few thousand rows is not viable: use bulk ops or kernels).
|
|
239
|
+
|
|
240
|
+
Style. Write clear TypeScript without comments. Name constants instead of repeating numbers. Keep the game playable on touch (virtual controls or pointer) and keyboard.
|
|
241
|
+
|
|
242
|
+
Size. A template-scale game is a few hundred lines. Before writing your own inventory, shop, quest tracker, enemy waves, score and timer HUD, NPC daily routine, collectibles, checkpoints, arena or playtest bot, read the blocks topic and use the kit building blocks; they are allocation free and deterministic already.`, GAME_LOOP = {
|
|
243
|
+
id: "game-loop",
|
|
244
|
+
title: "Game API: game, scenes, loop and entities",
|
|
245
|
+
summary: "createGame or runCanvasGame, GameScene enter/fixedUpdate/update, entities, transforms, seeded random.",
|
|
246
|
+
body: `A canvas game hands a scene to the kit runner, which creates the game on the best engine for the device, wires DOM or surface input and drives the loop:
|
|
247
|
+
|
|
248
|
+
import type { Ctx } from "@cimplify/sdk/interactives";
|
|
249
|
+
import type { Game, GameScene } from "@cimplify/sdk/game";
|
|
250
|
+
import { runCanvasGame } from "@cimplify/sdk/game/kit/canvas";
|
|
251
|
+
|
|
252
|
+
class Play implements GameScene<Game> {
|
|
253
|
+
readonly name = "play";
|
|
254
|
+
enter(game: Game): void {}
|
|
255
|
+
fixedUpdate(game: Game): void {}
|
|
256
|
+
update(game: Game): void {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default async function main(ctx: Ctx): Promise<void> {
|
|
260
|
+
await runCanvasGame(ctx, { scene: new Play() });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
runCanvasGame options: scene, seed, backend, api ("webgl2" default), ui, onFrame(game, nowMs), warmup. It returns the Game.
|
|
264
|
+
|
|
265
|
+
Game members: world (spawn(), despawn(entity)), transforms, loop (FixedStepLoop), random (SeededRandom: next(), unit(), range(lo, hi)), input, physics, nav, ai (GameAi or null when the backend has no AI), graphics, animation, audio, ui, net, scratch, scenes, clock, stepSeconds. addScene(scene), addSystem(system), start(sceneName), scenes.switchTo(name).
|
|
266
|
+
|
|
267
|
+
GameScene: readonly name, optional systems (GameSystem[] with fixedUpdate/update), enter(game), exit(game), fixedUpdate(game) at the fixed rate (game.stepSeconds, 60 Hz by default), update(game) once per rendered frame. Build everything in enter(); keep fixedUpdate allocation free.
|
|
268
|
+
|
|
269
|
+
Entities and transforms: const e = game.world.spawn(); game.transforms.add(e, x, y, z); setPosition(e, x, y, z); setRotation(e, qx, qy, qz, qw); setScale(e, sx, sy, sz); translate(e, dx, dy, dz); position(e, outVec3); rotation(e, outQuat); setParent(child, parent); teleport(e, x, y, z). A yaw rotation is setRotation(e, 0, Math.sin(yaw / 2), 0, Math.cos(yaw / 2)).
|
|
270
|
+
|
|
271
|
+
Math: Vec2, Vec3, Vec4, Quat, Box3, Sphere, Ray, Plane, curves (CatmullRomCurve3 and friends), Noise, mat4 helpers, detSin/detCos for deterministic simulation.
|
|
272
|
+
|
|
273
|
+
HUD: const slot = game.ui.hudNumber("fares"); game.ui.setNumber(slot, value); hudText(name) and setText(slot, text) for words; game.ui.haptic("light" | "medium" | "heavy" | "success" | "warning" | "error" | "selection"); game.ui.show(panel, props) and hide(panel) for host panels.`
|
|
274
|
+
}, INPUT = {
|
|
275
|
+
id: "input",
|
|
276
|
+
title: "Input actions and touch controls",
|
|
277
|
+
summary: "defineButton/defineAxis bindings for keys, pointer, gamepad and virtual touch controls; pressed/value reads.",
|
|
278
|
+
body: `Define actions once in enter():
|
|
279
|
+
|
|
280
|
+
const steer = game.input.defineAxis("steer", [
|
|
281
|
+
{ device: "keys", negative: "ArrowLeft", positive: "ArrowRight" },
|
|
282
|
+
{ device: "keys", negative: "KeyA", positive: "KeyD" },
|
|
283
|
+
{ device: "virtual", control: "steer" },
|
|
284
|
+
]);
|
|
285
|
+
const gas = game.input.defineButton("gas", [{ device: "key", code: "ArrowUp" }, { device: "key", code: "KeyW" }, { device: "pointer" }, { device: "virtual", control: "gas" }]);
|
|
286
|
+
|
|
287
|
+
ButtonBinding devices: "key" (code is a KeyboardEvent.code), "pointer", "gamepad" (button, threshold), "virtual" (control name). AxisBinding devices: "keys" (negative, positive), "gamepad" (axis, deadzone, invert), "virtual" (control, invert).
|
|
288
|
+
|
|
289
|
+
Read in fixedUpdate: game.input.pressed(action), justPressed(action), justReleased(action), value(action) (axis in -1..1, button 0..1). game.input.pointerCount() and pointerAt(i) give raw pointers; PinchGesture, LongPressGesture, WheelState and OrientationState are exported from @cimplify/sdk/game.
|
|
290
|
+
|
|
291
|
+
Touch controls: phones have no keyboard, so every game needs a touch path. For a walking or driving game use a one-line preset from the controls topic (thirdPersonCharacter, firstPerson, vehicleDriver); they draw sticks and buttons with the engine game UI and feed the same actions as keys and gamepads. For custom pads use TouchControls from the kit (sticks, a look area and buttons that write virtual controls), or feed virtual actions yourself with game.input.virtualEvent(control, value, timeMs). Touches that land on no control pass through to the game as normal pointers. game.input.look holds this frame's look drag in points. game.input.drag, motion, wheel and pinch are per fixed step (read them in fixedUpdate: a frame's movement lands on its first step, so the steps add up to it exactly); game.input.frame holds the same for the rendered frame (read it in update, as cameras do), with pressed, released, pressX and pressY for the press. game.input.device reports the last device used (InputDevice.keyboard, pointer or gamepad).
|
|
292
|
+
|
|
293
|
+
Kit controls (from @cimplify/sdk/game/kit): OrbitControls, MapControls, FirstPersonControls, PointerLockControls, FlyControls, DragControls, TrackballControls, GyroLookControls. VirtualStick, VirtualSlider and TiltSteering (from @cimplify/sdk/game) drive craft.`
|
|
294
|
+
}, CONTROLS = {
|
|
295
|
+
id: "controls",
|
|
296
|
+
title: "Controls kit: one-line third-person, first-person and vehicle presets",
|
|
297
|
+
summary: "thirdPersonCharacter, firstPerson, vehicleDriver: touch sticks and buttons, character motor, collision cameras, lock-on, animation.",
|
|
298
|
+
body: `One call wires touch controls, the character or vehicle and the camera. Call it once in enter() and read your own actions in fixedUpdate:
|
|
299
|
+
|
|
300
|
+
import type { Game, GameScene } from "@cimplify/sdk/game";
|
|
301
|
+
import { thirdPersonCharacter, type ThirdPersonRig } from "@cimplify/sdk/game/kit";
|
|
302
|
+
|
|
303
|
+
class Plaza implements GameScene<Game> {
|
|
304
|
+
readonly name = "plaza";
|
|
305
|
+
private hero: ThirdPersonRig | null = null;
|
|
306
|
+
|
|
307
|
+
enter(game: Game): void {
|
|
308
|
+
this.hero = thirdPersonCharacter(game, { position: [0, 0, 0], ui: ctx.ui.game(), viewport: { width, height } });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fixedUpdate(game: Game): void {
|
|
312
|
+
const hero = this.hero;
|
|
313
|
+
if (hero !== null && game.input.justPressed(hero.actions.interact)) this.talk();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
The rig registers its own system (pass register: false and call rig.fixedUpdate(game) and rig.update(game) from your scene to control order). Pass ui: ctx.ui.game() to draw the controls with the engine game UI (restyle with theme: theme({ accent: "#ff6a00ff" })); without ui the controls still work, invisibly. On the engine surface the controls follow the screen size and safe-area insets on their own (rotation, notches); elsewhere pass viewport: { width, height, insets } or call rig.controls.setViewport. VoiceOver and TalkBack users can press every on-screen button and nudge the sticks (swipe up and down), with no extra code. Controls hide themselves when the player switches to keyboard or gamepad and come back on the next touch.
|
|
318
|
+
|
|
319
|
+
Presets:
|
|
320
|
+
- thirdPersonCharacter(game, options): floating move stick bottom left, a look area on the right half, Jump, Run (toggle), Use, Crouch (toggle) and Lock buttons. Character: walk below half stick, run at full stick, sprint, crouch, coyote time, jump buffering, variable jump height (release early for a short hop), slopes, steps, ground snapping, moving platforms, turning to face movement. Camera: third-person follow with collision (never clips through walls; pulls in at once, eases back out), shoulder offset (rig.camera.swapShoulder()), recenter (rig.camera.recenter()), sprint FOV kick and lock-on (push entities into rig.lockTargets; Q, right stick press or the Lock button toggles the nearest one in front).
|
|
321
|
+
- firstPerson(game, options): same controls with Fire (drag across it to aim while firing); eye height drops when crouched; head bob and landing dip (view: { headBob: false } turns bob off).
|
|
322
|
+
- vehicleDriver(game, { preset: "danfo" | "keke" | "okada" | "car", position }) or { vehicle: existingKitVehicle }: steering stick (horizontal only), Gas, Brake, Drift (handbrake) and Horn buttons; chase camera tuned per vehicle with speed FOV and distance, free look that returns behind, wall pull-in.
|
|
323
|
+
|
|
324
|
+
Options: character (radius, height, crouchHeight, walkSpeed, runSpeed, sprintSpeed, jumpHeight, gravity, coyoteSeconds, jumpBufferSeconds, stepHeight, maxSlopeDegrees, turnSharpness, modelForward), follow (distance, height, shoulder, pitch, fov, sprintFovKick, lookSensitivity, invertY, autoRecenter, lockSoftZone), layout ("leftStickRightButtons", "twinStick", "oneThumb", "driving"), move (mode: "floating" | "fixed", radius, deadZone, curve), look (sensitivity, inertiaSeconds), buttons ({ control, label, icon, mode: "hold" | "toggle", doubleTap, cooldownSeconds, lookThrough }[]) and prefix (action name prefix when two rigs share a game; call rig.setActive(false) on the one not in use, for example when the player gets into a danfo).
|
|
325
|
+
|
|
326
|
+
Actions (rig.actions): moveX, moveY, lookX, lookY, jump, sprint, crouch, interact, fire, lockOn, swapShoulder, recenter for characters; steer, gas, brake, handbrake, horn for vehicles. Keyboard (WASD, arrows, Space, Shift, C, E, F, Q, V, R), gamepad and touch all feed these actions, and a touch acts in the same frame it lands.
|
|
327
|
+
|
|
328
|
+
Animation: pass animation: { port: game.graphics.clips, options: { locomotion: blendSpace, air: fallAction, crouch: crouchSpace, footIk: { left, right, physics: game.physics }, rootMotion } } to blend locomotion by speed, fade the air layer by grounded state, align feet on uneven ground and optionally drive movement from root motion. For planted feet add footIk: { ..., leftContact: { footDown, footUp, tilt }, rightContact: { ... } } with footDown and footUp clip event ids (or curves: [{ action, times, weights }]) and tilt = createFootTiltChain(game.graphics.clips, root, "LeftFoot"): the foot locks where it lands, releases smoothly when it lifts and tilts to the ground normal. Add feet: clips.createBoneSet(root, ["LeftFoot", "RightFoot"]) so the lock latches the animated foot bone instead of a guess. clips.readBonePoses(set, out, offset) writes 7 floats per bone (world x, y, z and rotation quaternion) for any bones, with no allocation.
|
|
329
|
+
Mixamo and mocap clips: const names = await game.graphics.loadClips(playerRoot, { url: "run.glb" }) bakes every clip in the file onto the player's rig once (humanoid retarget, matches live retargeting), so no hidden source rig per player; game.graphics.clips.bakeRetarget(target, sourceRoot, "Run") does the same from an already loaded model.
|
|
330
|
+
State machines: new AnimationGraph(game.graphics.clips, { params: [{ name: "speed", kind: "float" }, { name: "kick", kind: "trigger" }], states: [{ kind: "blend", name: "move", space, x: "speed" }, { kind: "clip", name: "shoot", action: kickAction, loop: "once", duration }], transitions: [{ from: "move", to: "shoot", conditions: [{ param: "kick", op: "trigger" }], seconds: 0.1 }, { from: "shoot", to: "move", exitTime: 0.9, seconds: 0.2 }], entry: "move" }); look up handles once (graph.param("speed")), then each frame graph.setFloat(speed, v) and graph.update(dt); onEnter, onExit and onClipEvent give events. Nothing allocates per frame.
|
|
331
|
+
Cleaner blends: clips.inertialize(from, to, seconds) cuts to the new clip and decays the pose difference (no foot sliding from a long crossfade); clips.setClipSyncMarkers(root, "Run", [{ time: 0.1, id: LEFT_DOWN }, { time: 0.45, id: RIGHT_DOWN }]) plus clips.setClipSyncMode(action, "markers") keeps feet in step across walk, jog and run of different lengths. Far crowds: clips.setAnimationLod(root, 4) re-poses that character every fourth frame (1 to 16); clip time, events and root motion stay exact.
|
|
332
|
+
Team kits: make the kit material once with tintMask (R shirt, G shorts, B socks), then game.graphics.setTint(playerRoot, { primary: 0xd81e2c, secondary: 0xffffff, tertiary: 0xd81e2c }) per player: one model and one material for every team. Faces and hair: kind "skin" (scatter, thickness), "eye" (irisRadius, pupil) and "hair" (melanin, redness); glTF materials named skin, eye or hair import as these automatically.
|
|
333
|
+
|
|
334
|
+
Building blocks for custom rigs: TouchControls, CharacterMotor, CharacterAnimator, FollowCamera, FirstPersonView and ChaseCamera (chasePreset(name)) are exported from @cimplify/sdk/game/kit. They keep state in typed arrays and allocate nothing per frame; keep it that way in your code (no new objects, arrays or closures in fixedUpdate and update).`
|
|
335
|
+
}, GRAPHICS = {
|
|
336
|
+
id: "graphics",
|
|
337
|
+
title: "Graphics: meshes, materials, instancing, lights, camera, environment and post",
|
|
338
|
+
summary: "createGeometry/createMaterial/mesh, instanced batches, BoxBatch, lights, camera, setEnvironment, post chains, models.",
|
|
339
|
+
body: `Primitives: boxGeometry(w, h, d), planeGeometry(w, h), sphereGeometry(radius, widthSegments, heightSegments), cylinderGeometry(...) from @cimplify/sdk/game, plus capsuleGeometry, coneGeometry, torusGeometry, roundedBoxGeometry, extrudeGeometry, latheGeometry, tubeGeometry and textGeometry.
|
|
340
|
+
|
|
341
|
+
const g = game.graphics;
|
|
342
|
+
const geometry = g.createGeometry(boxGeometry(2, 1, 4));
|
|
343
|
+
const material = g.createMaterial({ kind: "standard", color: 0xf5c400, roughness: 0.6, metalness: 0 });
|
|
344
|
+
const bus = game.world.spawn();
|
|
345
|
+
game.transforms.add(bus, 0, 0.5, 0);
|
|
346
|
+
g.mesh(bus, geometry, material);
|
|
347
|
+
|
|
348
|
+
Material kinds: "standard" (color, roughness, metalness, emissive, emissiveIntensity, map and other texture slots, flatShading), "physical", "toon", "matcap", "unlit", "sprite", "points", "lines", "shader". "physical" adds clearcoat, sheen, specular, transmission with ior, thickness and attenuationColor and attenuationDistance, iridescence with iridescenceIOR and iridescenceThicknessRange, and anisotropy, with their maps. The Cimplify engine refuses iridescenceMap, iridescenceThicknessMap, anisotropyMap and a dispersion above 0 with an unsupported_feature error instead of drawing the material without them; bake those effects into the base maps. Change at run time with setMaterialColor(material, rgb), setMaterialOpacity, setMaterialTexture(material, "map", texture).
|
|
349
|
+
|
|
350
|
+
Many copies of one shape: an instanced batch. g.instanced({ geometry, material, capacity, colors: true }) returns { node, capacity, matrices, colors }; write 16 floats per instance into matrices (mat4ComposeArrays), then g.setInstanceCount(batch, n) and g.markInstancesDirty(batch). The kit's BoxBatch (from @cimplify/sdk/game/kit) wraps this for coloured boxes: new BoxBatch(game, capacity); each frame batch.begin(); set batch.box[BOX_X], [BOX_Y], [BOX_Z], [BOX_SX], [BOX_SY], [BOX_SZ]; batch.add(0xrrggbb); batch.end().
|
|
351
|
+
|
|
352
|
+
Camera: g.camera(entity, fovDegrees, near, far) makes the entity's camera active. The kit helper addSceneCamera(game, x, y, z, fov, pitch) adds a camera, a directional sun and a hemisphere light. For a chase camera, move the camera entity in update() and aim it with a look-at rotation; CameraRig (kit) smooths follow with dead zones and look-ahead.
|
|
353
|
+
|
|
354
|
+
Lights: g.create(entity, { kind: "light", light: "directional" | "point" | "spot" | "ambient" | "hemisphere" | "rectArea" | "probe", color, intensity, castShadow, shadowMapSize, shadow: { cascades, maxFar, normalBias } }). Hemisphere lights take groundColor.
|
|
355
|
+
|
|
356
|
+
Environment: g.setEnvironment({ sky: { turbidity, rayleigh, clouds: { coverage, speed } } | false, background: { color } | { sky: true } | { environment: true }, environment: textureId, environmentIntensity, fog: { mode: "height", color, density, height, falloff }, toneMapping: "agx", exposure, shadows: true, volumetrics }). g.setTimeOfDay(hours, latitude?, dayOfYear?) moves the sun.
|
|
357
|
+
|
|
358
|
+
Post: g.setPostChain(g.createPostChain({ effects: [{ kind: "bloom", strength, radius, threshold }, { kind: "color_grade", exposure, contrast, saturation }, { kind: "vignette", offset, darkness }] })). Other effects: fxaa, smaa, taa, ao, ssr, dof, motion_blur, lut, grain, god_rays, lens_dirt, sharpen, chromatic, outline.
|
|
359
|
+
|
|
360
|
+
Models and textures: const model = await g.loadModel({ bytes }) (glTF/GLB) returns { id, nodeNames, clipNames, materials }; g.instantiate(entity, model.id); g.playClip(node, clip...) for animations. g.loadTexture({ bytes }) for KTX2/PNG. Bytes come from ctx.assets.take(path) (see the workshop topic). Fonts: g.loadFont(...) then g.setText(node, text).
|
|
361
|
+
|
|
362
|
+
Particles: g.createEmitter(...) and g.emit(...); the kit Particles and Spray helpers wrap common effects. Feel: ScreenShake, HitStop, Springs, ScreenFlash from the kit.`
|
|
363
|
+
}, PHYSICS = {
|
|
364
|
+
id: "physics",
|
|
365
|
+
title: "Physics: bodies, colliders, queries and characters",
|
|
366
|
+
summary: "addBody/addCollider shapes, forces and impulses, raycasts, contacts, character controller, and cloth (flags, curtains, capes).",
|
|
367
|
+
body: `Physics is deterministic and runs in the fixed step. Add the engine feature "physics" to [profile] engine_features.
|
|
368
|
+
|
|
369
|
+
game.physics.addBody(entity, { kind: "dynamic", linearDamping: 0.1, angularDamping: 0.5, canSleep: true, ccd: false });
|
|
370
|
+
game.physics.addCollider(entity, { shape: { kind: "cuboid", halfExtents: [1, 0.5, 2] }, friction: 0.8, restitution: 0.1, mass: 1200 });
|
|
371
|
+
|
|
372
|
+
Body kinds: "dynamic", "kinematic", "fixed". A collider on an entity without a body is static (ground, walls, kerbs). Shapes: ball (radius), cuboid (halfExtents), capsule (halfHeight, radius), cylinder, cone, trimesh (vertices, indices), convex_hull (points), heightfield (rows, columns, heights, scale). Collider options: offset, rotation, density or mass, friction, restitution, sensor (trigger only), events, membership and filter (collision groups).
|
|
373
|
+
|
|
374
|
+
Motion: applyImpulse(entity, v), applyTorqueImpulse, addForce, addTorque, applyImpulseAtPoint, setLinearVelocity, setAngularVelocity, linearVelocity(entity, outVec3), teleport(entity, position), wake(entity), setGravity(v).
|
|
375
|
+
|
|
376
|
+
Queries: raycast(rayQuery, hit), castShape(query, hit). Contacts each step: contactCount(), contactEntityA(i), contactEntityB(i), contactStarted(i). Joints: joint(a, b, desc), setJointMotor. Characters: createCharacterController(desc) then moveCharacter(controller, collider, desired, outMove).
|
|
377
|
+
|
|
378
|
+
Never move a dynamic body by setting its transform every frame; drive it with forces, impulses or velocities.
|
|
379
|
+
|
|
380
|
+
Cloth. game.cloth(shape, options) makes a sheet of particles held together by stretch and bend constraints (XPBD), stepped in the fixed step and identical on every device:
|
|
381
|
+
|
|
382
|
+
const flag = game.cloth({ grid: { columns: 24, rows: 16, spacing: 0.04, origin: { x: 0, y: 3, z: 0 }, down: { x: 0, y: -1, z: 0 }, pin: "top" } }, { geometry: flagMesh, damping: 0.4 });
|
|
383
|
+
game.cloths.setColliders([{ sphere: { center: { x: 0.5, y: 2, z: 0.2 }, radius: 0.3 } }, { capsule: { a, b, radius: 0.1 } }]);
|
|
384
|
+
|
|
385
|
+
A grid lays columns along across (default +x) and rows along down (default +z) from origin, and pins the top row ("top", the default), the two top corners ("corners") or nothing ("none"); pins adds particle indices of your own. { mesh: { positions, triangles, pins } } uses your own triangle mesh (positions x, y, z each, triangles as index triples). Options: mass per particle (0.01 kg), stretch and bend compliance (0 is stiff), tetherSlack, damping, gravity, substeps (1 to 64, default 8), thickness and friction (0 to 1) against colliders. With geometry, the engine writes the particle positions straight into that geometry's vertices each frame (vertex i is particle i, so build the mesh from the same grid) and recomputes its normals. sheet.setAnchors(xyz) moves the pinned particles smoothly over the next step (a flag pole, a cape on a running character), sheet.positions() reads the particles (x, y, z, inverse mass each) without copying, sheet.setOptions(options) changes the feel, and game.cloths.digest() checks a replay. Up to 32 sphere or capsule colliders. Cloth lives in the physics world, so rollback snapshots rewind it. It needs the native engine (feature cloth_1, added when your code calls cloth); elsewhere game.cloth throws unsupported_feature, and bad sizes, NaNs or out of range pins throw invalid_argument.`
|
|
386
|
+
}, VEHICLES = {
|
|
387
|
+
id: "vehicles",
|
|
388
|
+
title: "Vehicles: KitVehicle and presets (danfo, keke, okada, car)",
|
|
389
|
+
summary: "KitVehicle.spawn with vehiclePreset(name, 'arcade' | 'sim'), DriveControls, speed and wheels.",
|
|
390
|
+
body: `Raycast vehicles with tuned presets live in the kit:
|
|
391
|
+
|
|
392
|
+
import { KitVehicle, type DriveControls, vehiclePreset } from "@cimplify/sdk/game/kit";
|
|
393
|
+
|
|
394
|
+
const chassis = game.world.spawn();
|
|
395
|
+
game.transforms.add(chassis, x, 1.2, z);
|
|
396
|
+
const danfo = KitVehicle.spawn(game.physics, chassis, vehiclePreset("danfo", "arcade"));
|
|
397
|
+
const drive: DriveControls = { throttle: 0, brake: 0, steer: 0, handbrake: 0 };
|
|
398
|
+
|
|
399
|
+
fixedUpdate(game: Game): void {
|
|
400
|
+
drive.steer = game.input.value(this.steer);
|
|
401
|
+
drive.throttle = game.input.value(this.gas);
|
|
402
|
+
drive.brake = game.input.value(this.brake);
|
|
403
|
+
danfo.drive(drive, game.stepSeconds);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
Preset names: "danfo" (West African minibus), "keke" (tricycle), "okada" (motorbike), "car". Models: "arcade" (grip and stability assists, auto reverse when braking at a stop) or "sim". KitVehicle.spawn adds the dynamic chassis body and colliders itself, so do not add another body to that entity. Read danfo.vehicle.speed (m/s), danfo.speedKmh, danfo.spec, wheel poses with danfo.wheelPosition(i, out) and danfo.wheelRotation(...), wheelSpeed(i).
|
|
407
|
+
|
|
408
|
+
The vehicle needs ground: a static cuboid collider (for example halfExtents [600, 0.5, 600] centred half a metre below y = 0) or world-stream colliders. Put the visible model on a child entity (setParent) offset down to the wheel line so the body and art line up. Steering input is -1..1, throttle and brake 0..1.
|
|
409
|
+
|
|
410
|
+
For a player-driven vehicle with touch pedals, a steering stick and a chase camera in one call, use vehicleDriver(game, { preset: "danfo", position }) from the controls topic; it returns rig.vehicle (the KitVehicle) and rig.drive.
|
|
411
|
+
|
|
412
|
+
Lower level: game.physics.createVehicle(entity, VehicleDesc) and destroyVehicle(vehicle); VehicleSolver and the Vehicle class (gears, wheels) are exported from @cimplify/sdk/game.`
|
|
413
|
+
}, ENTITY_TABLES = {
|
|
414
|
+
id: "entity-tables",
|
|
415
|
+
title: "Entity tables: 100k entities, bulk ops, cursors and forEach",
|
|
416
|
+
summary: "game.table(schema) engine-owned SoA columns, bulk ops in Rust SIMD (math, select, reduce, sort, spatial), batched paths, raycasts and line of sight, inventories, damage and auras, cols and each for index access, one reused cursor.",
|
|
417
|
+
body: `For many similar things (bullets, boids, coins, crowds, particles with rules, units in a strategy game) use an entity table. Its columns live in engine memory and the game sees them as typed arrays over that memory, with no copies on the web, iOS or Android.
|
|
418
|
+
|
|
419
|
+
const SCHEMA = { pos: "vec3", vel: "vec3", hp: "f32", team: "u8", flags: "u32" } as const;
|
|
420
|
+
const swarm = game.table(SCHEMA, { capacity: 100_000, selections: 4, cell: 4 });
|
|
421
|
+
const [low, near, order] = swarm.selections;
|
|
422
|
+
for (let i = 0; i < 1000; i++) {
|
|
423
|
+
const e = swarm.at(swarm.spawn());
|
|
424
|
+
e.pos.x = game.random.range(-50, 50);
|
|
425
|
+
e.vel.z = 1;
|
|
426
|
+
e.hp = 100;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
Field kinds: "f32", "i32", "u32", "u8", "vec2", "vec3", "vec4" (f32 columns per axis), and fixed-size arrays such as "u32[8]" (one column per element, up to 32). Options: capacity (fixed, the table never grows), selections (index lists, up to 16), cell (spatial grid cell size), gridTable (power of two). spawn() appends a zeroed row; remove(row) swaps the last row into the hole and returns the row that moved, so update anything that pointed at it. count is the live row count.
|
|
430
|
+
|
|
431
|
+
Choose in this order, fastest first:
|
|
432
|
+
|
|
433
|
+
1. Bulk op. One call runs over every row (or a selection) in Rust with SIMD:
|
|
434
|
+
math: swarm.integrate("pos", "vel", dt), axpy(dst, a, b, s), add, sub, mul, min, max (vector fields work per axis), scale(dst, a, s), offset(dst, a, s), clamp(dst, a, lo, hi), abs, neg, lerp(dst, a, b, t), fill(field, value), copy(dst, src), length(dst, "vel"), distance(dst, "pos", x, y, z), distanceSquared.
|
|
435
|
+
select: swarm.select(low, "hp", "<", 25) fills a selection and returns its length; comparisons "<", "<=", ">", ">=", "==", "!=", "range" (value <= v < high), "bits" (v & value != 0 on integer fields); pass from to filter a selection again: swarm.select(low, "team", "==", 1, 0, low).
|
|
436
|
+
reduce: sum, mean, minimum, maximum, argmin, argmax (row or -1), countNonZero, each optionally over a selection.
|
|
437
|
+
sort: swarm.sort(order, "hp", descending, from?) writes rows into a selection in key order (stable radix sort).
|
|
438
|
+
spatial: swarm.buildGrid("pos") once per step, then swarm.near(near, x, y, z, radius) fills a selection and swarm.nearest(near, x, y, z, radius, excludeRow) returns the closest row or -1.
|
|
439
|
+
Every op takes an optional selection last, so work can be limited to the rows a select or near produced.
|
|
440
|
+
|
|
441
|
+
Game-shaped bulk ops. Build each helper once (in enter()), then call it every step; results are bit-identical on every device, in the JS fallback and on the tick server.
|
|
442
|
+
Inventories: fixed-size slot arrays are fields like items: "u32[8]" and counts: "u32[8]" (item 0 means empty). const bag = units.inventory("items", "counts", 99) (99 is the stack limit, 0 for none). bag.give(item, amount, rows?, overflow?) stacks onto matching slots and then fills empty ones, partially when full, and lists rows that could not take everything; item and amount may be numbers or field names (per-row loot), and an amount field is left holding what did not fit. bag.take(item, amount, rows?, short?) is all or nothing. bag.transfer(fromRows, toRows, item, amount, failed?) moves between paired rows only when the giver has enough and the receiver has room. bag.move(fromSlot, toSlot, rows?, toRows?, failed?) moves, stacks or swaps one slot; bag.split(slot, amount, rows?, toRows?, failed?) moves part of a stack into the first empty slot; bag.count(item, intoField?, atLeast = 1, rows?, has?) writes totals and selects rows holding at least that many. The cursor reads slots with e.items.get(k) and e.items.set(k, v); cols.items[k] is slot k's column.
|
|
443
|
+
Damage and auras: units.buildGrid("pos") once per step, then const blast = units.areaEffect("hp", { position: "pos", radius: 4 or "blastRadius", amount: 30 or "power", falloff: "constant" | "linear" | "quadratic" | "smooth", multiplier: "armor", min: 0, max: 100, from: projectiles }) and blast.apply(sources?, hits?) subtracts amount times the falloff from every row within each source's radius (a negative amount heals), clamps to min and max, lists each row hit once in hits, and returns the number of source-target pairs. Sources come from the same table (a row never hits itself unless skipSelf: false) or from another table via from. Radii are capped at 16 grid cells; raise cell for bigger blasts.
|
|
444
|
+
Raycasts and line of sight against game.physics: const shots = units.rays(game.physics, { origin: "pos", direction: "aim", maxDistance: 50 or "range", distance: "hitDistance", normal: "hitNormal", entity: "hitEntity", collider: "hitCollider", exclude: "ownCollider", hit: "hitMask" }) then shots.cast(rows?, hits?) fills those fields (misses read the max distance and entity 0xffffffff) and lists rows that hit. const sight = units.lineOfSight(game.physics, { origin: "eye", target: "enemyPos", hit: "canSee" }); sight.cast(rows?, visible?) lists rows whose segment is clear.
|
|
445
|
+
Paths on game.nav: const routes = units.paths(game.nav, { start: "pos", goal: "goal", direction: "step", corner: "next", status: "pathState", length: "pathLength", corners: "pathCorners", maxCorners: 16, budget: 64 }); call routes.run(rows?) every step. Each call answers at most budget rows and resumes where it stopped, so 1000 agents repath over a few frames; routes.pending is true until a sweep completes. status is 1 complete, 2 partial (closest reachable point), 3 failed; direction is the unit vector toward the next corner, so steering is units.axpy("vel", "vel", "step", accel * dt).
|
|
446
|
+
Flow fields when many agents share one goal (a crowd heading to the stadium, a herd to water, an army to a rally point): const field = units.flowField({ origin: [0, 0], cell: 1, width: 1000, height: 1000 }); field.rasterize(game.nav) once per navmesh (or field.fill(1) and field.setCost(x, z, 0) for blocked cells, multipliers above 1 for slow ground), then field.build(goal) whenever the goal moves (one integer Dijkstra over the grid, about 13 ms for a 1 km city at 1 m cells) and const flow = units.flow(field, { position: "pos", direction: "step", distance: "toGoal", status: "flowState" }); flow.run(rows?) every step writes each agent's unit direction toward the goal (smoothed across cells; smooth: false for raw 8-way) in about 12 ns per agent, so 100k agents steer in about a millisecond. Batched picks against the rendered scene (triangle accurate, unlike physics colliders): const aim = units.picks(game.graphics, { origin: "eye", direction: "look", maxDistance: 100, distance: "hitDistance", point: "hitPoint", node: "hitNode", hit: "hitMask" }) (or target: "enemyPos" for line of sight), then aim.cast(rows?, hits?); the scene is gathered once per call instead of once per ray. status is 1 when the agent's cell reaches the goal and 3 outside the field or cut off. Prefer paths for a few agents with different goals, flowField for many agents with the same goal.
|
|
447
|
+
2. Kernel: an ordinary TypeScript function in a bounded subset that the build compiles to bytecode the engine runs natively over the table (see the kernels topic).
|
|
448
|
+
3. Index access over the columns, for per-row logic that is neither. swarm.cols holds every column as a typed array (vector fields as { x, y, z }), and swarm.each(visit, selection?) calls visit(row, cols) for each live row:
|
|
449
|
+
|
|
450
|
+
const push = (i: number, c: TableColumns<typeof SCHEMA>): void => { if (c.hp[i] < 10) c.vel.x[i] = -c.vel.x[i]; };
|
|
451
|
+
swarm.each(push, low);
|
|
452
|
+
|
|
453
|
+
Or hoist the arrays and loop yourself: const px = swarm.cols.pos.x, vx = swarm.cols.vel.x; for (let i = 0; i < swarm.count; i++) px[i] += vx[i] * dt;
|
|
454
|
+
4. forEach, map and reduce with the cursor, the friendliest form, for small tables and UI-sized work:
|
|
455
|
+
|
|
456
|
+
const steer = (e: EntityCursor<typeof SCHEMA>): void => { if (e.hp < 10) e.vel.x = -e.vel.x; };
|
|
457
|
+
swarm.forEach(steer, low);
|
|
458
|
+
const heal = (e: EntityCursor<typeof SCHEMA>, out: Float64Array): void => { out[0] = e.hp + 1; };
|
|
459
|
+
const addHp = (sum: Float64Array, e: EntityCursor<typeof SCHEMA>): void => { sum[0] += e.hp; };
|
|
460
|
+
swarm.map("hp", heal);
|
|
461
|
+
const total = swarm.reduce(addHp, 0);
|
|
462
|
+
|
|
463
|
+
map and reduce callbacks write their number into a one-slot Float64Array (map's out[0] starts as the field's current value; reduce's sum[0] starts as the initial value) instead of returning it, so no JS engine boxes a number per row. You can also loop yourself: for (let row = 0; row < swarm.count; row++) { const e = swarm.at(row); ... } runs as fast as forEach.
|
|
464
|
+
|
|
465
|
+
On iOS, games run in JavaScriptCore without a JIT, where every cursor field is a function call. Measured on an iPhone at 100k rows, integrating pos += vel * dt took, in ms per frame: engine bulk op 0.029, raw typed arrays 13, cursor forEach 48, cursor at() 51 (plain JS objects 6.5). So on iOS per-row JS is for small tables and UI-sized work: above a few thousand rows use a bulk op or a kernel (the build turns simple forEach, map and reduce callbacks into kernels for you), and where you must touch rows in JS use cols or each, never the cursor.
|
|
466
|
+
|
|
467
|
+
The cursor is one reused object (swarm.at(row) returns it pointed at that row; e.row reads the row). It reads and writes the columns directly: e.pos.x is a typed array read, never an allocation. Never keep a cursor between calls or store it in an array; keep row numbers instead. Define visitor functions once (in enter() or as module constants), not inline in update, so the frame allocates nothing.
|
|
468
|
+
|
|
469
|
+
Raw access when you want it: swarm.f32("pos", 0) is the x column, swarm.values("team") any column, selection.rows and selection.length the index list. Tables are versioned: after swarm.dispose() (or when the game ends) every bulk op and iteration helper throws, a kept cursor reads undefined and ignores writes, and swarm.valid turns false; nothing ever reads or writes freed memory. Raw column arrays you took with f32() or values() are yours to drop. The game disposes its tables for you on game.dispose().`
|
|
470
|
+
}, KERNELS = {
|
|
471
|
+
id: "kernels",
|
|
472
|
+
title: "Kernels: per-entity TypeScript compiled to native SIMD",
|
|
473
|
+
summary: "kernel((e: Each<T>, dt: f32) => ...) compiled at build time to typed SoA bytecode; world.run(k, table, ...args); vectors, fixed arrays, structs, state machines, behaviour trees, neighbour loops, events, engine calls (raycast, lineOfSight, groundHeight, navNext, flow), systems, the rule library (steering, particles), timers, change detection, graph operations, curves, gradients, lookup tables, samplers, grid stencils (cell), matrices, whole-table results (rowSum, rowMin, rowMax, rowCount, rowArgMin, rowArgMax, rowPrefixSum, histogram, scatterAdd, compactRows into kernelArray), row recurrences (rowRecur, rowFeedback, rowPhase, rowScan) and spatial queries (nearest, within, inCone, inBox, alongRay, filters, break), grid transforms (FFT, convolution, summed-area tables, distance transforms, multigrid Poisson), and trained gradient-boosted tree models (model(file) from XGBoost, LightGBM or ONNX, predict over columns).",
|
|
474
|
+
body: `Order of preference for work over many rows: a bulk op first (one call, see entity-tables), a kernel second, cols/each third, cursor forEach last. A kernel is an ordinary TypeScript function; cimplify build compiles it to bytecode that the engine runs over the table's columns in Rust with SIMD (and on the tick server as native code). Results are bit-identical on iOS, Android, web and tick.
|
|
475
|
+
|
|
476
|
+
import { kernel, within, add, scale, len, normalize, sub, type Each, type f32, type vec2 } from "@cimplify/sdk/game";
|
|
477
|
+
interface Agent { pos: vec2; vel: vec2; target: vec2; speed: f32 }
|
|
478
|
+
const steer = kernel((e: Each<Agent>, dt: f32, maxSpeed: f32) => {
|
|
479
|
+
const to = sub(e.target, e.pos);
|
|
480
|
+
const d = len(to);
|
|
481
|
+
if (d < 0.001) return;
|
|
482
|
+
e.vel = add(e.vel, scale(sub(scale(normalize(to), Math.min(maxSpeed, d * 2)), e.vel), 4 * dt));
|
|
483
|
+
e.pos = add(e.pos, scale(e.vel, dt));
|
|
484
|
+
e.speed = len(e.vel);
|
|
485
|
+
});
|
|
486
|
+
const agents = game.table({ pos: "vec2", vel: "vec2", target: "vec2", speed: "f32" }, { capacity: 100_000 });
|
|
487
|
+
game.world.run(steer, agents, dt, 6); // every row
|
|
488
|
+
game.world.run(steer, within(agents, near), dt, 6); // only a selection
|
|
489
|
+
|
|
490
|
+
The subset: numbers (f32 by default; annotate i32 or u32 for integers), booleans, vec2/vec3/vec4/quat values and their functions (add, sub, mul, div, scale, negate, dot, cross, len, lenSq, dist, distSq, normalize, lerp, clamp, smoothstep, qmul, qconj, qaxis, qrotate), Math functions (abs, sqrt, floor, ceil, round, trunc, sign, sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, pow, min, max, hypot), noise2/noise3, hash32 and rand01(seed, stream) for deterministic randomness, if/else, ternaries, switch, early return, for loops with a constant or argument bound, for..of over fixed arrays, helper functions and other kernels (inlined), outer const numbers, vectors and structs. Refused with a build error that names the line and the fix: allocation (new, array growth), strings, any, closures, captured let variables, while loops, recursion, Math.random, host I/O (console, ctx, fetch).
|
|
491
|
+
|
|
492
|
+
Automatic lowering: you do not have to write kernel() for every hot loop. The build finds plain for, for-of and while loops over typed arrays (Float32Array, Float64Array, Int32Array, Uint32Array, Uint8Array), table columns (table.f32("x")) and ECS columns (store.fields.x, or query rows via const r = query.rows[0][i]) whose bodies are numeric, and runs the hot ones as kernels with exactly the results JavaScript would give: arithmetic is done in f64 like JavaScript, stores round to the array type (Float32Array rounds like Math.fround, Int32Array wraps like ToInt32), NaN, -0 and infinities behave as in JavaScript, and f32 or integer arithmetic is used only where it provably gives the same bits. Each lowered loop is run at build time against the original on edge values (NaN, signed zeros, infinities, subnormals, ulp neighbours) and random inputs on every engine path, and kept in JavaScript if any bit differs. Hotness comes from cimplify capture and test runs (.cimplify/loops.json), or from frame-update loops over entity-sized arrays before any capture. A hot loop that cannot be lowered gets a kernel.lower_missed build warning naming the line, the fix and an estimated speedup ("allocates an object at line 42; hoist it out of the loop"); cimplify inspect lists hot loops too. Keep bodies to arithmetic, comparisons, ternaries, Math.min/max/abs/floor/ceil/round/trunc/sign/sqrt/fround, element reads and writes at [i] or [i + k], local numbers and accumulators (sum += x, n++, best = Math.max(best, x)). Calls, allocation, strings, break and reading an element another iteration writes keep a loop in JavaScript. At run time the lowered loop falls back to the original JavaScript when the engine is missing, the arrays overlap or are too short, and it times both and keeps JavaScript on devices where JavaScript is faster, so lowering never changes results. Opt out with no_lower = ["src/sim.ts", "src/sim.ts#integrate"] under [kernels] in interactive.toml. Math.sin, cos, tan, asin, acos, atan, atan2, exp, log, log2 and pow have no last-bit guarantee across JavaScript engines, so they are lowered only with exact_math = true under [kernels]; the loop then uses the engine's exact maths in the kernel and in its JavaScript fallback, so every device computes the same bits (more deterministic than plain Math).
|
|
493
|
+
|
|
494
|
+
Fields: field types come from the entity interface. Structs and fixed arrays become columns: interface Mob { stats: { hp: f32; armor: f32 }; inventory: FixedArray<u32, 8> } reads columns "stats.hp" and "inventory.3"; create the table with fields({ stats: { hp: "f32", armor: "f32" }, inventory: ["u32", 8] }).
|
|
495
|
+
|
|
496
|
+
64-bit values (for lockstep rules that must equal plain JavaScript, money, big counters and ids): annotate f64 (a JavaScript number in double precision) and u64 (a bigint) on fields, locals and arguments; table fields are "f64" and "u64" (game.table({ balance: "f64", energy: "u64" }, ...); the cursor reads a number or a bigint). An unannotated number is still f32, so write const total: f64 = e.balance * rate; literals and f32/i32/u32 values widen to f64 exactly. f64 supports + - * / % **, comparisons and Math.abs, sqrt, floor, ceil, round, trunc, sign, sin, cos, tan, asin, acos, atan, atan2, exp, log, log2, pow, min, max and fround; + - * / sqrt, comparisons and conversions equal JavaScript bit for bit everywhere, and the transcendentals give the same bits on every device (plain JavaScript may differ in the last bit between browsers; the kernel never does). u64 supports + - * / % & | ^ ~ << >> and comparisons with bigint literals (1n); results wrap modulo 2^64 and shift amounts are taken modulo 64 (plain JavaScript bigints do not wrap until they are stored in a u64 field). Convert explicitly: f64(x), u64(x) (truncates and wraps; NaN gives 0n), toU32(x) (the low word of a u64, or ToUint32 of an f64), Math.fround(x) to f32; mixing a bigint with a number is refused with that fix. Bit operations on u32 or u64: popcnt, clz, ctz, rotl, rotr, bitExtract(x, offset, count), bitInsert(base, value, offset, count), Math.clz32, morton2/morton3 (16 and 10 bits per axis into a u32), morton2Wide/morton3Wide (32 and 21 bits into a u64) and unmorton2/unmorton3 returning { x, y } or { x, y, z }. Kernels that use f64, u64 division, shifts or bit operations need kernel_isa_4 (the build adds it) and give the same bits on every device.
|
|
497
|
+
|
|
498
|
+
State machines: when the kernel body is one switch on an integer field (switch (e.state) { case 0: ...; break; ... }), rows are grouped by state and each state runs as its own SIMD program. Behaviour trees: behaviour(selector(sequence(condition(e => e.hunger > 5), action((e, dt) => { ...; return Status.running; })), action(wander))) compiles the whole tree with masks; run it with world.run like a kernel.
|
|
499
|
+
|
|
500
|
+
Neighbours: for (const n of e.neighbours(radius)) walks every other row within radius on the table's position field (position or pos, vec2 or vec3; or name it: e.neighbours(r, "home")). Neighbours are read-only and see the values from the start of the run; inside the loop add into outer variables with +=, -=, Math.min or Math.max (sum += n.vel.x), then write the entity after the loop. The radius is a constant or a kernel argument.
|
|
501
|
+
|
|
502
|
+
Events: const Died = events("died", { who: "u32", hp: "f32" }); inside a kernel Died.emit({ who: e.row, hp: e.hp }); once per frame Died.drain((event) => { ... event.row, event.who ... }) visits them in row order without allocating.
|
|
503
|
+
|
|
504
|
+
Engine calls: inside a kernel, raycast(origin, dir, maxDist) returns { dist, normal, entity, hit }, lineOfSight(from, to) returns a boolean, groundHeight(x, z) returns the terrain height (NaN off the terrain), navNext(from, goal) returns { dir, status } toward the next path corner, and flow(pos) returns { dir, distance, status } from the attached flow field. Write them like ordinary calls (const h = raycast(e.pos, e.aim, 40); if (h.hit) e.target = h.entity;). The build splits the kernel at each call: the engine runs the matching batched query once for every row that reaches the call (rows that returned early or sit in a false branch issue no query), then the kernel resumes with the results, so 10k raycasts cost one engine call, not 10k. Physics, nav and graphics are attached for you; add terrain or a flow field with game.world.kernelServices({ physics: game.physics, nav: game.nav, graphics: game.graphics, terrain, flow: field }). Calls must be outside loops, outside neighbour loops and outside state-machine kernels (the build says so with the fix). Packages that use engine calls need the kernel_isa_2 engine feature, which the build adds.
|
|
505
|
+
|
|
506
|
+
Engine writes, more queries, spawn and despawn (kernel_isa_3, added by the build when used): applyImpulse(e.body, v), applyForce(e.body, f) and setVelocity(e.body, v) push physics bodies (store the body id from game.physics.bodyId(entity) in a u32 field); overlap(center, radius) returns { count, entity } and sweep(origin, dir, radius, maxDist, exclude?) returns { dist, normal, entity, hit } (pass e.body as exclude when sweeping from a body, or the sweep stops inside it at distance 0); setClipWeight(action, w), playClip(root, clipIndex, loop, fade, speed) returning the action id (game.graphics.clipIndex(root, "run") gives the index) and setIkTarget(chain, pos, weight) drive animation; setInstance(node, index, pos, rot?, scale?) and setInstanceColour(node, index, rgba) move instanced meshes; setAnchor(node, pos) moves a world-UI anchor; playSound(sound, bus, pos, gain) fires a positional one-shot; pick(origin, dir, maxDist) returns { dist, node, instance, hit } against the render scene; voxelAt(pos) returns { material, solid } and setVoxel(pos, material) digs (0) or fills; chemistryAt(x, y, z) returns { species, kelvin, amount } for a chemistry grid cell. Each call becomes one batched engine operation per kernel stage, in row order, with no allocation. Attach ui, audio, voxels or chemistry with game.world.kernelServices({ ..., ui, audio, voxels, chemistry }). spawn({ pos: e.pos, vel: v, ttl: 2 }) adds a row and despawn() removes the current one; both apply after the run (kills compact the table keeping row order, then spawns append in row order, unset fields start at zero), so new rows are seen by the next run. Spawn into another table with game.world.run(fire, spawnInto(ships, bullets), dt). When the spawn table is full the extra rows are dropped and the kernel.spawnOverflow event ({ dropped, capacity, table }) says how many; drain it with spawnOverflow.drain((e) => ...). game.world.kernelSpawns holds the last run's { spawned, dropped, killed }.
|
|
507
|
+
|
|
508
|
+
Systems: const systems = game.world.kernelSystems(); systems.add({ name: "steer", kernel: steer, set: agents, reads: ["target"], writes: ["pos", "vel", "speed"], args }); systems.run() each step. The declared reads and writes are checked against the compiled kernel, and systems that touch the same fields are ordered into stages.
|
|
509
|
+
|
|
510
|
+
Rule library (ready-made kernels, already compiled, bit-identical to the engine's own passes): steerAgents runs the engine's steering rules (seek with arrive, direction, face, separation from neighbours, speed and acceleration limits) over a table made with STEER_AGENT_FIELDS (mode is STEER_SEEK, STEER_DIRECTION, STEER_FACE or STEER_STOP; world.run(steerAgents, agents, dt, maxSpeed, maxAccel, separationRadius, separationWeight, 1)); particleRules steps particles (gravity, drag, curl noise, plane bounce, age, retire with despawn) over PARTICLE_FIELDS; particleColours writes a tint and a size over life from the LIFE_TINT gradient and LIFE_SIZE curve (copy it with your own curve consts to restyle); regenerating is the timer example below. An unmodified rule kernel (steerAgents, particleMotions, particleColours, regenerating, or your own kernel whose body is exactly the rule call) is recognised by its compiled fingerprint and runs as the engine's native code with the same results; nativeTwinOf(kernel) names the twin, and any change to the kernel makes it run as your bytecode. The rules are kernel standard library functions, so your own kernels call them and add to them: kernel((e: Each<Unit>, dt: f32) => { steerAgent(e, dt, 3, 8, 1.2, 1.5, 1); if (e.hp <= 0) despawn(); }) needs only the SteerAgent fields on Unit; particleMotion(e, dt, gravity, drag, plane, bounce, collide, noise) is particleRules without the retire, and particleLook(e) the colour and size pass. Their building blocks work on plain values anywhere in a kernel, neighbour loops included: arriveVelocity, separationPush (push = add(push, separationPush(offset, radius)) inside for (const o of e.neighbours(r))), limitLength2, steerAccelerate, headingFrom, curlForce, particleVelocity and planeBounce.
|
|
511
|
+
|
|
512
|
+
Timers and staggered updates: give the table a u32 wake field and let the kernel write e.wake = tick + period (or WAKE_NEVER to sleep until something wakes it). A KernelSchedule keeps the rows in a timer wheel, so each tick costs only the rows that are due: const schedule = new KernelSchedule(table.capacity); schedule.rebuild(table.values("wake"), table.count, tick) once, then every tick schedule.collect(tick, wake); game.world.run(k, within(table, schedule.into(table.selections[0])), tick, ...); schedule.settle(wake). Sleeping rows are skipped entirely (they still count as neighbours), the due rows run in row order, and after the host writes a wake value itself it calls schedule.set(row, wake). Call rebuild again after rows are added or removed. kernelToolkit.stagger(tick, k, ids, count, out) lists every kth row (by row or by an id column) so a heavy kernel updates a different slice each tick. Short cooldowns that every row checks each tick need no schedule: keep an f32 field and write e.cooldown = Math.max(e.cooldown - dt, 0); if (e.cooldown === 0) { ...; e.cooldown = period; } inside the kernel.
|
|
513
|
+
|
|
514
|
+
Change detection: const watch = new KernelWatch(2, capacity); each tick watch.changed([table.values("hp"), table.values("armor")], table.count) lists the rows whose watched columns changed since the last commit (whoever wrote them: kernels, bulk ops or your code), watch.mark(row) adds a row by hand, run the kernel within watch.into(selection), then watch.commit(sameColumns, count). The first call, and any call after the row count changes, lists every row.
|
|
515
|
+
|
|
516
|
+
Graph operations (kernelToolkit, integer results identical in the engine and in JavaScript, no allocation after setup): componentsGrid labels 4- or 6-connected regions of equal cell value (each region is named by its lowest cell index, so the labels never depend on scan order), componentsEdges does the same for a node and edge list (power networks, conveyor lines), bfsGrid gives unit-step distances from source cells over passable cells and floodFill marks what it reaches, nearestLabelGrid gives every reached cell the owner of its nearest source (ties go to the smaller owner: territory), Csr plus bfsGraph and propagateGraph run distances and label propagation (each node takes the most common label around it, ties to the smaller label) over edge lists, and propagateGrid does label propagation on grids. Build the scratch arrays (parent, labels, queue, dist) once and reuse them. Inside a kernel the same operations run over the table itself, with each row a grid cell in row-major order: const c = gridComponents(e.material, grid) gives c.label and c.count, const r = gridReach(e.label, e.isBase !== 0, grid) gives r.dist, r.owner and r.reached (a label of 0xFFFFFFFF is a wall), gridPropagate(e.label, grid, rounds) spreads labels, and edgeComponents(labels, e.from, e.to) labels the nodes of an edge table into a kernelArray. They see every row, so they sit outside loops and neighbour loops, and run only in built kernels.
|
|
517
|
+
|
|
518
|
+
Spatial queries (kernel_isa_4, added by the build when used) walk other rows nearest first, with ties going to the lower row, so results never depend on the platform: for (const n of e.nearest(3, 20, { cell: 4 })) visits the 3 closest rows within 20; e.within(reach, opts) visits every row within reach, closest first; e.inCone(axis, cosHalfAngle, reach, opts) keeps rows inside a cone around axis (normalize it); e.inBox(forward, vec3(halfLength, halfWidth, halfHeight), opts) keeps rows inside a box turned to forward (Y is up in 3D, XY is the plane in 2D); e.alongRay(dir, length, radius, opts) keeps rows within radius of the segment, in order along it. Options: limit (the first k), where: { field: "team", equals: 2 } (or any: bits, none: bits, mask with equals) on an i32 or u32 field, field (the position field), and cell, the grid cell size: pass about the typical distance between neighbours (it defaults to the reach, and to the neighbours(radius) of the same kernel); e.nearest(k) with no reach needs a cell. Inside the loop use n.field, n.row, += and Math.min/Math.max like neighbours, plus continue and break: break ends that row's walk after the current neighbour (find the first visible enemy, stop after enough mass). The reach can exceed the cell size: the engine searches outward ring by ring and stops once the nearest k are settled.
|
|
519
|
+
|
|
520
|
+
Grid transforms (kernelTransforms; identical bits in JavaScript and in the engine, no allocation after construction): new Fft2(w, h) (powers of two up to 65536) runs forward(re, im), inverse(re, im) and forwardReal(input, re, im) on Float32Array planes (row-major); new Convolver(w, h, kernel) blurs or blooms with convolve(image, out) or two channels at once with convolvePair(a, b, outA, outB) (the kernel image is wrapped around the origin). new SummedAreaTable(w, h).build(values, table) makes a 2D prefix sum (the same blocked summation as the kernel scan) and boxSum(table, w, h, x0, y0, x1, y1) sums any rectangle in four reads; inside a kernel, bind the table with bindSampler("sat", table) and call satBox(sat, x0, y0, x1, y1) with const sat = sampler("sat", { width, height, channels: 1, x0: 0, y0: 0, scale: 1, cell: 1 }). new DistanceTransform(w, h, d?).fromMask(mask, out) gives exact squared distances to the nearest set cell (nearest holds that cell's index; signed(mask, scratch, out) gives a signed distance field for text, icons, outlines and glows); jumpFlood(w, h, mask, a, b, out) is the approximate jump-flooding version the engine also runs on the GPU. new Multigrid(w, h, d, spacing, "dirichlet" | "neumann").solve(u, f, cycles) solves the Poisson equation (pressure projection, diffusion, smooth terrain sculpting, seamless image blending) in a few V-cycles and returns the largest residual. Build these once, outside fixedUpdate, and reuse them every frame.
|
|
521
|
+
|
|
522
|
+
Curves, tables, samplers, stencils and matrices (kernel_isa_3, added by the build when used). Declare the data as an outer const of literal numbers; the build bakes it into the kernel, so a value that is not a compile-time number is refused with the fix. curve({ kind: CurveKind.hermite, keys: [0, 0.5, 1], values: [0, 2, 1], tangents: [0, 1, -1] }, t) evaluates a keyed curve (CurveKind.step, linear, hermite with tangents, bezier with outs and ins handles; up to 256 keys and 4 channels, values key-major); gradient(ramp, t) returns a vec4 colour from a 4-channel curve; lut({ x0, scale, values, linear?, wrap? }, x) and lut2({ nx, ny, x0, y0, sx, sy, values, linear?, wrap? }, x, y) read 1D and 2D tables (nearest, or linear and bilinear; clamped unless wrap). Engine-owned buffers: const terrain = sampler("terrain", { width, height, channels, x0, y0, scale, cell }) at the top level, bindSampler("terrain", heights) once on the host, then sample(terrain, x, z, channel?, nearest?) reads it bilinearly and heightNormal(terrain, x, z) gives the surface normal. Grid stencils: on a table laid out as a W x H (x D) grid in row order, cell(e.temp, dx, dy, dz, grid, wrap?) reads the field at a fixed offset from the row (clamped at the edges, or wrapped), from a snapshot taken at the start of the run, so diffusion can read and write the same field: e.temp = e.temp + k * (cell(e.temp, -1, 0, 0, grid) + cell(e.temp, 1, 0, 0, grid) - 2 * e.temp) with grid: GridSize passed as [W, H, D]. Matrices: mat3 and mat4 are column-major fixed arrays (arguments or fields); m3mul, m4mul, m3vec, m4point, m4dir, m3transpose, m4transpose, m3inverse and m4inverseAffine give the same bits in kernels and plain code. The same functions run outside kernels, except cell. A 256 x 256 heat-diffusion stencil with cell runs within 1.5x of hand-written Rust; a neighbours(radius) walk over the same grid is about 40x slower, so use cell for grids.
|
|
523
|
+
|
|
524
|
+
Whole-table results (kernel_isa_3, added by the build when used): rowSum(x), rowMin(x), rowMax(x) and rowCount(condition?) combine every row that reaches the call (rows that returned early or sit in a false branch do not count) and give every row the same answer; rowArgMin(x) and rowArgMax(x) give the row number of the smallest or largest value (the first such row on ties, NaN never wins, 0xffffffff when no row reached the call); rowPrefixSum(x, exclusive?) gives each row the running total of the rows before it (true) or up to and including it. Counts and sums into arrays: declare const hits = kernelArray("hits", { kind: "u32", length: 16 }) at the top level (kind "f32", "i32" or "u32"; the length is a literal), then histogram(hits, e.team) adds 1 per row to its bin, scatterAdd(heat, e.cell, e.energy) adds a value into a cell (an f32 array takes f32 values) and const n = compactRows(picked) writes the reaching rows' numbers into a u32 array and returns how many. Bins or cells past the length are skipped. Arrays add to what they already hold, so clear them with hits.data.fill(0) before a run that should start fresh; read the results from hits.data after the run. Order is fixed: rows contribute in visit order and float totals are added in fixed blocks of 256 rows, so the result is the same on every device, thread count and back-end. Each of these ends a stage, like an engine call: write several in a row (const total = rowSum(e.hp); const worst = rowArgMin(e.hp);) and they share one pass. They must sit outside loops, neighbour loops and state-machine kernels (the build says so with the fix). Ordering and grouping (kernel_isa_4): const n = sortRows(order, e.score, descending?) writes the reaching rows' numbers into a u32 kernel array in key order (equal keys keep row order) and returns how many; topRows(best, e.score, k, smallest?) keeps only the k largest (or smallest) and returns how many it kept; const g = groupRows(e.team) gives g.group, the row's group number (0 for the smallest key, then 1, 2, ...), and g.groups, how many there are, so scatterAdd(totals, g.group, e.gold) and histogram(sizes, g.group) give per-group sums and counts; const d = dedupRows(e.id) gives d.first (true on the first row of each distinct key) and d.distinct. Keys are numbers (u32, i32 or f32; -0 sorts before 0, NaN after every number). Kernels that use them give the same results on every device. Row recurrences (kernel_scan_1, added by the build when used) carry a value from row to row in visit order: rowRecur(a, b, exclusive?) gives y = a * y + b (f32, or wrapping for u32 and i32), rowFeedback(b, a1, a2) gives y = (b - a1 * y1) - a2 * y2 (the feedback half of a biquad), rowPhase(increment, period, exclusive?) accumulates a wrapped phase in f64 and gives it as f32 (rowPhase64 gives the f64), and rowScan(x, "add" | "mul" | "min" | "max" | "and" | "or" | "xor", exclusive?) is a running total or extreme. exclusive true gives the value before this row's step. Each f32 step rounds once per operation, so a recurrence gives the same bits as the same loop in plain JavaScript with Math.fround, on every back-end. They end a stage like rowSum and must sit outside loops.
|
|
525
|
+
|
|
526
|
+
Joins, lists, command buffers, lookups and row sampling (kernel_isa_4, added by the build when used). Other tables: take them as Rows<T> arguments and pass the tables to world.run. units.get(ref) finds the row whose id field (u32) equals the reference (store each row's entity id in id; a reference whose generation no longer matches reads as missing), units.lookup(key) finds the row whose key field equals the key (an item table keyed by symbol("sword"), a build-time u32), units.at(index) reads a row by number (enum-keyed tables) and units.count is the row count. A joined row reads like the entity (t.pos, t.stats.armor), t.exists tells whether it was found, and a missing row reads zeros. Reads see the values from the start of the run, also when the other table is the kernel's own table, so the order rows run in never matters. Writes through a join are commands: t.hp -= dmg, t.hp *= 0.5, t.hits++, t.flags |= 4, t.shield = Math.max(t.shield, v) and t.owner = e.id are collected during the run and applied after it, sorted by target row, then source row, then their order in the kernel, each step one rounded operation, so many rows can damage one target and the result is the same on every device and thread count. Lists: a field typed List<u32, 16> (declare it in the table with fields({ bag: list("u32", 16) })) holds up to 16 elements per row in a pool sized when the table binds; the row keeps only an offset and a length, so removing rows never moves list data. Use bag.push(x) (false and counted when full), bag.pop(), bag.get(i), bag.set(i, x), bag[i], bag.length, bag.indexOf(x), bag.includes(x), bag.removeAt(i), bag.swapRemove(i), bag.clear() and for (const x of bag); another row's list only takes push, applied after the run like a command. Queues between kernels: const hits = queue("hits", { target: "u32", amount: "f32" }, hitsTable) at the top level, hits.push({ target: t.row, amount }) in one kernel, world.run(resolve, hitsTable) in the next (the items arrive in row order), then hits.clear(). Row sampling: pickRows(n, condition, seed, tick, weight?) keeps the n rows with the smallest counter-based random key (weighted when a weight is given), the same on every host; read them with game.world.kernelData(k).picks[0]. game.world.kernelData(k) also reports list overflow counters and command counts (sent, applied, dropped, overflow). Examples:
|
|
527
|
+
interface Unit { id: u32; pos: vec3; hp: f32; hits: u32; bag: List<u32, 16> }
|
|
528
|
+
interface Missile { pos: vec3; vel: vec3; target: Ref<Unit> }
|
|
529
|
+
const home = kernel((m: Each<Missile>, units: Rows<Unit>, dt: f32, speed: f32) => {
|
|
530
|
+
const t = units.get(m.target);
|
|
531
|
+
if (!t.exists) return;
|
|
532
|
+
const to = sub(t.pos, m.pos);
|
|
533
|
+
m.vel = scale(normalize(to), speed);
|
|
534
|
+
m.pos = add(m.pos, scale(m.vel, dt));
|
|
535
|
+
});
|
|
536
|
+
const strike = kernel((a: Each<Attacker>, units: Rows<Unit>) => {
|
|
537
|
+
const t = units.get(a.target);
|
|
538
|
+
t.hp -= a.damage;
|
|
539
|
+
t.hits++;
|
|
540
|
+
});
|
|
541
|
+
const trade = kernel((tr: Each<Trader>, units: Rows<Unit>) => {
|
|
542
|
+
const t = units.get(tr.partner);
|
|
543
|
+
if (!t.exists || tr.bag.length === 0) return;
|
|
544
|
+
t.bag.push(tr.bag.pop());
|
|
545
|
+
});
|
|
546
|
+
game.world.run(home, missiles, units, dt, 30);
|
|
547
|
+
Kernels with these operations run on the engine interpreter on device and tick and on the JavaScript VM on the web, with the same bits; they are not GPU kernels.
|
|
548
|
+
|
|
549
|
+
Trained tree models (gradient-boosted trees from XGBoost, LightGBM or ONNX; the build adds nothing to the engine features): put the exported model under assets/ (XGBoost booster.save_model("difficulty.json"), LightGBM booster.save_model("difficulty.txt"), or an ONNX file with one TreeEnsembleRegressor or TreeEnsembleClassifier), then const difficulty = model("models/difficulty.json") at the top level reads assets/models/difficulty.json at build time and compiles it to kernel bytecode: every split is a compare and every tree a chain of selects, so no row branches, and the trees are summed in a fixed order, so a prediction has the same bits on every device and back-end. Missing values (NaN) follow each split's default direction, base_score and the objective's output (sigmoid, softmax, exp) are applied for you, and categorical splits, linear trees and vector leaves are refused with the fix. Predict over SoA columns, in batches, without allocating after the first call:
|
|
550
|
+
import { model } from "@cimplify/sdk/game";
|
|
551
|
+
const difficulty = model("models/difficulty.json");
|
|
552
|
+
const features = [speed, accuracy, deaths, level, streak, minutes]; // Float32Array columns, one per feature in training order
|
|
553
|
+
const out = new Float32Array(players.capacity);
|
|
554
|
+
difficulty.predict(features, out, players.count); // one output; multi-class models take one Float32Array per class
|
|
555
|
+
await difficulty.ready(); // optional: the generated wasm SIMD back-end, compiled in the background; until then predict runs a branching JS walk with the same bits
|
|
556
|
+
difficulty.features, difficulty.outputs and difficulty.featureNames describe the model; difficulty.info has its format, trees, depth and transform. Predictions match XGBoost's raw margins bit for bit and its probabilities to 2 ulp; LightGBM sums in double, so expect a few ulp there. A model file is a build input and is not shipped as an asset.
|
|
557
|
+
|
|
558
|
+
Your forEach, map and reduce callbacks: when a table.forEach or table.map callback fits the subset, the build compiles it to a kernel automatically (locals it reads become kernel arguments) and says so in a build note; when it does not, the build rewrites it into a direct column loop (no cursor calls) and the note says why it could not be a kernel.
|
|
559
|
+
|
|
560
|
+
Editor: add { "compilerOptions": { "plugins": [{ "name": "@cimplify/cli/dist/kernel-plugin.cjs" }] } } to tsconfig.json to see kernel errors while you type.
|
|
561
|
+
|
|
562
|
+
CPU or GPU: on engines with GPU kernels each run picks the CPU interpreter or the GPU from a cost model (rows, kernel size, neighbour walks, whether the table already lives on the GPU, whether you need the results this frame, and the device class). Results are the same on both, bit for bit, including sin, cos, exp, log, pow and atan2 (the GPU runs the same maths library as the CPU), values too small for normal floats (the GPU recomputes those exactly instead of flushing them to zero), sorting, scatter-add and spatial queries, so a kernel may feed gameplay state on either device. Override per kernel with kernel(fn, { device: "cpu" | "gpu" | "auto" }); add feeds: "render" for kernels whose output only feeds drawing (instances, particles) so the GPU skips the same-frame readback and the written fields land at the next frame turn. Neighbour and query kernels gain the most (a 1M-row flock runs in about 2.5 ms on an M-series GPU against about 106 ms on five CPU lanes); light per-row kernels move to the GPU only from tens of thousands of rows and only when their results stay on the GPU. Kernels with engine calls, f64 transcendentals or data ops always run on the CPU.
|
|
563
|
+
|
|
564
|
+
Development: configureKernels({ mode: "dev", checkEvery: 30 }) runs kernels as plain JavaScript (so breakpoints work) and every 30th run compares every column with the bytecode, reporting the field, row and both values when they differ.
|
|
565
|
+
|
|
566
|
+
Maths library: random numbers, fixed point, prediction, geometry, motion, rotation, small networks, noise, simulation and colour helpers are in the kernel-math topic.`
|
|
567
|
+
}, KERNEL_MATH = {
|
|
568
|
+
id: "kernel-math",
|
|
569
|
+
title: "Kernel maths library: random, fixed point, geometry, motion, rotation, networks, noise, simulation, colour, harmonics, filters, DCT",
|
|
570
|
+
summary: "Order-independent random numbers (rngKey, randNormal, randOnSphere, randPermute), Q16.16 and Q32.32 fixed point, intercepts, ballistics, closest points, rays, smoothDamp, PID, slerp, swing-twist, dense and int8 layers, simplex, worley, curl, fBm, Verlet, SPH kernels, OKLab, spherical harmonics, biquad and one-pole filters, DCT; the same bits in kernels and in plain JavaScript.",
|
|
571
|
+
body: `Every helper below is an ordinary function from @cimplify/sdk/game. Inside a kernel the build inlines it (no call, no allocation, no new opcode); outside a kernel it runs as plain JavaScript and gives the same bits, because every step rounds to f32 and transcendentals use the kernel's own maths (fsin, fcos, ftan, fasin, facos, fatan, fatan2, fexp, flog, flog2, fpow, which you can also call yourself; Math.sin in plain JavaScript is a different function). The guarantee holds for f32 inputs: table fields, kernel results and literals that f32 holds exactly (0.5, 2, 9.8125), or Math.fround(0.1).
|
|
572
|
+
|
|
573
|
+
import { add, kernel, rngKey, randOnSphere, randRange, smoothDamp3, simplex3, type Each, type f32, type u32, type vec3 } from "@cimplify/sdk/game";
|
|
574
|
+
interface Spark { pos: vec3; vel: vec3; goal: vec3; heat: f32 }
|
|
575
|
+
const sparks = kernel((e: Each<Spark>, seed: u32, tick: u32, dt: f32) => {
|
|
576
|
+
const k = rngKey(seed, e.row, tick, 1);
|
|
577
|
+
const d = smoothDamp3(e.pos, e.goal, e.vel, 0.25, 20, dt);
|
|
578
|
+
e.pos = d.value;
|
|
579
|
+
e.vel = add(d.velocity, randOnSphere(k, 0));
|
|
580
|
+
e.heat = randRange(k, 1, 0.5, 1) * simplex3(e.pos.x, e.pos.y, e.pos.z, seed);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
Random numbers are counter based: rngKey(seed, row, tick, site) hashes the four words into a key and draw n of that key is a hash of (key, n), so a value depends only on those numbers, never on row order, job split, back-end, GPU or replay. Give each call site its own site number and each draw its own n. randBits(key, n) u32, randUnit(key, n) in [0, 1) with 24 bits, randRange(key, n, low, high), randInt(key, n, low, high) in [low, high), randChance(key, n, p), randNormal(key, n) and randNormal2 (Box-Muller), randGaussian(key, n, mean, deviation), randExp(key, n, rate), randWeighted(key, n, weights) picks an index in proportion to a fixed array of weights (zero weights are never picked), randShuffle(key, n, values, out) (Fisher-Yates over a fixed array), randPermute(key, index, count) is a bijection of 0..count-1 (swap-or-not, any count, no tables), randInDisc, randOnCircle, randOnSphere, randInBall, randHemisphere(key, n, normal), randCosHemisphere(key, n, normal) (cosine weighted), poissonPoint(cellX, cellY, cellSize, minDist, seed) (one point per grid cell, points in different cells at least minDist apart). rand01(seed, stream) and hash32(a, b) are the single-opcode primitives underneath.
|
|
584
|
+
|
|
585
|
+
Fixed point for lockstep games that avoid floats: Q16.16 lives in an i32 (type fx; FX_ONE = 65536): fxFromInt, fxToInt (floor), fxFromF32 (round), fxToF32, fxAdd, fxSub, fxNeg, fxAbs, fxFloor, fxFrac, fxMul (floor of the exact product), fxDiv (exact quotient truncated toward zero, saturates to FX_MAX or FX_MIN, x / 0 saturates), fxSqrt (exact floor root), fxSin and fxCos (radians, 1024-entry table with linear interpolation, error below 2^-15), fxLerp. Q32.32 is { hi: i32, lo: u32 } (type fx64; make struct fields of it): fx64FromInt, fx64FromF32, fx64ToF32, fx64FromFx, fx64ToFx, fx64Add, fx64Sub, fx64Neg, fx64Lt, fx64Mul (floor of the exact product), fx64Div and fx64Sqrt (an f32 estimate refined once, about 46 correct bits), fx64Sin, fx64Cos. umulhi, uaddCarry and usubBorrow expose the u64 lane opcodes. Instruction sets: fxSin, fxCos and denseTable read tables (kernel_isa_3); fx64 maths uses the u64 opcodes (kernel_isa_2); everything else is kernel_isa_1, the widest set, which can also run on the GPU. The build adds the feature for you.
|
|
586
|
+
|
|
587
|
+
import { kernel, fxMul, fxAdd, fxFromF32, type Each, type i32 } from "@cimplify/sdk/game";
|
|
588
|
+
interface Mover { x: i32; vx: i32 }
|
|
589
|
+
const step = kernel((e: Each<Mover>, dt: i32) => { e.x = fxAdd(e.x, fxMul(e.vx, dt)); });
|
|
590
|
+
game.world.run(step, movers, fxFromF32(1 / 60));
|
|
591
|
+
|
|
592
|
+
Prediction and geometry: interceptTime(relPos, relVel, speed) and interceptTime2 (closed-form quadratic, -1 when no intercept); launchAngles(speed, distance, height, gravity) returns { low, high, ok }; launchSpeed(angle, distance, height, gravity) (-1 when unreachable); projectilePos and projectileVel; dragPos and dragVel (linear drag, closed form); quadDragStep(pos, vel, gravity, drag, dt, substeps) returns { pos, vel } (quadratic drag); closestOnSegment, segmentDistSq, closestOnTriangle, closestInAabb, closestInObb(p, center, rotation, half), closestSegments(p1, q1, p2, q2) returns { s, t, a, b, distSq }; raySphere, rayAabb, rayPlane(origin, dir, plane) with plane = vec4(normal, w) where dot(normal, p) + w = 0, all returning the hit distance or -1 (0 when the origin is inside); rayTriangle returns { t, u, v, hit }; inCone(p, apex, axis, cosHalfAngle, range), inPolygon(p, vertices, count) over a fixed array of up to N vertices, inTriangle; spheresOverlap, aabbOverlap, sphereAabbOverlap; sweepSpherePlane(from, to, radius, plane) returns the fraction of the move at contact (0 when already touching, -1 when clear); planeFromPoints.
|
|
593
|
+
|
|
594
|
+
Motion: smoothDamp, smoothDamp2, smoothDamp3 (critically damped spring with a speed limit, returns { value, velocity }; store the velocity in a field), expSmooth, expSmooth2, expSmooth3 (rate-based, frame-rate independent), approach(current, target, halfLife, dt), decay(value, rate, dt), moveToward, moveToward2, moveToward3, rotateToward(angle, target, maxDelta) along the short way, springStep, pidStep(state, error, kp, ki, kd, dt, integralLimit) returns { output, state } with state { integral, previous } kept as struct fields.
|
|
595
|
+
|
|
596
|
+
Rotation: qslerp, qnlerp, qFromTo(from, to), qLookRotation(forward, up) (+Z forward, +Y up), qSwingTwist(q, axis) returns { swing, twist } with q = swing * twist, qClampSwingTwist(q, axis, maxSwing, maxTwist), qFromAxisAngle, qToAxisAngle returns { axis, angle }, qFromEuler(vec3) and qToEuler(q) with intrinsic Y-X-Z order (yaw y, then pitch x, then roll z: q = qy * qx * qz), qRotateToward, qAngle, qForward, qUp, qRight, qIntegrate(q, angularVelocity, dt), qnormalize, basisFrom(normal) returns an orthonormal { t, b }; wrapAngle to [-pi, pi), angleDiff(from, to), rotate2, angleOf2, dirOf2.
|
|
597
|
+
|
|
598
|
+
Small networks and statistics: dense(input, weights, bias, out) with weights row-major [output][input], denseRelu, denseTable(input, table, out) reading weights then biases from a KernelLut (cheap for wide layers), quantize(x, scale) and denseQ8(input, packed, bias, scale, out) with four int8 weights per u32 (low byte first) and an i32 accumulator. out is a scratch fixed array you own (const H: [f32, f32, f32, f32] = [0, 0, 0, 0]); use the returned array and give each layer its own scratch. relu, leakyRelu, sigmoid, tanhf, softplus, softmax(input, out), argmax (first maximum wins, NaN never wins), statsAdd(stats, x) (Welford, stats { count, mean, m2 }), statsVariance, statsSampleVariance, ema.
|
|
599
|
+
|
|
600
|
+
import { argmax, dense, kernel, type Each, type f32, type i32 } from "@cimplify/sdk/game";
|
|
601
|
+
interface Mob { hp: f32; fear: f32; dist: f32; choice: i32 }
|
|
602
|
+
const W = [0.5, -0.25, 1, 0.75, -0.5, 0.25] as const;
|
|
603
|
+
const B = [0.125, -0.25] as const;
|
|
604
|
+
const H: [f32, f32] = [0, 0];
|
|
605
|
+
const think = kernel((e: Each<Mob>) => { e.choice = argmax(dense([e.hp, e.fear, e.dist], W, B, H)); });
|
|
606
|
+
|
|
607
|
+
Noise (seeded, table free, approximately [-1, 1]): simplex2, simplex3, simplex4, simplex2d and simplex3d (value plus analytic gradient), worley2 and worley3 return { x: F1, y: F2 }, curl2 and curl3 (divergence free, for particles and smoke), fbm2, fbm3, ridged2, ridged3 (octaves, lacunarity, gain, seed; normalised), warp2 and warp3 (domain warping: feed the warped point to another noise call). Approximate costs in instructions: simplex2 110, simplex3 210, simplex4 360, worley2 160, curl3 630, fbm2 with four octaves 490.
|
|
608
|
+
|
|
609
|
+
Simulation: eulerStep, verletStep and rk2Step(pos, vel, accel, dt, substeps) take your acceleration function (p, v) => vec3 and return { pos, vel } (verlet also { acc }); sphPoly6(distSq, h), sphSpikyGrad(offset, h), sphViscosityLap(r, h); reflectVelocity(vel, normal, restitution, friction); sphereImpulse(p1, v1, m1, p2, v2, m2, restitution) returns { v1, v2 }; springForce.
|
|
610
|
+
|
|
611
|
+
Colour: srgbToLinear, linearToSrgb (and the vec3 forms with 3), rgbToHsv, hsvToRgb, rgbToHsl, hslToRgb (hue in [0, 1)), linearToOklab, oklabToLinear, oklabToOklch, oklchToOklab (hue in radians), mixOklab(a, b, t) on linear colours, luminance (Rec. 709), premultiply, unpremultiply, blendOver(src, dst) on premultiplied colours.
|
|
612
|
+
|
|
613
|
+
Spherical harmonics for light probes and ambient lighting (real SH, one channel per call; keep r, g and b as three sets): L1 is a vec4 (type SH4: x = band 0, y, z, w = band 1 for the y, z and x directions) with sh4Basis(dir), sh4Project(dir, value), sh4Eval(sh, dir), sh4Irradiance(sh, normal), sh4Add, sh4Scale, sh4Rotate(sh, q). L2 is a fixed array of 9 (type SH9, standard order: band 0, then y, z, x, then xy, yz, 3z^2 - 1, xz, x^2 - y^2) with sh9Basis(dir, out), sh9Project(dir, value, out), sh9Eval, sh9Irradiance (Ramamoorthi-Hanrahan cosine-lobe factors pi, 2pi/3, pi/4; divide by pi for the diffuse radiance), sh9Add(a, b, out), sh9Scale(a, s, out), sh9Rotate(sh, q, out) (exact: band 1 as a vector, band 2 from five fixed sample directions); store probes in FixedArray<f32, 9> fields. Accumulate a probe by adding sh9Project(dir, radiance * weight, scratch) into it each frame.
|
|
614
|
+
|
|
615
|
+
import { kernel, sh9Irradiance, type Each, type f32, type FixedArray, type vec3 } from "@cimplify/sdk/game";
|
|
616
|
+
interface Lit { normal: vec3; probe: FixedArray<f32, 9>; ambient: f32 }
|
|
617
|
+
const ambient = kernel((e: Each<Lit>) => { e.ambient = sh9Irradiance(e.probe, e.normal) * 0.3183099; });
|
|
618
|
+
|
|
619
|
+
Filters with per-row state (signals, input, haptics; they pair with pidStep): biquadLowpass, biquadHighpass, biquadBandpass (0 dB peak), biquadNotch(freq, sampleRate, q) and biquadPeaking(freq, sampleRate, q, gainDb) design RBJ cookbook coefficients { b0, b1, b2, a1, a2 } (normalised by a0); biquadStep(coefficients, state, x) runs direct form I and returns { y, state } with state { x1, x2, y1, y2 } kept as struct fields (design inside the kernel from cutoff and sample-rate arguments, a few instructions, or keep per-row coefficients in a struct field). onePoleAlpha(cutoffHz, dt) and onePole(previous, x, alpha) smooth; dcBlock(state, x, r) with state { x1, y1 } and r near 1 (0.995) removes drift.
|
|
620
|
+
|
|
621
|
+
import { biquadLowpass, biquadStep, kernel, type BiquadState, type Each, type f32 } from "@cimplify/sdk/game";
|
|
622
|
+
interface Stick { raw: f32; smooth: f32; filter: BiquadState }
|
|
623
|
+
const smoothStick = kernel((e: Each<Stick>, cutoff: f32, rate: f32) => {
|
|
624
|
+
const r = biquadStep(biquadLowpass(cutoff, rate, 0.7071), e.filter, e.raw);
|
|
625
|
+
e.smooth = r.y;
|
|
626
|
+
e.filter = r.state;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
DCT for compact formats: dct8, idct8, dct16, idct16 (orthonormal DCT-II and its inverse DCT-III, fixed f32 coefficients and a fixed summation order, so the same bits everywhere), dct8x8(input, scratch, out) and idct8x8 on row-major 8x8 blocks (rows, then columns). Inputs, scratch and outputs are fixed arrays you own and must be distinct.
|
|
630
|
+
|
|
631
|
+
Trained networks: mlp(net, [inputs], outputs) runs a small network (up to 8 layers, 128 wide, 32 outputs) for every row, with net a kernelArray("policy", { kind: "u32", length }) filled by setMlp(net, words) or loadMlp(net, url). Train one from your own samples or recorded play without leaving Cimplify: trainMlp({ samples, inputs, targets }, { widths: [32, 64, 64, 8], epochs, batch, learningRate, momentum, seed }) returns { blob, loss, blobLoss } (blobLoss is the error of the network exactly as it will run in the game), or run cimplify kernel train samples.json --widths 32,64,64,8 --out policy.mlp with { "inputs": [[...]], "targets": [[...]] }. Training is deterministic: the same samples and options give the same file on every machine and in every browser, so a network can be checked into the game and retrained by anyone. A 32-64-64-8 network on two thousand samples trains in about a second.
|
|
632
|
+
|
|
633
|
+
import { kernel, kernelArray, mlp, type Each, type f32 } from "@cimplify/sdk/game";
|
|
634
|
+
export const policy = kernelArray("policy", { kind: "u32", length: 6000 });
|
|
635
|
+
interface Npc { seen: f32; hp: f32; ammo: f32; flee: f32; fight: f32 }
|
|
636
|
+
const decide = kernel((e: Each<Npc>) => {
|
|
637
|
+
const y = mlp(policy, [e.seen, e.hp, e.ammo], 2);
|
|
638
|
+
e.flee = y[0];
|
|
639
|
+
e.fight = y[1];
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
Explicit vectors: f32x2, f32x3, f32x4, i32x2, i32x3 and i32x4 hold two to four lanes of one row (made from numbers, one number for every lane, or a vec2/vec3/vec4). Kernels already process many rows at once, so ordinary numbers and vec3/vec4 maths are vectorised for you; you never need explicit vectors to make a kernel fast. Write them when one row does the same maths on several values together (four colour channels, a normal that is normalised and dotted several times, swizzles, horizontal sums): the kernel then does that maths as one step per vector instead of one per component. Lane-wise maths (vadd, vmul, vlerp, vclamp and the rest) runs at the same speed as the vec form; chains of vdot, vlength, vnormalize and vcross run faster, about 1.14x on a tangent-frame kernel. They give exactly the same results as the vec form. Lanes: v.x .. v.w, vlane(v, k), vwith(v, k, value), vtovec(v) back to a vec. Lane-wise: vadd, vsub, vmul, vdiv, vmin, vmax, vabs, vneg, vsqrt, vfloor, vmuladd (multiply then add, rounded twice), vlerp, vclamp, vand, vor, vxor, vnot, vshl, vshr, vushr, vfloat, vint; a number as the second argument applies to every lane. Comparisons vlt, vle, veq, vne give masks for vselect(mask, a, b). vshuffle(v, [3, 2, 1, 0]) reorders lanes (a second vector adds lanes 4..7). vdot, vsum, vhmin, vhmax, vlength, vnormalize and vcross (f32x3) reduce or combine. Store results in fields as numbers or vecs; read the lanes you need into numbers before an engine call or a whole-table step, and make vectors inside a neighbour loop rather than before it.
|
|
643
|
+
|
|
644
|
+
import { kernel, f32x3, f32x4, vdot, vlerp, vnormalize, vclamp, vmuladd, type Each, type f32, type vec3, type vec4 } from "@cimplify/sdk/game";
|
|
645
|
+
interface Lit { n: vec3; l: vec3; base: vec4; t: f32; out: vec4 }
|
|
646
|
+
const shade = kernel((e: Each<Lit>) => {
|
|
647
|
+
const d = Math.max(vdot(vnormalize(f32x3(e.n)), vnormalize(f32x3(e.l))), 0);
|
|
648
|
+
const c = vclamp(vmuladd(vlerp(f32x4(e.base), f32x4(1, 0.9, 0.8, 1), e.t), d, 0.05), 0, 1);
|
|
649
|
+
e.out = vec4(c.x, c.y, c.z, c.w);
|
|
650
|
+
});`
|
|
651
|
+
}, NAV = {
|
|
652
|
+
id: "nav",
|
|
653
|
+
title: "Navigation: road traffic, navmesh paths and crowds",
|
|
654
|
+
summary: "game.nav.loadRoads lanes and addVehicle traffic, external player vehicle, nav tiles, findPath, crowd agents.",
|
|
655
|
+
body: `Road traffic runs on a lane graph inside the engine (car following, give way, signals):
|
|
656
|
+
|
|
657
|
+
const lanes: LaneSpec[] = [
|
|
658
|
+
{ points: [[-40, 0, -20], [40, 0, -20], [40, 0, 20], [-40, 0, 20], [-40, 0, -20]], speedLimit: 11, next: [0] },
|
|
659
|
+
];
|
|
660
|
+
game.nav.loadRoads({ lanes, maxVehicles: 12, seed: 7 });
|
|
661
|
+
const car = game.nav.addVehicle(0, 15, 9, { length: 4.4, desire: 0.9 });
|
|
662
|
+
|
|
663
|
+
LaneSpec: points ([x, y, z] polyline), speedLimit (m/s), next (lane indexes a vehicle continues onto; a lane that lists itself loops), signal, group, giveWay, conflicts. SignalSpec: phases ({ groups, seconds }[]), amber, offset. DriverDesc for addVehicle(lane, positionAlongLane, speed, driver): length, desire, headway, jam, accel, decel.
|
|
664
|
+
|
|
665
|
+
Read poses after each step: const poses = game.nav.vehicles; const o = game.nav.vehicleOffset(car); x = poses[o + POSE_X], z = poses[o + POSE_Z], heading from poses[o + POSE_DIR_X] and poses[o + POSE_DIR_Z] (constants exported from @cimplify/sdk/game). Place each traffic entity with transforms.setPosition and a yaw rotation in update().
|
|
666
|
+
|
|
667
|
+
Road traffic alone needs no navmesh: loadRoads starts navigation for roads by itself. A game that also walks crowds on a navmesh calls game.nav.init(...) first and then loadRoads, because navigation cannot be re-initialised afterwards.
|
|
668
|
+
|
|
669
|
+
The player's own vehicle joins the graph so traffic yields to it: const me = game.nav.addVehicle(lane, along, 0, { length: 5.2 }); then every fixed step game.nav.setExternalVehicle(me, lane, along, speed). removeVehicle(id) when the player leaves the road. game.nav steps itself inside the game loop.
|
|
670
|
+
|
|
671
|
+
Walkable navigation: game.nav.init({ tileWorld, maxAgents, ... }), addTile(bytes) (tiles from encodeNavTile or baked into a world), nearest(point, out), findPath(from, to, navPath), addAgent(position, { radius, maxSpeed, maxAcceleration }), setAgentTarget(agent, target), stopAgent, agentPosition(agent, out). A streamed world streams its baked tiles from its nav packs with stream.loadNav(game.nav, { maxAgents }).`
|
|
672
|
+
}, CRAFT = {
|
|
673
|
+
id: "craft",
|
|
674
|
+
title: "Craft: boats, aircraft, helicopters and water",
|
|
675
|
+
summary: "createBoat/createAircraft/createHelicopter presets, flight modes, water.sea states, kit craft input.",
|
|
676
|
+
body: `Boats and aircraft are physics craft:
|
|
677
|
+
|
|
678
|
+
const water = game.physics.water;
|
|
679
|
+
water.configure(water.sea("calm"));
|
|
680
|
+
water.setLevel(0);
|
|
681
|
+
const boat = game.physics.createBoat(hull, { preset: "speedboat", mode: "arcade" });
|
|
682
|
+
boat.throttle = 1;
|
|
683
|
+
boat.steer = -0.4;
|
|
684
|
+
|
|
685
|
+
Boat presets: "canoe", "speedboat", "ferry", "cargo_ship", "sailboat" (boat.sheet sets the sail). Aircraft presets via createAircraft(entity, { preset }): "light_plane", "jet", "glider", "tiltrotor" (pitch, roll, yaw, throttle, flaps, brake, gearDown, tilt). Helicopters via createHelicopter(entity, { preset }): "helicopter", "quadcopter", "blimp" (collective, cyclicPitch, cyclicRoll, pedal, engine). createCraftPreset(entity, name, options) covers every preset. Modes: "arcade" or "sim". Craft create their own dynamic body. Put a boat over open water: a ground collider under the hull beaches it and it will not move.
|
|
686
|
+
|
|
687
|
+
Telemetry: craft.speed, airspeed, submerged, planing, bank, pitchAttitude, angleOfAttack, stalled, onGround, climb, thrust.
|
|
688
|
+
|
|
689
|
+
Water: sea states "mirror", "lake", "calm", "moderate", "rough", "storm"; water.setCurrent(x, z), water.setWind(x, z, gust, period), water.height(x, z), water.sample(x, z, out). The same Water model is exported for rendering wave heights.
|
|
690
|
+
|
|
691
|
+
Input: BoatInput, AircraftInput and HelicopterInput (from @cimplify/sdk/game) map input actions, VirtualStick, VirtualSlider and TiltSteering onto craft each fixed step with input.apply(craft).`
|
|
692
|
+
}, WORLD = {
|
|
693
|
+
id: "world-streaming",
|
|
694
|
+
title: "World streaming: big cities in cells",
|
|
695
|
+
summary: "game.world.stream with a baked .world file and .cells packs, update(eye, velocity), actors and colliders, nav tiles.",
|
|
696
|
+
body: `Large places are baked (engine-world bake) into one .world file (cell table, proxies, impostors, colliders, lanes, nav tiles) and .cells packs ordered by distance from the spawn. Put the packs in a delivery group; the build keeps the first pack (the spawn) in it and moves the rest to a group the device fetches on request. Give the stream the pack paths in pack order and ctx.assets: the engine asks for the packs the camera is about to need, nearest first, and drops packs it has left behind.
|
|
697
|
+
|
|
698
|
+
const stream = game.world.stream({
|
|
699
|
+
world: worldBytes,
|
|
700
|
+
packPaths,
|
|
701
|
+
assets: ctx.assets,
|
|
702
|
+
materials: [atlasMaterial, impostorMaterial],
|
|
703
|
+
tier: "mid",
|
|
704
|
+
colliders: { budget: 24, actors: 16, stepSeconds: game.stepSeconds },
|
|
705
|
+
});
|
|
706
|
+
await stream.ready;
|
|
707
|
+
|
|
708
|
+
Every frame: stream.update(cameraPosition, cameraVelocity). Every fixed step: stream.setActor(i, position) for the player and nearby vehicles, stream.setActorCount(n), stream.stepColliders() so colliders exist around moving bodies. Nav tiles ship in their own .nav packs, listed in packPaths after the .cells packs (the bake writes both); stream.loadNav(game.nav, { maxAgents }) streams them into the nav as the camera nears the ground and removes them when they are released, and stream.navTiles counts the tiles loaded. game.prefetch.stream(stream.prefetchLayer(), { stalled: () => stream.stats.waiting > 0 }) adds speed and heading aware lookahead over the packs, meshes before nav. stream.stats reports live, loading and resident bytes. Dispose it in ctx.on_dispose.
|
|
709
|
+
|
|
710
|
+
Small scenes do not need streaming: build them from primitives or a few models. Workshop environments and the Lagos street kit supply ready baked blocks.`
|
|
711
|
+
}, SCENERY = {
|
|
712
|
+
id: "scenery",
|
|
713
|
+
title: "Photoreal scenery: Gaussian splats",
|
|
714
|
+
summary: "graphics.scenery.load(manifest, { assets }) streams baked splat scenery around the camera; setTransform, grid, stats, remove.",
|
|
715
|
+
body: `Captured places (streets, rooms, landscapes) are baked (engine-bake splat) into a splats.cspm manifest plus <sha256>.cspl chunks in the same folder. The engine streams chunks around the active camera and draws them with your meshes.
|
|
716
|
+
|
|
717
|
+
if (game.graphics.scenery.supported) {
|
|
718
|
+
const scenery = await game.graphics.scenery.load("assets/scenery/splats.cspm", { assets: ctx.assets });
|
|
719
|
+
scenery.setTransform({ x: 0, y: 0, z: 0 });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
Chunks are read only when the engine asks for them (at most 8 at a time); nothing to call per frame. A URL or string without options.assets is fetched, with chunks next to the manifest URL. scenery.grid (cellSize, origin [x, z], cols, rows) matches the engine_world cells of a WorldStream baked for the same place. scenery.stats reports resident chunks, splats drawn and pool use. scenery.remove() frees it. supported is false off the native engine; load then throws unsupported_feature, so keep a mesh fallback.
|
|
723
|
+
|
|
724
|
+
Splats can move and change colour. Bake with engine-bake splat and label each splat with up to 255 regions, through a PLY region property, --region-labels <file> (one byte per splat) or --regions regions.json (boxes, spheres or rig joints). Add --rig rig.glb to bind splats to a rig and --drape <region>:<mesh.json> to bind a garment to a cloth mesh.
|
|
725
|
+
|
|
726
|
+
const body = await game.graphics.scenery.splatBody({ manifest: "assets/performer/splats.cspm", rig: modelRoot, assets: ctx.assets, regions: { jacket: 1 }, heroBudget: [160000, 80000, 0], standIn: { show: () => mesh.show(), hide: () => mesh.hide() } });
|
|
727
|
+
body.edit("jacket", { hue: 0.4, saturation: 1.1, gain: 0.9 });
|
|
728
|
+
body.drape(sheet, "jacket");
|
|
729
|
+
body.motion(() => game.kernels.run(wave, table, time), { table, reach: 0.2, opacity: "fade" });
|
|
730
|
+
const twin = new SplatTwin({ body, variants: product.variants, looks: { "Sky": { jacket: { hue: 2.1 } } } });
|
|
731
|
+
twin.select(variantId);
|
|
732
|
+
|
|
733
|
+
splatBody follows that model's animation mixer, so a body shares clips with a mesh on the same rig, joint scale included. edit recolours one region and only sits on a scene once something is edited. drape moves the region with an engine cloth, whenever you call it. motion runs your kernel over a table with one row per splat (scenery.motionRows), reading rest position from rx, ry, rz. It writes the channels you name: offset (dx, dy, dz by default), rotation, colour and opacity, and memory is paid per channel. Scenes each keep their own table. heroBudget holds each scene to its own numbers, and the scenes sharing a device are together held to the largest of those numbers, nearest bodies kept first. stats.standIn is true while a scene's budget is zero, and the standIn you passed is shown for that time. SplatTwin maps a product's variants to looks; your package supplies the looks, by variant id or name.`
|
|
734
|
+
}, PREFETCH = {
|
|
735
|
+
id: "prefetch",
|
|
736
|
+
title: "Predictive prefetch: no loading stalls",
|
|
737
|
+
summary: "game.prefetch predicts where the camera goes (1, 3 and 8 s) and fetches content before it is needed; scene prefetch, next-scene hints, idle menus, network aware.",
|
|
738
|
+
body: `Pass ctx.assets to createGame({ backend, assets: ctx.assets }). Scenery loaded with { assets } is predicted automatically: chunks the camera will need in the next second are requested at soon priority, the 3 and 8 second horizons at prefetch priority, and chunks left far behind are released. Prediction reads only the camera, never the simulation, so replays and rooms are unchanged.
|
|
739
|
+
|
|
740
|
+
game.addScene({ name: "menu", prefetch: { idle: true, next: ["level1"] }, enter() {} });
|
|
741
|
+
game.addScene({ name: "level1", prefetch: { assets: ["assets/level1/props.glb", "assets/level1/music.ogg"], next: ["level2"] }, enter() {} });
|
|
742
|
+
const loading = game.prefetch.scene("level2");
|
|
743
|
+
if (loading.ready) startLevel2();
|
|
744
|
+
|
|
745
|
+
Entering a scene hints its next scenes (bounded while playing, unbounded in idle scenes such as menus and level end; call game.prefetch.idle(true) on a results screen). Hints for scenes you can no longer reach are cancelled. Streams for your own content: game.prefetch.stream({ items: [{ center, radius, bytes, levels: [{ path: coarse }, { path: fine, within: 30 }] }], radius: loadRadius, need: stallRadius }, { maxSpeed, reach }) requests coarse levels first and finer ones as the camera closes in; reach = PrefetchReach.fromNav(game.nav, grid, { from: spawn }) keeps predictions to places the player can walk to. The host's network hint (wifi, cellular, metered, saver) shortens the horizons: saver keeps only the next second. Check readiness with cimplify capture and cimplify test (test/readiness.json); builds warn with stream.stalls when mid or low tier devices would stall along test/camera.json.`
|
|
746
|
+
}, TERRAIN = {
|
|
747
|
+
id: "terrain",
|
|
748
|
+
title: "Terrain, grass, scatter and planets",
|
|
749
|
+
summary: "openTerrain(game.backend.terrain, { size, recipe }), update(eye, velocity) each frame, heightAt, sculpt/paint/hole, grass, wind, scatter into ScatterColumns, openPlanet.",
|
|
750
|
+
body: `Open fields, hills and planets are engine terrain: streamed tiles drawn with continuous LOD, lit like every mesh, with physics colliders and navigation from the same heights.
|
|
751
|
+
|
|
752
|
+
import { openTerrain, ScatterColumns } from "@cimplify/sdk/game";
|
|
753
|
+
|
|
754
|
+
const terrain = openTerrain(game.backend.terrain, {
|
|
755
|
+
size: 4096,
|
|
756
|
+
spacing: 1,
|
|
757
|
+
tier: "mid",
|
|
758
|
+
recipe: { amplitude: 60, frequency: 0.004, octaves: 7, seed: 7, rockSlope: 0.25, snowHeight: 35 },
|
|
759
|
+
});
|
|
760
|
+
terrain.grass({ density: 96, height: [0.35, 0.8], radius: 60, color: [0.1, 0.22, 0.04] });
|
|
761
|
+
terrain.setWind(1, 0.3, 0.4, 0.5);
|
|
762
|
+
|
|
763
|
+
Every frame (update, not fixedUpdate): terrain.update(eyeX, eyeY, eyeZ, velocityX, velocityY, velocityZ) streams tiles around the camera, then terrain.colliders(playerX, playerY, playerZ) keeps heightfield colliders under the player. terrain.heightAt(x, z) gives the ground height (NaN while a tile is still streaming); normalAt(x, z) the surface normal. Place spawns with y = terrain.heightAt(x, z).
|
|
764
|
+
|
|
765
|
+
Sculpting (worlds and editors): terrain.sculpt("raise" | "lower" | "smooth" | "flatten", x, z, radius, strength, target?) per step of a stroke; terrain.paint(layer, x, z, radius, strength) changes the ground material (layers 0 grass, 1 rock, 2 snow, up to 8, recoloured with terrain.setLayers([{ color, roughness, tiling }])); terrain.hole(x, z, radius) cuts caves and tunnel mouths and hole(x, z, radius, false) fills them. Colliders and navigation follow edits.
|
|
766
|
+
|
|
767
|
+
Grass bends away from up to eight interactors: keep one Float32Array(32) of x, y, z, radius per interactor (player, wheels) and call terrain.setInteractors(data, count) each frame.
|
|
768
|
+
|
|
769
|
+
Scatter trees and rocks deterministically by seed: const rocks = new ScatterColumns(512); terrain.scatter({ area: [-200, -200, 200, 200], seed: 3, spacing: 9, chance: 0.5, slope: [0, 0.4], scale: [0.6, 1.4] }, rocks); then for i < rocks.count place an instanced rock at rocks.x[i], rocks.y[i], rocks.z[i] with yaw rocks.yaw[i] and scale rocks.scale[i]. The same seed gives the same layout on every device.
|
|
770
|
+
|
|
771
|
+
Planets: const planet = openPlanet(game.backend.terrain, { radius: 2000, amplitude: 60, seed: 4 }) opens a cube-sphere planet centred on the origin. Call planet.gravity(9.81) once per fixedUpdate before the physics step and world gravity is replaced by a pull toward the planet centre. Only one terrain or planet is open per game; dispose() it in ctx.on_dispose.
|
|
772
|
+
|
|
773
|
+
Noise for gameplay: noise(game.backend.terrain, "perlin" | "simplex2" | "simplex3" | "worley", x, y, z, seed).`
|
|
774
|
+
}, FLUIDS = {
|
|
775
|
+
id: "fluids",
|
|
776
|
+
title: "GPU fluids: water, viscous liquids and sand",
|
|
777
|
+
summary: "game.fluid({ bounds, material, emitters, look }), emit/pour, collider(entity, shape) so bodies push fluid and feel buoyancy, setLook, pause, clear; budgets for phones.",
|
|
778
|
+
body: `Fluids are particles simulated on the GPU (MLS-MPM: particles carry velocity and an affine velocity field, a grid resolves pressure and viscosity) and drawn as a smooth surface with refraction, absorption, a Fresnel reflection and foam. They are cosmetic: the result never feeds gameplay state except the optional forces on colliders, so multiplayer games keep fluids client-side.
|
|
779
|
+
|
|
780
|
+
const water = game.fluid({
|
|
781
|
+
bounds: { min: { x: -2, y: 0, z: -2 }, max: { x: 2, y: 3, z: 2 } },
|
|
782
|
+
material: "water",
|
|
783
|
+
capacity: 32768,
|
|
784
|
+
emitters: [{ kind: "stream", position: { x: 0, y: 2.6, z: 0 }, velocity: { x: 0, y: -2, z: 0 }, radius: 0.12, rate: 4000 }],
|
|
785
|
+
});
|
|
786
|
+
water.pour({ kind: "box", position: { x: -1, y: 0.6, z: 0 }, size: { x: 0.8, y: 0.5, z: 1.6 } });
|
|
787
|
+
water.collider(boat, { box: { x: 0.6, y: 0.2, z: 1.2 } });
|
|
788
|
+
|
|
789
|
+
bounds is the simulation box (fluid stays inside it, walls are solid). cell (default 0.1 m) sets the detail: each axis must span 8 to 192 cells. capacity is the particle budget (up to 262144); streams recycle the oldest particles once full. Sizes: phone games use 16k to 32k particles on a 48-cell grid; desktop up to 256k on a 96-cell grid.
|
|
790
|
+
|
|
791
|
+
Emitters: { kind: "stream", position, velocity, radius, rate } pours particles per second through a disc; pour({ kind: "box" | "sphere", position, size | radius, velocity? }) fills a volume once at rest density. emit returns an index; setEmitter(index, null) stops it. Up to 8 emitters.
|
|
792
|
+
|
|
793
|
+
Materials: "water" (default), "sand" (granular: no tension, frictional, piles up). setParameters({ viscosity, stiffness, friction, gravity, foam }) changes the feel at runtime: viscosity 0 is splashy water, 10 to 30 is honey or mud.
|
|
794
|
+
|
|
795
|
+
Colliders: collider(entity, { sphere: r } | { box: halfExtents } | { capsule: { radius, halfHeight } }) follows the entity's transform each frame. If the entity has a physics body the fluid also uses its velocity (paddles and boats push water) and, with feedback (on by default for bodies), the impulse the fluid exerts is applied back to the body one frame later, so floating crates bob. Up to 16 colliders.
|
|
796
|
+
|
|
797
|
+
Look: setLook({ color, absorption, refraction, smoothing, foam, reflection, resolution, visible }). absorption is per metre per channel (clear water absorbs red first); resolution (0.25 to 1, default 0.5) is the surface buffer scale, the main cost knob on phones.
|
|
798
|
+
|
|
799
|
+
Capability: fluids need GPU compute. game.fluid throws unsupported_feature when the device has none (engine feature fluid_1, exports engine_fluid_*); offer a fallback (a flat animated water plane) in a catch. On iOS the Metal presenter tints the scene behind the fluid by its absorption without the refraction offset; wgpu (web and desktop) also bends the background.
|
|
800
|
+
|
|
801
|
+
Frame cost (M-series Mac, wgpu): see docs/lanes/fluids-destruction.md for the measured 32k and 256k numbers and budgets.`
|
|
802
|
+
}, DESTRUCTION = {
|
|
803
|
+
id: "destruction",
|
|
804
|
+
title: "Destruction: pre-fractured models",
|
|
805
|
+
summary: "Bake with engine-bake fracture, load the GLB, game.destructible(model, { fracture, health, impactThreshold }), damage(point, radius, amount), collapse by support, debris pooling, chips.",
|
|
806
|
+
body: `Walls, crates and whole buildings break into pieces that were cut at build time (Voronoi cells clipped to the mesh, interiors capped), so breaking never stalls a frame.
|
|
807
|
+
|
|
808
|
+
Bake once:
|
|
809
|
+
|
|
810
|
+
engine-bake fracture building.glb building.fractured.glb building.cfrc --count 500 --seed 7 [--impact x y z] [--inner r g b]
|
|
811
|
+
|
|
812
|
+
The fractured GLB holds an "intact" node and fragment_0..N; the .cfrc file holds each fragment's hull, mass, neighbours and whether it touches the ground. Ship both as assets.
|
|
813
|
+
|
|
814
|
+
const model = await game.graphics.loadModel("assets/building.fractured.glb");
|
|
815
|
+
const fracture = new Uint8Array(await (await fetch("assets/building.cfrc")).arrayBuffer());
|
|
816
|
+
const building = game.destructible(model, {
|
|
817
|
+
fracture,
|
|
818
|
+
position: { x: 0, y: 0, z: 0 },
|
|
819
|
+
health: 1,
|
|
820
|
+
impactThreshold: 60,
|
|
821
|
+
impactScale: 0.02,
|
|
822
|
+
maxDebris: 300,
|
|
823
|
+
lifetime: 6,
|
|
824
|
+
chips: { emitter: dustEmitter, entity: dustEntity, perBreak: 6 },
|
|
825
|
+
onBreak: (event) => { if (event.kind === "break") game.audio.play(crunch); },
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
Until something breaks, the intact mesh renders and every fragment is a fixed collider. building.damage(point, radius, amount) subtracts health with a linear falloff; a fragment at zero health becomes a dynamic body thrown outward at breakSpeed. Hard hits break fragments on their own: when the impulse of a new contact exceeds impactThreshold (N s), the excess times impactScale is damage at the hit fragment and its neighbours within spread. With support on (default), fragments no longer connected to the ground through intact neighbours fall too, so knocking out a ground floor brings the storey above down.
|
|
829
|
+
|
|
830
|
+
Breaking is engine work: the intact mesh and the fragments swap in the engine (no per-fragment script calls), and at most maxBreaksPerStep fragments (default 64) turn dynamic per physics step, so a whole building collapses over a few frames instead of one heavy frame. Broken pieces land on the world and on intact pieces but pass through each other; set debrisCollide: true to let rubble pile up, at about four times the physics cost of a large collapse.
|
|
831
|
+
|
|
832
|
+
Debris: at most maxDebris fragments simulate at once; older ones are frozen in place (or removed with removeRetired) when they come to rest after lifetime seconds, or removed when the pool overflows. Nothing allocates while breaking.
|
|
833
|
+
|
|
834
|
+
Multiplayer: breaking and fragment motion run in the physics step from deterministic inputs (damage calls, contacts), so every peer that applies the same damage at the same tick sees the same rubble. Chips are cosmetic GPU particles.
|
|
835
|
+
|
|
836
|
+
Capability: engine feature destruction_1 (exports engine_destruct*), native engine only; game.destructible throws unsupported_feature otherwise.`
|
|
837
|
+
}, ATMOSPHERE = {
|
|
838
|
+
id: "weather",
|
|
839
|
+
title: "Weather, time of day and volumetrics",
|
|
840
|
+
summary: "setWeather cloud/rain/wetness/wind, setTimeOfDay, sky, height fog and volumetric fog, clouds and atmosphere, and media volumes (flame and smoke boxes you write texel by texel).",
|
|
841
|
+
body: `game.graphics.setWeather({ cloudCover: 0..1, haze: 0..1, precipitation: 0..1, snow: false, wetness: 0..1, wind: [x, z] }).
|
|
842
|
+
game.graphics.setTimeOfDay(hours, latitude?, dayOfYear?) positions the sun (for example latitude 6.5 for a city on the Gulf of Guinea coast). It also aims the first directional light, so a night scene below the horizon is lit from underneath: for a moonlit night keep your own directional light and skip setTimeOfDay.
|
|
843
|
+
|
|
844
|
+
Volumetrics live in the environment:
|
|
845
|
+
|
|
846
|
+
game.graphics.setEnvironment({
|
|
847
|
+
sky: { turbidity: 4, clouds: { coverage: 0.4, speed: 0.02 } },
|
|
848
|
+
background: { sky: true },
|
|
849
|
+
toneMapping: "agx",
|
|
850
|
+
shadows: true,
|
|
851
|
+
volumetrics: {
|
|
852
|
+
enabled: true,
|
|
853
|
+
fog: { density: 0.02, height: 0, falloff: 0.05, anisotropy: 0.6 },
|
|
854
|
+
clouds: { coverage: 0.5, density: 0.6, altitude: 1500, thickness: 800 },
|
|
855
|
+
atmosphere: { rayleigh: 1, mie: 1, sunDiscDegrees: 0.53 },
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
The sky only shows behind the scene when background is { sky: true }; without it the background is black.
|
|
860
|
+
|
|
861
|
+
Media volumes are boxes of glowing or smoky air you fill yourself: flames, smoke, steam, magic. game.graphics.mediaVolume({ origin, extent, size, emission?, extinction?, albedo? }) makes one (up to 4 per scene, up to 64 texels per axis). Each texel is RGBA8: rgb times emission is light the air gives off, alpha times extinction is how thick it is, and albedo is how much light thick air scatters back (grey smoke 0.35).
|
|
862
|
+
|
|
863
|
+
const fire = game.graphics.mediaVolume({ origin: { x: -0.2, y: 0, z: -0.2 }, extent: { x: 0.4, y: 0.6, z: 0.4 }, size: [16, 24, 16], emission: 24, extinction: 18, albedo: [0.35, 0.33, 0.31] });
|
|
864
|
+
const texels = fire.texels;
|
|
865
|
+
texels[fire.index(8, 2, 8)] = mediumTexel(255, 120, 20, 160);
|
|
866
|
+
fire.mark();
|
|
867
|
+
|
|
868
|
+
texels is a view straight into engine memory (x fastest, then y, then z), so writing it every frame copies and allocates nothing; call mark() after writing so the frame uploads it. set(desc) moves or resizes the box (a new size clears it) and dispose() frees the slot. Media need the native engine (feature media_volume_1, added when your code calls mediaVolume); elsewhere mediaVolume throws unsupported_feature. They light up through the volumetric fog pass, so the same tier advice applies. For flames and smoke that come from real burning chemistry, game.reactiveFluid writes a media volume for you (chemistry topic).
|
|
869
|
+
|
|
870
|
+
Volumetrics and many post effects cost GPU time: use them on mid and high tier and keep a plain fog for low tier (check the device tier you declared in [delivery] min_device_tier).`
|
|
871
|
+
}, AUDIO = {
|
|
872
|
+
id: "audio",
|
|
873
|
+
title: "Audio",
|
|
874
|
+
summary: "audio.load, buses (music, effects), play, playAt for 3D sound, volume and rate, listener.",
|
|
875
|
+
body: `Add "audio" to engine_features. Load once: const horn = await game.audio.load({ bytes }) (or { url } for packaged assets). Buses: game.audio.music and game.audio.effects, plus createBus(name, parent). Play: game.audio.play(sound, game.audio.effects, gain, rate, loop) returns a voice; playAt(entity, sound, bus, gain, rate, loop) plays at an entity's position; setVolume(voice, gain, rampSeconds), setPlaybackRate(voice, rate), stop(voice), setBusGain(bus, gain, ramp), duck(...). game.audio.setListener(cameraEntity). Procedural sounds: createSound(samples, channels, sampleRate). Audio starts after the first user gesture (game.audio.unlock() from a tap, game.audio.unlocked to check); suspend() and resume() follow ctx.lifecycle. Mixing: setVoiceBudget(voices) and setSoundLimit(sound, limit) cap how many voices play, setLimiter(desc) protects the master, setVoiceFilter(voice, desc) filters one voice, setBinaural(voice, on) and setOcclusion(voice, desc) place 3D sound (pass null to clear a desc); check game.audio.capabilities before using them. Music: game.audio.conductor plays tempo-synced segments, layers and stingers on the beat.`
|
|
876
|
+
}, CAMERA = {
|
|
877
|
+
id: "camera",
|
|
878
|
+
title: "Camera and face: face tracking and a camera view the app draws",
|
|
879
|
+
summary: "ctx.camera.request, track_face, FaceSession, camera views over or under the game, and a character from the player's face.",
|
|
880
|
+
body: `The camera belongs to the app, not your game: your game never receives camera frames, a video stream or pixels. Declare camera in [capabilities] with the reason the player sees, then ask once with ctx.camera.request({ facing: "front", purposes: ["preview", "face_tracking"], audio: false }); the player answers in the app's own sheet, and ctx.camera.release({ grant_id }) turns it off.
|
|
881
|
+
|
|
882
|
+
Face tracking: const tracker = await ctx.camera.track_face(grant_id), or let the kit do it with const face = await FaceSession.start(ctx.camera). Each frame tracker.update() is true when a new face reading arrived; read tracker.expressions (smile, jaw_open, blink_left, blink_right, brow_raise, pucker, 0 to 1, in FACE_LAYOUT.expressions order), tracker.arkit (the 52 ARKit weights in FACE_LAYOUT.arkit order; tracker.measured is 1 when the device measured them and 0 when they were estimated from the six expressions), tracker.head_pose (yaw, pitch, roll in radians, then x, y, z) and tracker.faces. tracker.colours() resolves skin, hair and eye as 0xRRGGBB numbers (-1 when not seen). FaceSession smooths all of these, gives face.expression(FaceExpression.Smile), face.weight(arkitWeight("jaw_open")), face.headPose(HeadPose.Yaw) and face.faceWidth(), and FaceAnchors places stickers on the nose, forehead, chin, eyes or mouth.
|
|
883
|
+
|
|
884
|
+
Camera view: const view = await ctx.camera.view(grant_id, { x: 16, y: 16, width: 120, height: 160, mirror: true, layer: "over" }) asks the app to show the camera at that rectangle, in points of your game surface. layer "over" draws it above your game; "under" draws it behind, so clear those pixels of your game to transparent to see it. view.place({ ... }) moves it, view.hide() and view.show() toggle it, view.close() removes it; FaceSession has face.view(placement) and closes its views with the session. The view needs the "preview" purpose and tracking needs "face_tracking"; without them the call is refused. Mirror defaults to true and layer to "over". While the camera is on and no view is shown, the app shows its own small camera-on preview. Clips include the camera image only when the player allows camera in clips.
|
|
885
|
+
|
|
886
|
+
A character from the player's face: const { identity } = await ctx.camera.capture_identity(grant_id) asks the app to make one. The app shows its own screens over your game (the player confirms it is their own face, frames it and captures), and your game receives only identity, the character record (CIDP v2, base64), never a picture. With the kit, const { identity, params } = await captureFace(face) on a FaceSession gives the record as bytes and its decoded parameters for buildIdentityCharacter. When it does not finish, the promise rejects with an IdentityCaptureError whose code is "consent" (the player said not now, or there is no "face_tracking" grant), "cancelled" (closed after agreeing), "refused" (the player's age does not allow it) or "unavailable" (another capture is open, the game is hidden, or the app cannot capture). Players under 16 cannot use it, and 16 and 17 year olds get a stylised character. The app may offer to save the character as the player's avatar; that is the player's choice and your game does not see it.`
|
|
887
|
+
}, GAME_AI = {
|
|
888
|
+
id: "game-ai",
|
|
889
|
+
title: "game.ai: NPC brains as data",
|
|
890
|
+
summary: "Utility AI archetypes (actions, considerations, curves, behaviours), spawn agents, read poses; deterministic, no model calls.",
|
|
891
|
+
body: `game.ai is deterministic engine AI (perception, utility decisions, steering, squads, schedules). A brain is a declaration, never code. Add "ai" to engine_features; game.ai is null on backends without it, so guard.
|
|
892
|
+
|
|
893
|
+
import { ai } from "@cimplify/sdk/game";
|
|
894
|
+
const commuter = ai.archetype("commuter", {
|
|
895
|
+
actions: [
|
|
896
|
+
ai.action("wander", { behaviour: ai.behaviour.wander(), base: 0.3 }),
|
|
897
|
+
ai.action("go_home", { behaviour: ai.behaviour.moveToGoal(1), consider: [ai.consider("goal_distance", ai.curve.rising())] }),
|
|
898
|
+
],
|
|
899
|
+
inputs: { goal_distance: ai.input.builtin("goal_distance", 0, 50) },
|
|
900
|
+
steering: { maxSpeed: 1.4 },
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
game.ai.init({ maxAgents: 64, seed: 3 });
|
|
904
|
+
const handle = game.ai.archetype(commuter);
|
|
905
|
+
const agent = game.ai.spawn(handle, { entity, position: [x, 0, z] });
|
|
906
|
+
game.ai.setGoal(agent, gx, 0, gz);
|
|
907
|
+
|
|
908
|
+
Behaviours: idle, moveToFocus, pursue, flee, keepDistance, search, wander, moveToGoal, attack, takeCover, holdFormation, useObject, followSchedule, bark, game(code). Curves: linear, rising, falling, polynomial, logistic, step, points. Inputs: builtin (focus_distance, focus_visible, goal_distance, allies_near, lateness and more), game(slot) for your own signals set with game.ai.setInput, need(index). Read results each frame with game.ai.x(agent), z(agent), forwardX/forwardZ, actionOf(agent), and drainEvents. Targets: addTarget, moveTarget, setFocus. Difficulty: setSkill(agent, 0..1). game.ai steps itself in the game loop.
|
|
909
|
+
|
|
910
|
+
game.ai never calls a language model; model calls are ctx.ai (see the ctx-ai topic).`
|
|
911
|
+
}, CTX_AI = {
|
|
912
|
+
id: "ctx-ai",
|
|
913
|
+
title: "ctx.ai: model calls (talking NPCs, hints, images)",
|
|
914
|
+
summary: "Declared prompts in the [ai] manifest table, ctx.ai.generate/converse/speak/image, consent, budgets, recorded inputs.",
|
|
915
|
+
body: `ctx.ai calls language, speech and image models on the player's behalf. Every prompt, schema, character, action set and style is declared in interactive.toml under [ai] (payer, languages, per_player_daily_units, per_session_units, per_interactive_monthly_units, [[ai.characters]], [[ai.prompts]] with name, template, output, schema, slots, player_text, [[ai.schemas]], [[ai.actions]], [[ai.styles]]). Code passes a prompt name and slot values, never free prompt text:
|
|
916
|
+
|
|
917
|
+
const run = await ctx.ai.generate({ prompt: "hint", slots: [...], output: ..., audience: ..., request_key: ..., stream: false, variant: 0 });
|
|
918
|
+
|
|
919
|
+
Other calls: converse (character dialogue), speak (voice), transcribe, image (style-locked pictures), translate, prefetch, cancel, forget, usage. The host asks the player for AI consent and enforces budgets; output is moderated before anyone else sees it.
|
|
920
|
+
|
|
921
|
+
Declaration rules the build checks: [capabilities] ai = "why the game uses AI" is required; every [[ai.prompts]] has name, template ({{slot}} placeholders), output ("text", "structured" with schema, or "actions" with set), quality ("fast", "standard" or "best") and cache ("none", "personal" or "interactive"; a prompt that takes player_text cannot use "interactive"); slots are [[ai.prompts.slots]] tables with name, kind and limits.
|
|
922
|
+
|
|
923
|
+
[capabilities]
|
|
924
|
+
ai = "The conductor gives you driving hints"
|
|
925
|
+
|
|
926
|
+
[ai]
|
|
927
|
+
payer = "creator"
|
|
928
|
+
languages = ["en"]
|
|
929
|
+
|
|
930
|
+
[[ai.prompts]]
|
|
931
|
+
name = "hint"
|
|
932
|
+
template = "Give a one line driving hint for a danfo near {{place}}."
|
|
933
|
+
output = "text"
|
|
934
|
+
quality = "fast"
|
|
935
|
+
cache = "none"
|
|
936
|
+
|
|
937
|
+
[[ai.prompts.slots]]
|
|
938
|
+
name = "place"
|
|
939
|
+
kind = "text"
|
|
940
|
+
max_chars = 40
|
|
941
|
+
|
|
942
|
+
In cimplify dev, ctx.ai.generate and ctx.ai.converse answer from a labelled loopback that fills the declared template, schema placeholders or the first declared action, so the game flow can be tested offline; with ANTHROPIC_API_KEY set they call the model the prompt's quality names. Speech, images and transcription are unavailable in dev.
|
|
943
|
+
|
|
944
|
+
Model output never runs inside a deterministic step. Treat a result like a player's tap: a timestamped input you apply between steps (the kit's AiInbox helps), so replays, ranked scores and multiplayer stay exact. Model output can only be text, schema-typed data or a choice from a declared action set.`
|
|
945
|
+
}, MULTIPLAYER = {
|
|
946
|
+
id: "multiplayer",
|
|
947
|
+
title: "Rooms and real-time multiplayer",
|
|
948
|
+
summary: "ctx.create_room/join_room, room entry rules, joinTickRoom transport, game.net channels and messages.",
|
|
949
|
+
body: `Turn-based and casual rooms: const room = await ctx.create_room({ ... }) or ctx.join_room({ ... }); the room's shared rules live in src/room.ts ([entries] room = "src/room.ts") and run authoritatively on Cimplify. The kit's TurnGame, CtxTurnRoom and LocalTurnRoom wrap turn engines; FriendChallenges posts results to chats.
|
|
950
|
+
|
|
951
|
+
Real time: rooms run on the tick service. Connect the game API with a tick transport:
|
|
952
|
+
|
|
953
|
+
import { joinTickRoom } from "@cimplify/sdk/game";
|
|
954
|
+
const transport = joinTickRoom(room);
|
|
955
|
+
game.net.connect(transport);
|
|
956
|
+
const inputs = game.net.channel("inputs", "latest");
|
|
957
|
+
|
|
958
|
+
Delivery is "reliable" or "latest" (newest value wins). Define compact messages with game.net.defineMessage(name, schema), write with a MessageWriter and send(channel); read with game.net.reader(). PlanarSnapshotSync (kit) handles snapshot interpolation for top-down games. Keep simulation deterministic and driven by inputs so prediction and reconciliation work.`
|
|
959
|
+
}, TICK_ROOMS = {
|
|
960
|
+
id: "tick-rooms",
|
|
961
|
+
title: "Authoritative tick rooms (your game logic on the server)",
|
|
962
|
+
summary: "defineTickRoom, the tick ctx, determinism rules, budgets, [entries] tick, errors and the cimplify dev inspect stream.",
|
|
963
|
+
body: `A tick room runs your own real-time game rules as the referee on the tick service: the same TypeScript runs on the device (for prediction and offline play) and authoritatively on tick, so a modified client cannot cheat. Declare it in interactive.toml and keep the rules in their own module:
|
|
964
|
+
|
|
965
|
+
[entries]
|
|
966
|
+
client = "src/client.ts"
|
|
967
|
+
tick = "src/tick.ts"
|
|
968
|
+
|
|
969
|
+
Find players with ctx.join_room({ role: "player", target: { kind: "matchmaking", queue: "quick" } }): the registry seats the player in an open room of that queue, or opens one (a tick room on your [entries] tick, 8 seats, Action rates, unless you created a room with access "matchmaking" before, whose terms then become the queue's template) and hands the player the host role; room.leave() reopens the seat for backfill. Queues are per Interactive and release channel.
|
|
970
|
+
|
|
971
|
+
Define the room once in src/tick.ts with defineTickRoom from @cimplify/sdk/interactives. Name state fields with defineTickState (types i8, u8, i16, u16, i32, u32, f32, f64; offsets are naturally aligned) or give stateBytes yourself:
|
|
972
|
+
|
|
973
|
+
import { defineTickRoom, defineTickState } from "@cimplify/sdk/interactives";
|
|
974
|
+
|
|
975
|
+
export const Score = defineTickState({ red: "u32", blue: "u32", ballX: "i32", ballZ: "i32" });
|
|
976
|
+
|
|
977
|
+
export default defineTickRoom({
|
|
978
|
+
state: Score,
|
|
979
|
+
init(ctx) {
|
|
980
|
+
Score.set(ctx.state, "ballX", 32768);
|
|
981
|
+
Score.set(ctx.state, "ballZ", 32768);
|
|
982
|
+
},
|
|
983
|
+
step(ctx) {
|
|
984
|
+
for (let seat = 0; seat < ctx.seats; seat++) {
|
|
985
|
+
if (ctx.alive[seat] === 0) continue;
|
|
986
|
+
ctx.x[seat] = Math.min(65535, Math.max(0, ctx.x[seat] + ctx.axisX[seat] / 512));
|
|
987
|
+
ctx.z[seat] = Math.min(65535, Math.max(0, ctx.z[seat] + ctx.axisZ[seat] / 512));
|
|
988
|
+
}
|
|
989
|
+
ctx.eachCommand((seat, action, payload) => {
|
|
990
|
+
if (action === 1) Score.set(ctx.state, "red", Score.get(ctx.state, "red") + 1);
|
|
991
|
+
});
|
|
992
|
+
if (ctx.random() < 0.001) ctx.log("rare event at", ctx.timeMs);
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
The ctx: seats; per-seat columns alive (read only), x, y, z (Int32, the cell spans 0 to 65535 on x and z), yaw (Uint16), axisX and axisZ (Int16 stick input, read only), buttons (Uint8, read only), animation and flags (Uint8); state (a DataView over your state bytes) and stateBytes (the same bytes as a Uint8Array); tick (step number, from 1), timeMs (simulated time, tick times the step length); random() (seeded, in [0, 1)); commandCount and eachCommand(visit(seat, action, payload)) for the discrete commands players sent this tick (payload at most 512 bytes); log(...parts) (shown by cimplify dev, dropped in production). Write only positions, yaw, animation, flags and state; seats, inputs and liveness belong to the server. A position outside the cell faults the room.
|
|
997
|
+
|
|
998
|
+
Determinism rules. Everything authoritative lives in ctx.state and the seat columns, because that is what checkpoints, late joiners, rollback and tournament replays restore. There is no ambient I/O on tick: no console, timers, fetch, storage, eval or WebAssembly, and Math.random and Date read the room's seed and simulated time. Use ctx.random() and ctx.timeMs so the device and the server agree. Do not keep authoritative values in module variables.
|
|
999
|
+
|
|
1000
|
+
Budgets per room: every step must finish within 4 ms, the room's heap is capped at 64 MiB, and state is at most 256 KiB (262144 bytes). Allocate nothing per step where you can (reuse typed arrays, avoid closures and arrays inside step).
|
|
1001
|
+
|
|
1002
|
+
Errors. defineTickRoom throws TickRoomError with a code: missing_step, invalid_state_bytes, state_bytes_exceeded, invalid_state_field, already_registered (a tick bundle registers exactly one room). On tick, a throw, a step over its deadline, a heap over its cap or a position outside the cell faults the room: it stops and keeps its last good checkpoint, and a failed init or a bundle that never registers refuses to open.
|
|
1003
|
+
|
|
1004
|
+
On the device: createTickRoomSimulation(room, { seats, seed, rateHz }) runs the same room locally with the identical region layout and random sequence; join(seat, x, y, z), leave(seat), input(seat, axisX, axisZ, buttons), command(seat, action, payload), step(), snapshot() and restore(image). Given the same seed, inputs and commands it reproduces tick bit for bit.
|
|
1005
|
+
|
|
1006
|
+
Authoritative state on clients. Set replicate: true on the room and tick sends its state to every client as deltas: it records the state each send, marks the 16-byte chunks that changed, and sends each client only the chunks changed since the last state that client acknowledged (a whole keyframe on join or when that state has left the history), split into datagrams. Declare which fields clients may see with defineTickState(schema, { replicated: ["score", "ballX", "ballZ"] }); those fields are laid out first and only they leave the server, so hidden information (cards in hand, a seeker's target) stays private. On the device the transport from joinTickRoom(room) keeps the confirmed copy: transport.state.tick, transport.state.state (a DataView over the replicated bytes), transport.state.onState((tick, view) => ...) to reconcile, and transport.state.predict((confirmed, out, fromTick, toTick) => ...) with transport.state.predicted(nowTick) for client prediction ahead of the confirmed tick. Read fields with the same TickState you defined (Score.get(transport.state.state, "red")).
|
|
1007
|
+
|
|
1008
|
+
Lockstep or authoritative state? Use authoritative state (replicate: true) when the server's view is the truth and clients may lag it a little: party games, board and card games, battle royales, worlds, anything with hidden information, many players, or players joining mid-match; it costs bandwidth for the changed state and needs no determinism on the device. Use lockstep with rollback when every player must see the same simulation frame for frame and react instantly to it (football, fighting, racing duels) with a few players: it sends only inputs, the device simulates everything locally with no input lag, but the rules must be bit-for-bit deterministic, all players run the full simulation, and a late joiner has to replay from the seed.
|
|
1009
|
+
|
|
1010
|
+
Lockstep with rollback (sports, fighting and other games where every player must see the same ball). Write the rules once as a pure step over inputs and let tick referee the order of inputs:
|
|
1011
|
+
|
|
1012
|
+
import { defineLockstep, defineLockstepRoom, LOCKSTEP_INPUT_STRIDE, LOCKSTEP_AXIS_X, LOCKSTEP_PRESENT } from "@cimplify/sdk/interactives";
|
|
1013
|
+
export const rules = defineLockstep({ seats: 10, stateBytes: 2048, rateHz: 60, inputDelay: 2, init(state, seed, humans) { ... }, step(state, inputs, tick) { ... } });
|
|
1014
|
+
export default defineLockstepRoom(rules, { minPlayers: 2, lobbySeconds: 8, stallSeconds: 3 }); // src/tick.ts
|
|
1015
|
+
|
|
1016
|
+
inputs holds LOCKSTEP_INPUT_STRIDE values per seat: LOCKSTEP_AXIS_X and LOCKSTEP_AXIS_Z (-127 to 127), LOCKSTEP_BUTTONS (0 to 255) and LOCKSTEP_PRESENT (1 when a person drives the seat this tick, 0 when your own bot should). Keep every value in state (a seeded random too), use only + - * / and Math.sqrt, floor, abs, min and max on numbers (no sin, cos, atan2 or exp: engines differ in the last bit), and never read the clock. On the device: const session = new LockstepSession(rules, { seat, link: new TickLockstepLink(transport, { seats: rules.seats }) }) with transport = joinTickRoom(room) (from @cimplify/sdk/game); call session.setInput(x, z, buttons) and session.advance() once per match tick, and draw from session.state. The room waits in a lobby until minPlayers seats are alive (or all seats, or lobbySeconds), picks the seed and the people in the match, then advances a tick only when it holds every person's input for it; a seat that leaves or stays silent for stallSeconds becomes a bot from a tick every client is told. Clients run ahead on predicted input (the last one seen), roll back and replay when real input differs, stall at maxLead ticks ahead, and compare state checksums the room publishes (session.stats: rollbacks, resimulatedTicks, maxRollback, stalls, checks, desyncs). The room uses its seats' presence columns (y, yaw, animation, flags) for its records, so do not write them yourself. Offline play is the same session with no link: session.begin(seed, 1 << seat). LockstepLoopback(room, rules, { players, latencyTicks, jitterTicks }) runs several sessions and the real room in one process for tests.
|
|
1017
|
+
|
|
1018
|
+
Build and dev. cimplify build bundles src/tick.ts (your src/ modules plus the SDK's tick API only; other libraries are refused) into a guest chunk that clients never download. cimplify dev runs it the way the tick service does and streams an inspect panel in the simulator and the terminal: logs, per-tick state and seat diffs, budget use as a percent of the 4 ms deadline and heap use, and errors with the TypeScript file, line and column. Join seat 0 from the panel to drive a seat.`
|
|
1019
|
+
}, PROGRESSION = {
|
|
1020
|
+
id: "progression",
|
|
1021
|
+
title: "Saves, leaderboards, achievements and economy",
|
|
1022
|
+
summary: "openSaveSlots, Leaderboard over ctx.open_counter, Achievements, defineEconomy and publication money gates.",
|
|
1023
|
+
body: `Saves: const slots = await openSaveSlots(ctx, { name: "progress", version: 1, initial: () => ({ best: 0 }), parse }); await slots.load("main"); await slots.save("main", value). Autosave wraps periodic saves.
|
|
1024
|
+
|
|
1025
|
+
Leaderboards: const board = await Leaderboard.open((request) => ctx.open_counter(request), { name: "fares", scope: "interactive" }); await board.submit(participantId, score); board.top(10), board.rankOf(id), board.around(id, 3). Get the participant from (await ctx.identity.current()).participant_id. RankedScores submits with a replay for ranked boards.
|
|
1026
|
+
|
|
1027
|
+
Achievements, DailyChallenges, StatusLine and streaks are in the kit. Economy: declare currencies and items with defineEconomy(...) and mirror them in [economy]; Ledger earns and spends through ctx.game.economy; DropTable needs odds disclosed in [economy] odds; kitPublicationGates(economy) must pass before publishing. Real money only flows through ctx.wallet and host checkout, never your own payment screens.`
|
|
1028
|
+
}, WORKSHOP = {
|
|
1029
|
+
id: "workshop",
|
|
1030
|
+
title: "Workshop content: reusable models, sounds and kits",
|
|
1031
|
+
summary: "cimplify add owner/item, [[content]] entries, licences, loading content/<item>/<file> through ctx.assets at run time.",
|
|
1032
|
+
body: `The Workshop is Cimplify's content library: models, characters, rigs, animations, environments, textures, materials, HDRIs, sounds, music, fonts, VFX, shaders, sprites, UI and templates made by creators, each with a licence (free, free_attribution, paid_per_game, revenue_share, team, exclusive) and per-tier baked variants.
|
|
1033
|
+
|
|
1034
|
+
Add items from the project folder:
|
|
1035
|
+
|
|
1036
|
+
cimplify add cimplify/danfo-bus cimplify/keke-napep cimplify/brt-bus
|
|
1037
|
+
|
|
1038
|
+
That resolves versions, acquires free grants, appends to interactive.toml
|
|
1039
|
+
|
|
1040
|
+
[[content]]
|
|
1041
|
+
item = "cimplify/danfo-bus"
|
|
1042
|
+
version = "^1.0.0"
|
|
1043
|
+
|
|
1044
|
+
pins digests in cimplify.lock and caches files in .cimplify/content. Optional group = "<delivery group>" decides when the item downloads. Paid items are bought on Workshop first. Credits for attribution licences are generated at build (cimplify credits).
|
|
1045
|
+
|
|
1046
|
+
At run time an item's files are package assets under content/<owner>/<item>/<file>:
|
|
1047
|
+
|
|
1048
|
+
const bytes = await ctx.assets.take("content/cimplify/danfo-bus/danfo.glb");
|
|
1049
|
+
const danfoModel = await game.graphics.loadModel({ bytes: bytes.slice().buffer });
|
|
1050
|
+
game.graphics.instantiate(bodyEntity, danfoModel.id);
|
|
1051
|
+
|
|
1052
|
+
Curated items include cimplify/danfo-bus, cimplify/keke-napep, cimplify/brt-bus, cimplify/okada, cimplify/kiosk, cimplify/market-stall, cimplify/generator, cimplify/roadside-umbrella, cimplify/street-signage, cimplify/yellow-barrier, cimplify/pole-and-wires, cimplify/lagos-street-atlas (palette texture the Lagos models sample), cimplify/floodlit-stadium, cimplify/stadium-crowd, cimplify/stadium-roar, cimplify/match-ball, cimplify/football-impacts and kenney/interface-clicks. Keep a primitive fallback (a coloured box) when an item is not added yet, so the game always runs. You cannot run cimplify add yourself from an edit; tell the creator which items to add.`
|
|
1053
|
+
}, BLOCKS = {
|
|
1054
|
+
id: "blocks",
|
|
1055
|
+
title: "Kit building blocks: inventory, shop, quests, waves, score, NPC schedules, pickups, checkpoints",
|
|
1056
|
+
summary: "Allocation-free gameplay blocks from @cimplify/sdk/game/kit that keep a game to a few hundred lines.",
|
|
1057
|
+
body: `Every block lives in @cimplify/sdk/game/kit, stores its state in typed arrays sized once in the constructor, never allocates in fixedUpdate or update and uses game.random and the fixed step, so replays stay identical. Build blocks in the scene constructor or enter(); call them from fixedUpdate. Time-driven blocks take the frame time object (game.loop.time), and position queries take a typed array plus an offset laid out x, y, z (game.transforms.store.fields.position with game.transforms.row(entity) * 3), so no numbers are boxed across calls.
|
|
1058
|
+
|
|
1059
|
+
Inventory: const bag = new Inventory(slots, stackLimit). Items are whole-number ids you name as constants. bag.add(item, n) returns how many fit, bag.remove(item, n) is all or nothing, bag.count(item), bag.has(item, n), bag.room(item), bag.saveInto(int32) and bag.restore(int32) (INVENTORY_SNAPSHOT_STRIDE per slot) for saves.
|
|
1060
|
+
|
|
1061
|
+
Shop: const shop = new Shop(COIN, [{ item: APPLE, quantity: 2, price: 3 }, { item: SWORD, quantity: 1, price: 10, stock: 1 }]). shop.buy(offer, bag) and shop.check(offer, bag) return ShopResult.Bought, UnknownOffer, TooPoor, SoldOut or NoRoom; shop.restock(). The currency is an in-game item in the same Inventory; declare it as an in-world currency in [economy] and never sell real-money value here.
|
|
1062
|
+
|
|
1063
|
+
Quests: const quest = new Objectives([{ id: "gather", target: 3 }, { id: "deliver", target: 1, after: "gather" }]). Resolve indexes once with quest.index(id); quest.record(i, amount) returns true when it completes; quest.active(i), quest.firstActive(), quest.progress[i], quest.allDone; quest.pollCompleted() drains completions (NO_OBJECTIVE when none) for toasts and achievements.
|
|
1064
|
+
|
|
1065
|
+
Waves and pools: const waves = new Waves([{ count: 4, interval: 0.5, delay: 2 }, { count: 8, interval: 0.3 }], { endless: true, growth: 2, waitForClear: true }); const enemies = new SlotPool(32). Each fixed step: const due = waves.step(game.loop.time, enemies.count); for (let i = 0; i < due; i++) { const slot = enemies.acquire(); if (slot !== NO_SLOT) spawnAt(slot); } and enemies.release(slot) when one dies. waves.wave, waves.waveSize(), waves.resting, waves.finished.
|
|
1066
|
+
|
|
1067
|
+
Score and timer HUD: const timer = new ScoreTimer({ seconds: 90, comboWindow: 1.5, maxCombo: 5 }) (seconds 0 is a stopwatch); const hud = new ScoreHud(game.ui, timer, { score: "score", clock: "time", combo: "combo" }). In fixedUpdate if (timer.step(game.loop.time)) endRound(); timer.add(points) applies the combo and returns the points awarded; in update hud.update(). timer.clockText() reads m:ss from a table built once; timer.state[SCORE], SCORE_BEST and SCORE_COMBO hold the numbers; timer.reset() keeps the best.
|
|
1068
|
+
|
|
1069
|
+
NPC daily schedules: const day = new DayClock(secondsPerDay, startHour); const npcs = new NpcSchedules(capacity, maxStops); const vendor = npcs.add({ x, z, speed, stops: [{ hour: 6, x: 4, z: 9, activity: SELLING }, { hour: 19, x: 0, z: 0, activity: SLEEPING }] }). Each fixed step day.step(game.loop.time) (true when a new day starts) then npcs.step(day, game.loop.time). Read npcs.positions (stride NPC_STRIDE, x at 0 and z at 2), npcs.facing, npcs.activities (NPC_WALKING while moving) and day.state[DAY_HOUR] to drive models, animation and dialogue.
|
|
1070
|
+
|
|
1071
|
+
Pickups and collectibles: const coins = new Pickups(capacity, radius); const keepOut = new KeepOut(n); keepOut.add(x, z, radius) around walls and goals. coins.scatter(slot, game.random, halfExtent, y, keepOut, kind) or coins.place(slot, x, y, z, kind); each step const got = coins.collect(position, offset) returns the slot collected or NO_PICKUP; coins.nearest(position, offset), coins.active, coins.positions (stride 3), coins.count. withinRadius(position, offset, cx, cz, radius) tests a circular zone such as a drop point.
|
|
1072
|
+
|
|
1073
|
+
Checkpoints and respawn: const route = new Checkpoints(capacity, { radius: 2, ordered: true, killY: -10 }); route.setSpawn(x, y, z); route.add(x, y, z) per gate. Each step route.touch(position, offset) returns the gate reached or NO_CHECKPOINT; if (route.fallen(position, offset)) { route.respawnInto(scratch); teleport there }. route.next(), route.reached, route.finished, route.reset().
|
|
1074
|
+
|
|
1075
|
+
Arena and bots: walledArena(game, { half, wallHeight }) builds a floor and four colliding walls, staticBox(game, x, y, z, hx, hy, hz) adds a static collider, daylight(game) adds sun and sky lights, a BoxBatch draws coloured boxes (write box[BOX_X..BOX_SZ], then add(rgb); no per-box call takes a float, so drawing allocates nothing). new SeekBot({ arrive, jumpEvery, jumpHold }).drive(game, rig, x, z, frame) walks a thirdPersonCharacter rig toward a point through the touch controls, which is all a playtest bot for the publication gates needs.
|
|
1076
|
+
|
|
1077
|
+
The courier template (courier.CourierPlaza) is the worked example: walledArena, staticBox crates, KeepOut, a one-slot Pickups parcel, withinRadius for the drop zone, BoxBatch drawing and a SeekBot playtest bot in about 150 lines.`
|
|
1078
|
+
}, TEMPLATES = {
|
|
1079
|
+
id: "templates",
|
|
1080
|
+
title: "Templates",
|
|
1081
|
+
summary: "cimplify create <name> --template <t>: blank, quiz, ordering, booking, racing, sport, board, tycoon, shootout, runner, five-a-side, ludo, word, danfo, courier, worlds, frontier, genesis, wasm-c, wasm-rust, godot.",
|
|
1082
|
+
body: `cimplify create <name> --template <t> scaffolds a project. Native profile: blank (empty GenUI Interactive), quiz (three questions), ordering (menu and total), booking (table booking). Canvas 2D: racing (hold to accelerate against five rivals), sport (kick to the goal), board. Game Kit scenes on the game API: tycoon (grow a jollof empire), shootout (five penalties against the keeper), runner (endless runner with coins), five-a-side (football against the computer), ludo (four players), word (five-letter word, streaks), danfo (drive a danfo round a city loop, stop at bus stops, carry passengers and collect fares, with traffic; it runs on primitives until cimplify add cimplify/danfo-bus brings the Workshop danfo model, which the scene then uses through takeDanfoArt(ctx.assets)), courier (third-person parcel delivery round a plaza built on thirdPersonCharacter; the smallest example of the controls kit, scene new courier.CourierPlaza()), worlds (an empty persistent world friends build in together: walk with the controls kit, toggle build mode, place, move, rotate, scale, duplicate, delete, undo and redo Workshop items with grid and surface snapping; offline it runs on a local loopback, online joinBuildersPlaza(ctx, { world_id, spawn: { kind: "default" } }) joins the world, see the worlds topic), frontier (an empty planet friends develop: gather, process materials by their properties, discover techs, invent with AI, build powered machines with conveyors and logic, farm and raise herds; see the frontier topic), genesis (play god on a world: shape nature, write a constitution in words that is read back from its rules, bless, curse, send prophets, plagues and earthquakes, and watch generations live; see the genesis topic). Engine profiles: wasm-c (C compiled to WebAssembly by Cimplify), wasm-rust (Rust to WebAssembly), godot (Godot 4 export).
|
|
1083
|
+
|
|
1084
|
+
Kit template scenes can be used directly (for example runCanvasGame(ctx, { scene: new runner.EndlessRunner() })) or copied into src and changed freely.`
|
|
1085
|
+
}, DANFO_EXAMPLE = {
|
|
1086
|
+
id: "danfo-example",
|
|
1087
|
+
title: "Worked example: a danfo driving game",
|
|
1088
|
+
summary: "Complete scene: ground, road loop from boxes, danfo vehicle, touch and keyboard controls, chase camera, bus stops and fares.",
|
|
1089
|
+
body: `interactive.toml needs [profile] kind = "canvas", game = "game_api", engine_features = ["physics", "audio"] and [entries] client = "src/client.ts".
|
|
1090
|
+
|
|
1091
|
+
import type { Ctx } from "@cimplify/sdk/interactives";
|
|
1092
|
+
import { boxGeometry, type ActionId, type Entity, type Game, type GameScene, type HudSlot, Quat, Vec3 } from "@cimplify/sdk/game";
|
|
1093
|
+
import { BoxBatch, BOX_SX, BOX_SY, BOX_SZ, BOX_X, BOX_Y, BOX_Z, type DriveControls, KitVehicle, vehiclePreset } from "@cimplify/sdk/game/kit";
|
|
1094
|
+
import { runCanvasGame } from "@cimplify/sdk/game/kit/canvas";
|
|
1095
|
+
|
|
1096
|
+
const LOOP_HALF_X = 40;
|
|
1097
|
+
const LOOP_HALF_Z = 25;
|
|
1098
|
+
const STOPS = [{ name: "Ridgeway Junction", x: 40, z: 0 }, { name: "Palm Market", x: 0, z: 25 }, { name: "Harbour Gate", x: -40, z: 0 }] as const;
|
|
1099
|
+
const STOP_RADIUS = 6;
|
|
1100
|
+
const FARE = 300;
|
|
1101
|
+
const CHASE_BACK = 9;
|
|
1102
|
+
const CHASE_UP = 4.5;
|
|
1103
|
+
|
|
1104
|
+
class Drive implements GameScene<Game> {
|
|
1105
|
+
readonly name = "drive";
|
|
1106
|
+
private body: Entity = 0 as Entity;
|
|
1107
|
+
private camera: Entity = 0 as Entity;
|
|
1108
|
+
private danfo: KitVehicle | null = null;
|
|
1109
|
+
private steer: ActionId = 0 as ActionId;
|
|
1110
|
+
private gas: ActionId = 0 as ActionId;
|
|
1111
|
+
private brake: ActionId = 0 as ActionId;
|
|
1112
|
+
private readonly drive: DriveControls = { throttle: 0, brake: 0, steer: 0, handbrake: 0 };
|
|
1113
|
+
private readonly at = new Vec3();
|
|
1114
|
+
private readonly turn = new Quat();
|
|
1115
|
+
private fares = 0;
|
|
1116
|
+
private lastStop = -1;
|
|
1117
|
+
private faresSlot: HudSlot | null = null;
|
|
1118
|
+
|
|
1119
|
+
enter(game: Game): void {
|
|
1120
|
+
const g = game.graphics;
|
|
1121
|
+
const ground = game.world.spawn();
|
|
1122
|
+
game.transforms.add(ground, 0, -0.5, 0);
|
|
1123
|
+
game.physics.addCollider(ground, { shape: { kind: "cuboid", halfExtents: [200, 0.5, 200] }, friction: 1 });
|
|
1124
|
+
const road = new BoxBatch(game, 8);
|
|
1125
|
+
road.begin();
|
|
1126
|
+
road.box[BOX_Y] = 0.01; road.box[BOX_SY] = 0.02;
|
|
1127
|
+
road.box[BOX_X] = 0; road.box[BOX_Z] = LOOP_HALF_Z; road.box[BOX_SX] = LOOP_HALF_X * 2 + 8; road.box[BOX_SZ] = 8; road.add(0x3a3a3a);
|
|
1128
|
+
road.box[BOX_Z] = -LOOP_HALF_Z; road.add(0x3a3a3a);
|
|
1129
|
+
road.box[BOX_Z] = 0; road.box[BOX_X] = LOOP_HALF_X; road.box[BOX_SX] = 8; road.box[BOX_SZ] = LOOP_HALF_Z * 2; road.add(0x3a3a3a);
|
|
1130
|
+
road.box[BOX_X] = -LOOP_HALF_X; road.add(0x3a3a3a);
|
|
1131
|
+
road.end();
|
|
1132
|
+
this.body = game.world.spawn();
|
|
1133
|
+
game.transforms.add(this.body, LOOP_HALF_X, 1.2, -10);
|
|
1134
|
+
this.danfo = KitVehicle.spawn(game.physics, this.body, vehiclePreset("danfo", "arcade"));
|
|
1135
|
+
const art = game.world.spawn();
|
|
1136
|
+
game.transforms.add(art, 0, 0, 0);
|
|
1137
|
+
game.transforms.setParent(art, this.body);
|
|
1138
|
+
g.mesh(art, g.createGeometry(boxGeometry(2.1, 2.2, 5.2)), g.createMaterial({ kind: "standard", color: 0xf5c400, roughness: 0.5 }));
|
|
1139
|
+
this.camera = game.world.spawn();
|
|
1140
|
+
game.transforms.add(this.camera, 0, CHASE_UP, 0);
|
|
1141
|
+
g.camera(this.camera, 60, 0.2, 500);
|
|
1142
|
+
const sun = game.world.spawn();
|
|
1143
|
+
game.transforms.add(sun, 30, 60, 20);
|
|
1144
|
+
g.create(sun, { kind: "light", light: "directional", intensity: 2.5, color: 0xffd6a0, castShadow: true });
|
|
1145
|
+
const sky = game.world.spawn();
|
|
1146
|
+
game.transforms.add(sky);
|
|
1147
|
+
g.create(sky, { kind: "light", light: "hemisphere", intensity: 0.6, color: 0xf2c9a0, groundColor: 0x4a3524 });
|
|
1148
|
+
this.steer = game.input.defineAxis("steer", [{ device: "keys", negative: "ArrowLeft", positive: "ArrowRight" }, { device: "keys", negative: "KeyA", positive: "KeyD" }, { device: "virtual", control: "steer" }]);
|
|
1149
|
+
this.gas = game.input.defineButton("gas", [{ device: "key", code: "ArrowUp" }, { device: "key", code: "KeyW" }, { device: "virtual", control: "gas" }]);
|
|
1150
|
+
this.brake = game.input.defineButton("brake", [{ device: "key", code: "ArrowDown" }, { device: "key", code: "KeyS" }, { device: "virtual", control: "brake" }]);
|
|
1151
|
+
this.faresSlot = game.ui.hudNumber("fares");
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
fixedUpdate(game: Game): void {
|
|
1155
|
+
const danfo = this.danfo;
|
|
1156
|
+
if (danfo === null) return;
|
|
1157
|
+
this.drive.steer = game.input.value(this.steer);
|
|
1158
|
+
this.drive.throttle = game.input.value(this.gas);
|
|
1159
|
+
this.drive.brake = game.input.value(this.brake);
|
|
1160
|
+
danfo.drive(this.drive, game.stepSeconds);
|
|
1161
|
+
game.transforms.position(this.body, this.at);
|
|
1162
|
+
for (let i = 0; i < STOPS.length; i++) {
|
|
1163
|
+
const dx = this.at.x - STOPS[i].x;
|
|
1164
|
+
const dz = this.at.z - STOPS[i].z;
|
|
1165
|
+
if (i !== this.lastStop && dx * dx + dz * dz < STOP_RADIUS * STOP_RADIUS && Math.abs(danfo.vehicle.speed) < 1) {
|
|
1166
|
+
this.lastStop = i;
|
|
1167
|
+
this.fares += FARE;
|
|
1168
|
+
if (this.faresSlot !== null) game.ui.setNumber(this.faresSlot, this.fares);
|
|
1169
|
+
game.ui.haptic("success");
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
update(game: Game): void {
|
|
1175
|
+
const q = game.transforms.rotation(this.body, this.turn);
|
|
1176
|
+
const yaw = Math.atan2(2 * (q.x * q.z + q.w * q.y), 1 - 2 * (q.x * q.x + q.y * q.y));
|
|
1177
|
+
game.transforms.position(this.body, this.at);
|
|
1178
|
+
game.transforms.setPosition(this.camera, this.at.x - Math.sin(yaw) * CHASE_BACK, this.at.y + CHASE_UP, this.at.z - Math.cos(yaw) * CHASE_BACK);
|
|
1179
|
+
const half = (yaw + Math.PI) / 2;
|
|
1180
|
+
game.transforms.setRotation(this.camera, 0, Math.sin(half), 0, Math.cos(half));
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
export default async function main(ctx: Ctx): Promise<void> {
|
|
1185
|
+
await runCanvasGame(ctx, { scene: new Drive() });
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
Next steps a creator usually asks for: traffic on the loop with game.nav.loadRoads and setExternalVehicle (nav topic), the Workshop danfo model in place of the box (workshop topic), passengers that board and alight at later stops, a horn on Space with game.audio, touch pads that call game.input.virtualEvent, golden-hour light with setTimeOfDay and bloom, and a leaderboard of the day's fares.`
|
|
1189
|
+
}, WORLDS = {
|
|
1190
|
+
id: "worlds",
|
|
1191
|
+
title: "Worlds: persistent places friends build in",
|
|
1192
|
+
summary: "ctx.create_world/join_world, roles, invites, moderation, copies, WorldBuilder ops with snapping and undo, the worlds template.",
|
|
1193
|
+
body: `A world is a persistent place on the tick service. Its objects, settings, roles and bans are saved and come back after every restart. Each world runs as copies of up to its copy capacity (50 players by default, at most 98); new players fill the busiest copy with room, and every copy shows the same builds.
|
|
1194
|
+
|
|
1195
|
+
Create one (the creator becomes the owner):
|
|
1196
|
+
|
|
1197
|
+
const world = await ctx.create_world({
|
|
1198
|
+
request_key: "my-first-world",
|
|
1199
|
+
pack: { pack_id: "cimplify/empty-world", generation: null },
|
|
1200
|
+
rules_entry: null,
|
|
1201
|
+
capacity_tier: "small",
|
|
1202
|
+
lifecycle: { kind: "persistent" },
|
|
1203
|
+
access: "friends",
|
|
1204
|
+
name: "Harbour of Okun",
|
|
1205
|
+
template: { sky: "sunset", ground: "grass", lighting: "golden_hour" },
|
|
1206
|
+
rating: { violence: "none", sexuality: "none", language: "mild", fear: "none", simulated_gambling: false },
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
access is "private" (members and invites only), "friends" (the owner's friends), "unlisted" or "public". World names can be real or invented: a world may be named after a real city or country, built on real map geometry, or mix real and inspired parts. Map data from OpenStreetMap keeps its attribution. Real businesses appear only when their owner opts in, and replicas of real landmarks are not placed.
|
|
1210
|
+
|
|
1211
|
+
Roles are visitor (walk and chat), builder (build and edit their own objects), co_builder (edit anyone's objects), moderator (kick, mute, ban and remove objects) and owner (everything, plus settings and roles). world.invite({ role, expires_in_ms }) returns a token; a friend joins with ctx.join_world({ world_id, spawn: { kind: "default" }, invite: token }). world.set_role({ participant_id, role }) changes a member; world.moderate({ participant_id, sanction: "kick" | "mute" | "unmute" | "ban" | "unban" | "block", duration_s }) applies safety actions; "block" hides the two players from each other in the world. Players can always report with ctx.report and leave. ctx.list_worlds({ page: { limit: 20 } }) lists the caller's worlds.
|
|
1212
|
+
|
|
1213
|
+
Building goes through the world's tick room with WorldBuilder and WorldMirror from @cimplify/sdk/interactives (worlds namespace). connectWorld(session.realtime) returns { client, mirror, builder }; call client.start(now) once and client.update(now) every frame. Every op returns an op id and the server answers with a result in mirror.resultFor(opId) (status applied, denied, invalid, conflict, limit, unlicensed, nothing, full):
|
|
1214
|
+
|
|
1215
|
+
builder.place(item, worlds.SNAP_GRID | worlds.SNAP_SURFACE, transform);
|
|
1216
|
+
builder.select(worlds.SelectMode.Replace, ids, count);
|
|
1217
|
+
builder.transformSelection(snap, dxMm, dyMm, dzMm, rotationQuat, scalePermille);
|
|
1218
|
+
builder.duplicate(dxMm, dyMm, dzMm); builder.deleteSelection(); builder.undo(); builder.redo();
|
|
1219
|
+
builder.settings({ sky, ground, lighting, visibility, gridMm, angleStepCentidegrees });
|
|
1220
|
+
builder.shareCopy(); builder.joinCopy(code);
|
|
1221
|
+
|
|
1222
|
+
Transforms are Float64Array(10): position x, y, z in millimetres (0 to 255,996 on x and z), quaternion x, y, z, w scaled by 32767 (worlds.yawRotation fills a yaw), scale x, y, z in permille. item is the index into the world's palette, which is the Interactive's licensed [[content]] Workshop items in manifest order. Grid snapping rounds x and z to the world grid and yaw to the angle step; surface snapping rests the object on whatever is under it; builder.preview(item, snap, transform, ignore, count) shows the same result before sending. Selecting locks objects so co-builders never overwrite each other: another builder's selection or edit makes your op or undo report a conflict instead of clobbering. shareCopy returns a code in mirror.lastSharedCode that a friend passes to joinCopy to move into your copy.
|
|
1223
|
+
|
|
1224
|
+
The worlds template (scene new worlds.BuildersPlaza() from @cimplify/sdk/game/kit/worlds) is an empty world friends build in: a toolbar of ui actions (worlds.WORLD_ACTIONS: build, item, place, select_near, select_add, clear, move, rotate, scale_up, scale_down, duplicate, delete, undo, redo, grid, surface) driven by game.ui.dispatch. It runs offline on a LocalWorld loopback; online use await worlds.joinBuildersPlaza(ctx, { world_id, spawn: { kind: "default" } }).`
|
|
1225
|
+
}, FRONTIER = {
|
|
1226
|
+
id: "frontier",
|
|
1227
|
+
title: "Frontier: an empty planet friends develop",
|
|
1228
|
+
summary: "The frontier template, the planet room, gathering, processing by material properties, the discovered tech tree, machines, power, conveyors and logic, farms, herds, weather and AI-assisted inventions.",
|
|
1229
|
+
body: `The frontier template starts friends on an empty planet that runs on the server and keeps running while everyone is away. The simulation is authoritative on the tick service; the game is a client that sends commands and draws a mirror of the planet.
|
|
1230
|
+
|
|
1231
|
+
The template's interactive.toml declares [world] simulation = "frontier" (with world in [capabilities]), so worlds created with ctx.create_world run as Frontier planets on the tick service. Join it from main(ctx): const session = await frontier.joinFrontierPlanet(ctx, { world_id, spawn: { kind: "last_location" } }, onInvent); then runCanvasGame(ctx, { scene: session.scene, onFrame(game, now) { session.onFrame(now); } }). Without a real-time connection the scene opens offline on an empty mirror. Import frontier from @cimplify/sdk/game/kit or the @cimplify/sdk/game/kit/frontier entry.
|
|
1232
|
+
|
|
1233
|
+
Commands go through scene.commands (FrontierCommands): survey(region, x, z, tool), gather(deposit, amount), craft(invention, count), learn(invention), place(invention, region, x, z), insert(machine, lane, slot, count), take(machine, count), till(region, x, z), harvest(plot), invent(runId), and send(command) for every other command (process, link, unlink, connect, logic, enable, plant, water, fertilise, tame, feed, collect, teach, drop, remove). Regions are regionKey(face, band, i, j), the same key the terrain uses for chunks. The scene's mirror (FrontierMirror) holds machines (flags alive, working, starved, blocked; lanes; progress), plots, herds, inventories and events such as surveyHit, techDiscovered, inventionAccepted, inventionRejected and refused with a reason code.
|
|
1234
|
+
|
|
1235
|
+
How the planet works. Deposits (ores, stone, wood, water, soil, clay, sand, oil, quartz, diamond, wild crops) are seeded per region by biome and depth, deplete when gathered and regrow where they are renewable; rain refills springs and reservoirs. Materials have properties (hardness, density, melting point, conductivity, flammability, energy, strength, elasticity and more); processing follows them: heat dries, chars, sinters clay to ceramic, melts sand to glass and smelts ore with a fuel reducer; press makes plates or crushes brittle things; cut needs a harder tool; combine alloys metals or makes mortar and rope; spin makes fibre. The tech tree is discovered by doing (reach 1,100 K for pottery, make an ingot for smelting, pure copper for electricity) and players can teach friends. Machines are inventions: kilns, furnaces, generators, drills, presses, saws, pumps, sprinklers, harvesters, chests, conveyors, sensors, logic gates, batteries and solar panels; generators and batteries share power along connections, conveyors move items between machines, logic gates enable machines from counts, power or signals. Crops need water, fertility and the right temperature; herds graze, drink, breed when fed and give milk, wool or eggs; weather brings rain, drought and frost.
|
|
1236
|
+
|
|
1237
|
+
Inventions. Declare the prompt with frontier.frontierAiToml() in interactive.toml ([capabilities] ai plus [ai] with the frontier_invent prompt and the frontier_invention schema). When a player describes an invention, call frontier.requestInvention(ctx.ai, { description, materials, techs, requestKey }) and pass the returned run id to scene.commands.invent(run). The room fetches the model's output from the run log, and the planet's rules validate it (known materials, discovered techs, conservation of mass and energy, physical limits such as heating below the body's melting point, balance caps). Accepted inventions become learnable for everyone on the planet; a refusal arrives as an inventionRejected event with the reason.`
|
|
1238
|
+
}, GENESIS = {
|
|
1239
|
+
id: "genesis",
|
|
1240
|
+
title: "Genesis: play god on a world you write",
|
|
1241
|
+
summary: "The genesis template, the world room, nature as data, constitutions in plain language read back from rules, god decrees, populations that live, believe and die, faiths that spread, and AI chronicles.",
|
|
1242
|
+
body: `The genesis template starts a god on a world that runs on the server and keeps living while everyone is away: settlements of people who are born, partner, work, believe, migrate and die; faiths that spread between neighbours and split; harvests that follow the climate; laws that tax, grant rights and change birth and death rates. The simulation is authoritative on the tick service; the game is a client that sends decrees and draws a mirror of the world.
|
|
1243
|
+
|
|
1244
|
+
The template's interactive.toml declares [world] simulation = "genesis" (with world in [capabilities]), so every world created with ctx.create_world runs as a Genesis room on the tick service; "frontier" does the same for Frontier planets and "build" (the default) for building worlds. Join it from main(ctx): const session = await genesis.joinGenesisWorld(ctx, { world_id, spawn: { kind: "default" } }, { onLaw(scene) {...}, onChronicle(scene) {...} }); then runCanvasGame(ctx, { scene: session.scene, onFrame(game, now) { session.onFrame(now); } }). Import genesis from @cimplify/sdk/game/kit or the @cimplify/sdk/game/kit/genesis entry. Offline the scene opens on an empty mirror.
|
|
1245
|
+
|
|
1246
|
+
Decrees go through scene.commands.decree({ target, power }). Targets: { kind: "world" }, { kind: "settlement", settlement }, { kind: "polity", polity }, { kind: "faith", faith }, { kind: "filter", filter }, { kind: "person", person } (never a player), { kind: "births", settlement, count } (the next count children born there). Powers: weather (tempC10 offset, rainPermille multiplier, months), terrain (raise, lower, crater, flatten, cone), bless and curse (death and birth rate multipliers in Q16 for months), disaster (earthquake, flood, volcano, drought, storm, meteor with magnitude 1 to 10), plague (betaQ16, lethalityQ16, months), miracle (food, devotion), prophet (founds a faith with a prophet), fate on births (gift bits, faith, destiny such as genesis.DESTINY.foundFaith), found (a new settlement with founders), law (a whole constitution) and nature (a new nature revision: gravity, day and year length, tilt, atmosphere, climate, species). Out-of-envelope nature (gravity over 50 m/s2, time scale outside 0.1x to 4x, a day under one real minute, air over 50 times sea level) is refused with a reason. Other controls: scene.commands.interest(settlement, genesis.TIER.active) to bring a town to individual detail, time(monthsPerMinute, paused), era(fromMonth, toMonth), follow(person).
|
|
1247
|
+
|
|
1248
|
+
Constitutions. Declare the prompts with genesis.genesisAiToml() ([capabilities] ai plus the constitution_draft prompt with the genesis_constitution schema and the chronicle_era prompt). When the god writes a law in words, call genesis.requestConstitution(ctx.ai, { words, world: scene.worldContext(), requestKey }) and pass the run id to scene.commands.draft(run, polity). The room parses the model's rules, validates them (known faiths and places, cost, no law may single out one player) and sends a review (scene.mirror.review) whose lines are rendered from the rules themselves, never from the model's words; show them, and on accept call scene.commands.enact(review). Chronicles: build the facts slot with genesis.factsText(seed, scene.mirror.facts, s => scene.mirror.name[s]) and call genesis.requestChronicle(ctx.ai, { facts, realm, requestKey }); the text is display only.
|
|
1249
|
+
|
|
1250
|
+
A constitution draft that validates is also forecast on the server off the tick thread: scene.mirror.forecast holds, per year, the population, births, deaths, food security and wealth with and without the change, so show it beside the read-back before enacting. Offline or for solo worlds on a device, genesis.LocalGenesis runs the same world in the engine (engine feature genesis_1): open, stepMonths, advance, interest, decree, indicators, settlements, active, save.
|
|
1251
|
+
|
|
1252
|
+
genesis.nameOf(seed, packed) generates names from each culture's sounds. genesis.screenName refuses real religions, deities and scriptures; real place names are allowed. Never use a real faith or public figure in a Genesis world.`
|
|
1253
|
+
}, CHEMISTRY = {
|
|
1254
|
+
id: "chemistry",
|
|
1255
|
+
title: "Chemistry: mix, heat, burn, dissolve, smelt and invent",
|
|
1256
|
+
summary: "game.chemistry: materials and reactions as data, a deterministic cell simulation for 2D grids and voxel volumes, vessels for machines, particles for fluids, burning liquids (game.reactiveFluid: a pour on a hot pan that boils, ignites and fills a media volume with flame and smoke), events, player and ctx.ai reaction proposals checked by conservation, and the chemistry sandbox template.",
|
|
1257
|
+
body: `game.chemistry is a game-grade chemistry engine that runs natively in the engine (feature chemistry_1). It is plausible rather than exact, deterministic on every device and on the server, and allocates nothing per tick.
|
|
1258
|
+
|
|
1259
|
+
Materials. Every world starts with about ninety materials: air (N2, O2, Ar), water, steam and ice from one material, CO2, CO, hydrogen, methane, propane, ethylene, wood, sawdust, charcoal, oil, plastic, sugar, ethanol, yeast, sulfur, salts and acids (NaCl, NaOH, HCl, H2SO4 and their dissolved forms such as "NaCl(aq)"), metals (Fe, Cu, Sn, Pb, Zn, Al, Mg, Na, Au, Ag, Ti, steel, bronze), ores (hematite, malachite, cassiterite, galena, Al2O3), limestone, lime, sand, glass, rock and gravel, gunpowder and TNT. Look them up with game.chemistry.material("H2O") (a numeric id) and materialInfo(id) (molar mass, melt and boil points, ignition, groups, colours, density). Phase is never stored: a material melts, boils or freezes when its heat says so, with latent heat, so ice takes time to melt and boiling water stays at 373 K.
|
|
1260
|
+
|
|
1261
|
+
Reactions. About eighty reactions ship: burning (wood, charcoal, oil, gases, metals), charring, rust (needs water), acids and bases, metals in acid, fizzing limestone, precipitation, the lime cycle and mortar, smelting iron, copper, tin and lead, electrolysis of water, brine and alumina (needs power), a Daniell cell that makes electricity, the Haber process with an iron catalyst, polymerisation, cracking, fermentation with yeast, glass, steel, bronze, gunpowder and TNT. Every soluble salt dissolves and precipitates by its solubility, and every carbon fuel burns, by rule. Rates follow Arrhenius: nothing happens below a reaction's temperature, then it speeds up as things get hotter.
|
|
1262
|
+
|
|
1263
|
+
Regions. const region = game.chemistry.region({ width: 160, height: 120, open: true }) makes a 2D grid (add depth for a voxel volume; open lets air in and heat out at the edges). Each cell is a small mixture of up to four materials. region.fill("air" | "vacuum" | id, { x0, y0, x1, y1 }, kelvin), region.paint(id, x, y), region.set(x, y, z, [[id, parcels], ...], kelvin), region.heatBox(box, joules) (negative cools), region.powerBox(box, joules) (electricity for electrolysis), region.step(ticks) in fixedUpdate. Powders pile, liquids flow and spread, hot gas rises, gas pressure equalises, rain and ash fall, bubbles rise, explosions fracture rock into gravel. Draw with region.colours() (one RGBA value per cell, glowing with temperature) into a pixel texture; read region.temperatureQ4 (1/16 K), region.species(slot), region.amounts(slot) and region.info as zero-copy views; region.probe(x, y) for one cell. Quiet areas sleep and wake when something changes, so big mostly-still worlds are cheap.
|
|
1264
|
+
|
|
1265
|
+
Events. region.watchReaction(game.chemistry.reaction("thermite")), region.watchMaterial(id) for phase changes, region.threshold(box, kelvin) for crossing a temperature, then region.events(e => ...) once per step: e.kind is "reaction" (code is the reaction, value the amount), "explosion" (value is the pressure in kPa, feed it to destruction and camera shake), "threshold", "phase" or "fracture", with e.x, e.y, e.z. region.fired(id) counts a reaction this tick.
|
|
1266
|
+
|
|
1267
|
+
Machines. const furnace = game.chemistry.vessel(8) is a well-mixed container: add(id, parcels, kelvin), heat(joules), power(joules) (returns electricity a battery made), step(), take(id, parcels), vent(maxAtm), state() (temperature, pressure, contents). Fluids: game.chemistry.particles(capacity) runs heat and reactions over a fluid solver's neighbour pairs and reports phase events so a liquid particle can become a gas.
|
|
1268
|
+
|
|
1269
|
+
Inventing. Players and the model can add materials and reactions. game.chemistry.define(text, { proposal: true }) takes lines like "species skyfuel formula=C2H6 hf=-84000 boil=185:14700 groups=fuel" and "reaction skyfuel_cracks: skyfuel@g -> C2H4 + H2 | ea=150000 rate=1100:1000 above=900"; it returns { ok: true } or { ok: false, line, refusal, reason }. The rules refuse anything that breaks conservation: atoms must balance, the stated energy must match formation enthalpies, a battery cannot give out more than the reaction releases, and the world's policy caps energy per parcel and can switch off kinds of chemistry such as explosives. With ctx.ai declare chemistry.chemistryAiSection() in interactive.toml and call chemistry.proposeReaction(game.chemistry, ctx.ai, { description, requestKey }); the model's answer is checked the same way.
|
|
1270
|
+
|
|
1271
|
+
Burning liquids. game.reactiveFluid(desc) couples a deterministic particle liquid to a chemistry volume: every fluid particle carries a little of a material, swaps heat with the voxel it sits in and with a pan under it, boils off as gas when hot enough, and the gas burns in the volume. Flames and smoke come out as a media volume and up to eight plumes (the hottest gas cells) for flame and smoke emitters. A flamb\xE9:
|
|
1272
|
+
|
|
1273
|
+
const pan = game.reactiveFluid({
|
|
1274
|
+
liquid: "C2H5OH",
|
|
1275
|
+
fluid: { origin: { x: 0.032, y: 0.012, z: 0.032 }, cell: 0.014, cells: [20, 20, 20], capacity: 4096 },
|
|
1276
|
+
volume: { origin: { x: 0, y: 0, z: 0 }, cell: 0.02, cells: [20, 24, 20] },
|
|
1277
|
+
smoke: ["CO2", "H2O"],
|
|
1278
|
+
});
|
|
1279
|
+
pan.hob({ first: [3, 3], last: [16, 16], metal: "Fe", panK: 760, panJoules: 64000, flameK: 1300 });
|
|
1280
|
+
pan.preheat(740);
|
|
1281
|
+
const fire = game.graphics.mediaVolume(pan.mediumDesc());
|
|
1282
|
+
pan.attach(fire);
|
|
1283
|
+
|
|
1284
|
+
Pour from fixedUpdate: pan.pour({ kind: "stream", position, radius: 0.02, velocity: { x: 0, y: -0.8, z: 0 }, rate: 1500 }) every step while pouring and pan.pour(null) to stop. fluid is the particle box (origin, cell and 8 to 192 cells per axis), volume the chemistry voxels (up to 64 per axis, so a media volume can mirror it). hob paints a metal pan over cells first to last on the floor and a burner ring of hot cells round its rim; it is a thermostat (pan to panK, ring to flameK) and setLit(false) turns it off. The game steps every reactive fluid in its fixed step: the particles move every step and the chemistry ticks every framesPerTick steps (default 2, so 30 Hz at 60 Hz; the reactive fluid opens its own chemistry at that rate unless you pass chemistry). On each tick an attached media volume is rewritten in place; read pan.plumeCount and pan.plume(i, out) (cell, kelvin, smoke and world position) to place flame and smoke emitters, pan.stats(out) for particles, burning cells and the pan temperature, pan.positions() for the particles (x, y, z each) to draw droplets, and pan.digest() for a replay check. Everything is integer chemistry and a deterministic CPU fluid, so the same inputs give the same flames on every device; nothing allocates per step. Reactive fluids need the native engine (feature reactive_fluid_1, added when your code calls reactiveFluid); elsewhere game.reactiveFluid throws unsupported_feature. Bad cells, materials or energies throw invalid_argument or unknown_resource.
|
|
1285
|
+
|
|
1286
|
+
Nature. createGame({ ..., nature: { ambientK: 210, pressureAtm: 0.01, gravity: 0.38, atmosphere: "mars" } }) changes the world: water boils lower at low pressure, fires need the oxygen the air actually has, and things fall slower.
|
|
1287
|
+
|
|
1288
|
+
Template: cimplify create my-lab --template chemistry is a falling-sand sandbox (chemistry.ChemistrySandbox): tap the top-left strip to change the tool (sand, water, wood, oil, salt, iron, rust, aluminium, charcoal, limestone, acid, sodium, TNT, gunpowder, fire, frost, spark, erase) and the top-right strip to invent a reaction with ctx.ai.`
|
|
1289
|
+
}, HOT_SWAP = {
|
|
1290
|
+
id: "hot-swap",
|
|
1291
|
+
title: "Changing code while the game runs",
|
|
1292
|
+
summary: "Export hot and call acceptHot so edits swap scenes and systems in place, keeping the world, its entities and their state.",
|
|
1293
|
+
body: `Export the scenes and systems as hot and accept them once the game exists. An edit then swaps them in place: the running scene stays entered, entities, components, physics and tables keep their state, and only module-level variables start fresh.
|
|
1294
|
+
|
|
1295
|
+
import type { Ctx } from "@cimplify/sdk/interactives";
|
|
1296
|
+
import { acceptHot, type Game, type GameScene, type GameSystem, type HotModule } from "@cimplify/sdk/game";
|
|
1297
|
+
import { runCanvasGame } from "@cimplify/sdk/game/kit/canvas";
|
|
1298
|
+
|
|
1299
|
+
const play: GameScene<Game> = { name: "play", fixedUpdate(game) {} };
|
|
1300
|
+
const spin: GameSystem<Game> = { fixedUpdate(game) {} };
|
|
1301
|
+
let score = 0;
|
|
1302
|
+
export const hot: HotModule<Game> = {
|
|
1303
|
+
scenes: [play],
|
|
1304
|
+
systems: [spin],
|
|
1305
|
+
keep: () => score,
|
|
1306
|
+
restore: (kept) => { score = kept as number; },
|
|
1307
|
+
assets: (paths) => {},
|
|
1308
|
+
};
|
|
1309
|
+
export default async function main(ctx: Ctx): Promise<void> {
|
|
1310
|
+
const game = await runCanvasGame(ctx, { scene: play });
|
|
1311
|
+
acceptHot(game.scenes, hot);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
Components keep their identity across a swap when their name and fields stay the same; a component whose fields changed starts with an empty store. Changing the engine or the package's libraries restarts the game instead.`
|
|
1315
|
+
}, AUTHORING_TOPICS = [
|
|
1316
|
+
GAME_LOOP,
|
|
1317
|
+
HOT_SWAP,
|
|
1318
|
+
ENTITY_TABLES,
|
|
1319
|
+
KERNELS,
|
|
1320
|
+
KERNEL_MATH,
|
|
1321
|
+
INPUT,
|
|
1322
|
+
CONTROLS,
|
|
1323
|
+
GRAPHICS,
|
|
1324
|
+
PHYSICS,
|
|
1325
|
+
VEHICLES,
|
|
1326
|
+
NAV,
|
|
1327
|
+
CRAFT,
|
|
1328
|
+
WORLD,
|
|
1329
|
+
SCENERY,
|
|
1330
|
+
PREFETCH,
|
|
1331
|
+
TERRAIN,
|
|
1332
|
+
FLUIDS,
|
|
1333
|
+
DESTRUCTION,
|
|
1334
|
+
ATMOSPHERE,
|
|
1335
|
+
AUDIO,
|
|
1336
|
+
CAMERA,
|
|
1337
|
+
GAME_AI,
|
|
1338
|
+
CTX_AI,
|
|
1339
|
+
MULTIPLAYER,
|
|
1340
|
+
TICK_ROOMS,
|
|
1341
|
+
PROGRESSION,
|
|
1342
|
+
WORKSHOP,
|
|
1343
|
+
BLOCKS,
|
|
1344
|
+
TEMPLATES,
|
|
1345
|
+
WORLDS,
|
|
1346
|
+
FRONTIER,
|
|
1347
|
+
GENESIS,
|
|
1348
|
+
CHEMISTRY,
|
|
1349
|
+
DANFO_EXAMPLE,
|
|
1350
|
+
THREEJS_PORT_TOPIC
|
|
1351
|
+
];
|
|
1352
|
+
|
|
1353
|
+
// src/interactives/authoring/guide.ts
|
|
1354
|
+
var TOPICS_BY_ID = new Map(AUTHORING_TOPICS.map((t) => [t.id, t]));
|
|
1355
|
+
function authoringTopic(id) {
|
|
1356
|
+
return TOPICS_BY_ID.get(id) ?? null;
|
|
1357
|
+
}
|
|
1358
|
+
function render(topic) {
|
|
1359
|
+
return `## ${topic.title} (${topic.id})
|
|
1360
|
+
|
|
1361
|
+
${topic.body}`;
|
|
1362
|
+
}
|
|
1363
|
+
function authoringGuide(ids) {
|
|
1364
|
+
if (ids === void 0) return [AUTHORING_RULES, ...AUTHORING_TOPICS.map(render)].join(`
|
|
1365
|
+
|
|
1366
|
+
`);
|
|
1367
|
+
let parts = [], unknown = [], seen = /* @__PURE__ */ new Set();
|
|
1368
|
+
for (let id of ids) {
|
|
1369
|
+
if (seen.has(id)) continue;
|
|
1370
|
+
seen.add(id);
|
|
1371
|
+
let topic = authoringTopic(id);
|
|
1372
|
+
topic === null ? unknown.push(id) : parts.push(render(topic));
|
|
1373
|
+
}
|
|
1374
|
+
return unknown.length > 0 && parts.push(`Unknown topics: ${unknown.join(", ")}. Known topics: ${AUTHORING_TOPICS.map((t) => t.id).join(", ")}.`), parts.join(`
|
|
1375
|
+
|
|
1376
|
+
`);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/interactives/authoring/lines.ts
|
|
1380
|
+
var SOURCE_ROOT = "src/", GAME_SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs"], EXCLUDED_SUFFIXES = [".d.ts", ".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
1381
|
+
var EMPTY_GAME_LINES = { total: 0, files: [] };
|
|
1382
|
+
function isSpace(code) {
|
|
1383
|
+
return code === 32 || code === 9 || code === 13 || code === 12 || code === 11 || code === 65279;
|
|
1384
|
+
}
|
|
1385
|
+
function countSourceLines(source) {
|
|
1386
|
+
let lines = 0, code = false, block = false, quote = 0, length = source.length;
|
|
1387
|
+
for (let i = 0; i < length; i++) {
|
|
1388
|
+
let c = source.charCodeAt(i);
|
|
1389
|
+
if (c === 10) {
|
|
1390
|
+
code && lines++, code = false, quote !== 96 && (quote = 0);
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
if (block) {
|
|
1394
|
+
c === 42 && source.charCodeAt(i + 1) === 47 && (block = false, i++);
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
if (quote !== 0) {
|
|
1398
|
+
isSpace(c) || (code = true), c === 92 ? i++ : c === quote && (quote = 0);
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (c === 47) {
|
|
1402
|
+
let next = source.charCodeAt(i + 1);
|
|
1403
|
+
if (next === 47) {
|
|
1404
|
+
for (; i + 1 < length && source.charCodeAt(i + 1) !== 10; ) i++;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (next === 42) {
|
|
1408
|
+
block = true, i++;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
isSpace(c) || (code = true, (c === 39 || c === 34 || c === 96) && (quote = c));
|
|
1413
|
+
}
|
|
1414
|
+
return code && lines++, lines;
|
|
1415
|
+
}
|
|
1416
|
+
function isGameSourcePath(path) {
|
|
1417
|
+
if (!path.startsWith(SOURCE_ROOT)) return false;
|
|
1418
|
+
let lower = path.toLowerCase();
|
|
1419
|
+
return EXCLUDED_SUFFIXES.some((suffix) => lower.endsWith(suffix)) ? false : GAME_SOURCE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
1420
|
+
}
|
|
1421
|
+
function countGameLines(files) {
|
|
1422
|
+
let counted = [], total = 0;
|
|
1423
|
+
for (let file of files) {
|
|
1424
|
+
let lines = countSourceLines(file.source);
|
|
1425
|
+
counted.push({ path: file.path, lines }), total += lines;
|
|
1426
|
+
}
|
|
1427
|
+
return counted.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0), { total, files: counted };
|
|
1428
|
+
}
|
|
1429
|
+
async function measureGameLines(project) {
|
|
1430
|
+
let files = [];
|
|
1431
|
+
for (let path of await project.list()) {
|
|
1432
|
+
if (!isGameSourcePath(path)) continue;
|
|
1433
|
+
let source = await project.read(path);
|
|
1434
|
+
source !== null && files.push({ path, source });
|
|
1435
|
+
}
|
|
1436
|
+
return countGameLines(files);
|
|
1437
|
+
}
|
|
1438
|
+
var TEMPLATE_LINE_CEILINGS = {
|
|
1439
|
+
courier: 153,
|
|
1440
|
+
"danfo-route": 688,
|
|
1441
|
+
"endless-runner": 412,
|
|
1442
|
+
"five-a-side": 659,
|
|
1443
|
+
"game-ui-showcase": 700,
|
|
1444
|
+
"jollof-tycoon": 365,
|
|
1445
|
+
ludo: 660,
|
|
1446
|
+
"penalty-shootout": 492,
|
|
1447
|
+
"word-puzzle": 242,
|
|
1448
|
+
worlds: 519
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// src/interactives/authoring/turn.ts
|
|
1452
|
+
var DEFAULT_MAX_STEPS = 40, DEFAULT_REPAIR_ROUNDS = 3, MAX_READ_BYTES = 2e5, MAX_WRITE_BYTES = 4e5, MAX_DIAGNOSTICS_SHOWN = 30, CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".json"], ASSET_TEXT_EXTENSIONS = [".json", ".txt", ".md", ".csv", ".svg", ".wgsl", ".glsl"], MANIFEST = "interactive.toml", AUTHORING_TOOLS = [
|
|
1453
|
+
{
|
|
1454
|
+
name: "list_files",
|
|
1455
|
+
description: "List every file in the project (paths relative to the project root).",
|
|
1456
|
+
input_schema: { type: "object", properties: {}, additionalProperties: false }
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
name: "read_file",
|
|
1460
|
+
description: "Read one text file from the project.",
|
|
1461
|
+
input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false }
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: "write_file",
|
|
1465
|
+
description: "Create or replace a whole file. Allowed: interactive.toml, src/** and tests/** (.ts, .tsx, .js, .mjs, .json), assets/** text files (.json, .txt, .md, .csv, .svg, .wgsl, .glsl). Never cimplify.lock, package.json or node_modules.",
|
|
1466
|
+
input_schema: {
|
|
1467
|
+
type: "object",
|
|
1468
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
1469
|
+
required: ["path", "content"],
|
|
1470
|
+
additionalProperties: false
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
name: "delete_file",
|
|
1475
|
+
description: "Delete a file you are allowed to write.",
|
|
1476
|
+
input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false }
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
name: "read_guide",
|
|
1480
|
+
description: "Read Cimplify Interactives guide topics by id before using an API you have not seen in this project.",
|
|
1481
|
+
input_schema: {
|
|
1482
|
+
type: "object",
|
|
1483
|
+
properties: { topics: { type: "array", items: { type: "string", enum: AUTHORING_TOPICS.map((t) => t.id) } } },
|
|
1484
|
+
required: ["topics"],
|
|
1485
|
+
additionalProperties: false
|
|
1486
|
+
}
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
name: "check",
|
|
1490
|
+
description: "Build and type-check the project the way Cimplify admission does and return the diagnostics.",
|
|
1491
|
+
input_schema: { type: "object", properties: {}, additionalProperties: false }
|
|
1492
|
+
}
|
|
1493
|
+
];
|
|
1494
|
+
function authoringSystemPrompt() {
|
|
1495
|
+
let index = AUTHORING_TOPICS.map((t) => `- ${t.id}: ${t.title}. ${t.summary}`).join(`
|
|
1496
|
+
`);
|
|
1497
|
+
return `You are Reesa, Cimplify's game and Interactive builder. You change a creator's Interactive project to do what they ask, then make sure it builds. Work directly on the files with the tools: read what you need, write whole files, and call check when you are done editing. Keep the creator's existing work unless they ask to replace it. When you finish, reply with two or three plain sentences saying what changed and what to try in the preview; mention any Workshop items the creator should add with cimplify add.
|
|
1498
|
+
|
|
1499
|
+
${AUTHORING_RULES}
|
|
1500
|
+
|
|
1501
|
+
Guide topics (read with read_guide):
|
|
1502
|
+
${index}`;
|
|
1503
|
+
}
|
|
1504
|
+
function normalizeProjectPath(raw) {
|
|
1505
|
+
if (typeof raw != "string" || raw.length === 0 || raw.length > 512 || raw.includes("\\") || raw.includes("\0") || raw.startsWith("/")) return null;
|
|
1506
|
+
let parts = raw.split("/").filter((p) => p.length > 0 && p !== ".");
|
|
1507
|
+
return parts.length === 0 || parts.some((p) => p === "..") ? null : parts.join("/");
|
|
1508
|
+
}
|
|
1509
|
+
function hasExtension(path, extensions) {
|
|
1510
|
+
let lower = path.toLowerCase();
|
|
1511
|
+
return extensions.some((e) => lower.endsWith(e));
|
|
1512
|
+
}
|
|
1513
|
+
function writablePath(raw) {
|
|
1514
|
+
let path = normalizeProjectPath(raw);
|
|
1515
|
+
return path === null ? null : path === MANIFEST || (path.startsWith("src/") || path.startsWith("tests/")) && hasExtension(path, CODE_EXTENSIONS) || path.startsWith("assets/") && hasExtension(path, ASSET_TEXT_EXTENSIONS) ? path : null;
|
|
1516
|
+
}
|
|
1517
|
+
function readablePath(raw) {
|
|
1518
|
+
let path = normalizeProjectPath(raw);
|
|
1519
|
+
if (path === null) return null;
|
|
1520
|
+
let first = path.split("/")[0];
|
|
1521
|
+
return first === "node_modules" || first === ".cimplify" || first === ".git" ? null : path;
|
|
1522
|
+
}
|
|
1523
|
+
function stringInput(input, key) {
|
|
1524
|
+
let value = input[key];
|
|
1525
|
+
return typeof value == "string" ? value : null;
|
|
1526
|
+
}
|
|
1527
|
+
function formatDiagnostics(diagnostics) {
|
|
1528
|
+
let shown = diagnostics.slice(0, (30)).map((d) => `${d.file === null ? "" : d.line === null ? `${d.file}: ` : `${d.file}:${d.line}: `}${d.severity} ${d.code}: ${d.message}`);
|
|
1529
|
+
return diagnostics.length > (30) && shown.push(`and ${diagnostics.length - (30)} more`), shown.join(`
|
|
1530
|
+
`);
|
|
1531
|
+
}
|
|
1532
|
+
function errorsOf(diagnostics) {
|
|
1533
|
+
return diagnostics.filter((d) => d.severity === "error");
|
|
1534
|
+
}
|
|
1535
|
+
var TurnState = class {
|
|
1536
|
+
constructor() {
|
|
1537
|
+
this.changed = /* @__PURE__ */ new Map();
|
|
1538
|
+
this.dirty = false;
|
|
1539
|
+
this.lastCheck = null;
|
|
1540
|
+
this.checks = 0;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
async function runTool(call, options, state) {
|
|
1544
|
+
let fail = (content) => ({ id: call.id, content, error: true }), ok = (content) => ({ id: call.id, content, error: false }), project = options.project, emit = (path) => options.onEvent?.({ kind: "tool", name: call.name, path });
|
|
1545
|
+
switch (call.name) {
|
|
1546
|
+
case "list_files": {
|
|
1547
|
+
emit(null);
|
|
1548
|
+
let files = (await project.list()).filter((f) => readablePath(f) !== null);
|
|
1549
|
+
return ok(files.join(`
|
|
1550
|
+
`));
|
|
1551
|
+
}
|
|
1552
|
+
case "read_file": {
|
|
1553
|
+
let path = readablePath(stringInput(call.input, "path") ?? "");
|
|
1554
|
+
if (path === null) return fail("that path is outside the project or not readable");
|
|
1555
|
+
emit(path);
|
|
1556
|
+
let text = await project.read(path);
|
|
1557
|
+
return text === null ? fail(`${path} does not exist`) : text.length > (2e5) ? ok(`${text.slice(0, (2e5))}
|
|
1558
|
+
[truncated at ${(2e5)} characters]`) : ok(text);
|
|
1559
|
+
}
|
|
1560
|
+
case "write_file": {
|
|
1561
|
+
let raw = stringInput(call.input, "path") ?? "", path = writablePath(raw), content = stringInput(call.input, "content");
|
|
1562
|
+
return path === null ? fail(`${raw} is not a path you may write; allowed: interactive.toml, src/**, tests/**, assets/** text files`) : content === null ? fail("content must be a string") : content.length > (4e5) ? fail(`content is over ${(4e5)} characters; split it into modules`) : (emit(path), await project.write(path, content), state.changed.set(path, "write"), state.dirty = true, ok(`wrote ${path}`));
|
|
1563
|
+
}
|
|
1564
|
+
case "delete_file": {
|
|
1565
|
+
let raw = stringInput(call.input, "path") ?? "", path = writablePath(raw);
|
|
1566
|
+
return path === null || path === MANIFEST ? fail(`${raw} is not a file you may delete`) : (emit(path), await project.remove(path) ? (state.changed.set(path, "delete"), state.dirty = true, ok(`deleted ${path}`)) : fail(`${path} does not exist`));
|
|
1567
|
+
}
|
|
1568
|
+
case "read_guide": {
|
|
1569
|
+
let topics = call.input.topics, ids = Array.isArray(topics) ? topics.filter((t) => typeof t == "string") : [];
|
|
1570
|
+
return emit(null), ok(authoringGuide(ids));
|
|
1571
|
+
}
|
|
1572
|
+
case "check": {
|
|
1573
|
+
emit(null);
|
|
1574
|
+
let diagnostics = await options.check();
|
|
1575
|
+
return state.lastCheck = diagnostics, state.dirty = false, state.checks++, options.onEvent?.({ kind: "check", errors: errorsOf(diagnostics).length, round: state.checks }), ok(diagnostics.length === 0 ? "the project builds with no diagnostics" : formatDiagnostics(diagnostics));
|
|
1576
|
+
}
|
|
1577
|
+
default:
|
|
1578
|
+
return fail(`unknown tool ${call.name}`);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function gameLinesOf(project) {
|
|
1582
|
+
try {
|
|
1583
|
+
return await measureGameLines(project);
|
|
1584
|
+
} catch {
|
|
1585
|
+
return EMPTY_GAME_LINES;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
async function runAuthoringTurn(options) {
|
|
1589
|
+
let linesBefore = await gameLinesOf(options.project), maxSteps = options.maxSteps ?? (40), repairRounds = options.repairRounds ?? (3), messages = [...options.history ?? [], { role: "user", text: options.request }], system = authoringSystemPrompt(), state = new TurnState(), summary = "", repairs = 0, end = "steps";
|
|
1590
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
1591
|
+
let reply = await options.model.send({ system, messages, tools: AUTHORING_TOOLS });
|
|
1592
|
+
if (messages.push({ role: "assistant", reply }), reply.text.length > 0 && (summary = reply.text, options.onEvent?.({ kind: "text", text: reply.text })), reply.stop === "refused") {
|
|
1593
|
+
end = "refused";
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
if (reply.calls.length > 0) {
|
|
1597
|
+
let results = [];
|
|
1598
|
+
for (let call of reply.calls)
|
|
1599
|
+
try {
|
|
1600
|
+
results.push(await runTool(call, options, state));
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
results.push({ id: call.id, content: error instanceof Error ? error.message : String(error), error: true });
|
|
1603
|
+
}
|
|
1604
|
+
messages.push({ role: "tool_results", results });
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (reply.stop === "truncated") {
|
|
1608
|
+
end = "truncated";
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
if (state.changed.size === 0) {
|
|
1612
|
+
end = "done";
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
if (state.dirty || state.lastCheck === null) {
|
|
1616
|
+
let diagnostics = await options.check();
|
|
1617
|
+
state.lastCheck = diagnostics, state.dirty = false, state.checks++, options.onEvent?.({ kind: "check", errors: errorsOf(diagnostics).length, round: state.checks });
|
|
1618
|
+
}
|
|
1619
|
+
let errors = errorsOf(state.lastCheck ?? []);
|
|
1620
|
+
if (errors.length === 0 || repairs >= repairRounds) {
|
|
1621
|
+
end = "done";
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
repairs++, messages.push({
|
|
1625
|
+
role: "user",
|
|
1626
|
+
text: `The build failed with these errors. Fix them, keep what the creator asked for, and check again:
|
|
1627
|
+
${formatDiagnostics(errors)}`
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
if (state.dirty) {
|
|
1631
|
+
let diagnostics = await options.check();
|
|
1632
|
+
state.lastCheck = diagnostics, state.checks++, options.onEvent?.({ kind: "check", errors: errorsOf(diagnostics).length, round: state.checks });
|
|
1633
|
+
}
|
|
1634
|
+
let changed = [...state.changed.entries()].map(([path, kind]) => ({ path, kind })), lines = state.changed.size === 0 ? linesBefore : await gameLinesOf(options.project);
|
|
1635
|
+
return { changed, summary, diagnostics: errorsOf(state.lastCheck ?? []), repairs, end, messages, lines, lineDelta: lines.total - linesBefore.total };
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/interactives/authoring/anthropic.ts
|
|
1639
|
+
var ANTHROPIC_AUTHORING_MODEL = "claude-opus-5-5", API_VERSION = "2023-06-01", FALLBACK_BETA = "server-side-fallback-2026-07-01", DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
1640
|
+
var DEFAULT_EFFORT = "high";
|
|
1641
|
+
function wireMessages(messages) {
|
|
1642
|
+
return messages.map((m) => {
|
|
1643
|
+
switch (m.role) {
|
|
1644
|
+
case "user":
|
|
1645
|
+
return { role: "user", content: m.text };
|
|
1646
|
+
case "assistant":
|
|
1647
|
+
return { role: "assistant", content: m.reply.echo };
|
|
1648
|
+
case "tool_results":
|
|
1649
|
+
return {
|
|
1650
|
+
role: "user",
|
|
1651
|
+
content: m.results.map((r) => ({ type: "tool_result", tool_use_id: r.id, content: r.content, is_error: r.error }))
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function stopOf(reason, calls) {
|
|
1657
|
+
return reason === "refusal" ? "refused" : calls > 0 ? "tools" : reason === "max_tokens" ? "truncated" : "done";
|
|
1658
|
+
}
|
|
1659
|
+
function asRecord(value) {
|
|
1660
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) ? value : {};
|
|
1661
|
+
}
|
|
1662
|
+
function parseAnthropicReply(body) {
|
|
1663
|
+
let content = body.content ?? [], texts = [], calls = [];
|
|
1664
|
+
for (let block of content)
|
|
1665
|
+
block.type === "text" && typeof block.text == "string" ? texts.push(block.text) : block.type === "tool_use" && typeof block.id == "string" && typeof block.name == "string" && calls.push({ id: block.id, name: block.name, input: asRecord(block.input) });
|
|
1666
|
+
return { text: texts.join(`
|
|
1667
|
+
`).trim(), calls, stop: stopOf(body.stop_reason, calls.length), echo: content };
|
|
1668
|
+
}
|
|
1669
|
+
function anthropicAuthoringModel(options) {
|
|
1670
|
+
let doFetch = options.fetch ?? fetch, url = `${(options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "")}/v1/messages`, fallbacks = options.fallbacks ?? true;
|
|
1671
|
+
return {
|
|
1672
|
+
async send(request) {
|
|
1673
|
+
let headers = {
|
|
1674
|
+
"content-type": "application/json",
|
|
1675
|
+
"x-api-key": options.apiKey,
|
|
1676
|
+
"anthropic-version": API_VERSION
|
|
1677
|
+
};
|
|
1678
|
+
fallbacks && (headers["anthropic-beta"] = FALLBACK_BETA);
|
|
1679
|
+
let body = {
|
|
1680
|
+
model: options.model ?? ANTHROPIC_AUTHORING_MODEL,
|
|
1681
|
+
max_tokens: options.maxTokens ?? 16e3,
|
|
1682
|
+
output_config: { effort: options.effort ?? DEFAULT_EFFORT },
|
|
1683
|
+
system: request.system,
|
|
1684
|
+
tools: request.tools,
|
|
1685
|
+
messages: wireMessages(request.messages)
|
|
1686
|
+
};
|
|
1687
|
+
fallbacks && (body.fallbacks = "default");
|
|
1688
|
+
let response = await doFetch(url, { method: "POST", headers, body: JSON.stringify(body) }), text = await response.text();
|
|
1689
|
+
if (!response.ok) {
|
|
1690
|
+
let detail = text.slice(0, 500);
|
|
1691
|
+
try {
|
|
1692
|
+
let parsed = JSON.parse(text);
|
|
1693
|
+
parsed.error?.message && (detail = parsed.error.message);
|
|
1694
|
+
} catch {
|
|
1695
|
+
detail = text.slice(0, 500);
|
|
1696
|
+
}
|
|
1697
|
+
throw new Error(`the model request failed with HTTP ${response.status}: ${detail}`);
|
|
1698
|
+
}
|
|
1699
|
+
return parseAnthropicReply(JSON.parse(text));
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
export { ANTHROPIC_AUTHORING_MODEL, AUTHORING_RULES, AUTHORING_TOOLS, AUTHORING_TOPICS, DEFAULT_MAX_STEPS, DEFAULT_REPAIR_ROUNDS, EMPTY_GAME_LINES, MAX_READ_BYTES, MAX_WRITE_BYTES, TEMPLATE_LINE_CEILINGS, THREEJS_PORT_MAP, THREEJS_PORT_TOPIC, THREEJS_PORT_TOPIC_ID, THREE_CONTROLS, THREE_GEOMETRIES, THREE_LIGHTS, THREE_MATERIALS, THREE_MATERIAL_PROPERTIES, THREE_MATH_HELPERS, THREE_SIDES, THREE_TEXTURE_SLOTS, THREE_VECTOR_METHODS, anthropicAuthoringModel, authoringGuide, authoringSystemPrompt, authoringTopic, countGameLines, countSourceLines, formatDiagnostics, isGameSourcePath, measureGameLines, normalizeProjectPath, parseAnthropicReply, readablePath, runAuthoringTurn, threejsPortGuide, threejsPortRequest, writablePath };
|