$
This commit is contained in:
1
core.cm
1
core.cm
@@ -775,7 +775,6 @@ return {
|
|||||||
clear: clear,
|
clear: clear,
|
||||||
|
|
||||||
// Internal (for runner)
|
// Internal (for runner)
|
||||||
_state: _state,
|
|
||||||
_begin_frame: _begin_frame,
|
_begin_frame: _begin_frame,
|
||||||
_process_events: _process_events,
|
_process_events: _process_events,
|
||||||
_end_frame: _end_frame
|
_end_frame: _end_frame
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function _update(dt) {
|
|||||||
|
|
||||||
// Exit on escape
|
// Exit on escape
|
||||||
if (lance3d._state.keys_held['escape']) {
|
if (lance3d._state.keys_held['escape']) {
|
||||||
$_.stop()
|
$stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ function frame() {
|
|||||||
// Process events
|
// Process events
|
||||||
if (!lance3d._process_events()) {
|
if (!lance3d._process_events()) {
|
||||||
log.console("Exiting...")
|
log.console("Exiting...")
|
||||||
$_.stop()
|
$stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ function frame() {
|
|||||||
lance3d._end_frame()
|
lance3d._end_frame()
|
||||||
|
|
||||||
// Schedule next frame
|
// Schedule next frame
|
||||||
$_.delay(frame, 1/60)
|
$delay(frame, 1/60)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ function _init() {
|
|||||||
|
|
||||||
function _update(dt) {
|
function _update(dt) {
|
||||||
if (lance3d.key('escape')) {
|
if (lance3d.key('escape')) {
|
||||||
$_.stop()
|
$stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ function frame() {
|
|||||||
|
|
||||||
if (!lance3d._process_events()) {
|
if (!lance3d._process_events()) {
|
||||||
log.console("Exiting...")
|
log.console("Exiting...")
|
||||||
$_.stop()
|
$stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ function frame() {
|
|||||||
|
|
||||||
lance3d._end_frame()
|
lance3d._end_frame()
|
||||||
|
|
||||||
$_.delay(frame, 1/60)
|
$delay(frame, 1/60)
|
||||||
}
|
}
|
||||||
|
|
||||||
_init()
|
_init()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function _init() {
|
|||||||
model = lance3d.load_model(model_path)
|
model = lance3d.load_model(model_path)
|
||||||
if (!model) {
|
if (!model) {
|
||||||
log.console("Error: Could not load model: " + model_path)
|
log.console("Error: Could not load model: " + model_path)
|
||||||
$_.stop()
|
$stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ function _update(dt) {
|
|||||||
|
|
||||||
// Exit on escape
|
// Exit on escape
|
||||||
if (lance3d.key('escape')) {
|
if (lance3d.key('escape')) {
|
||||||
$_.stop()
|
$stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle animation with space
|
// Toggle animation with space
|
||||||
@@ -213,7 +213,7 @@ function frame() {
|
|||||||
// Process events
|
// Process events
|
||||||
if (!lance3d._process_events()) {
|
if (!lance3d._process_events()) {
|
||||||
log.console("Exiting...")
|
log.console("Exiting...")
|
||||||
$_.stop()
|
$stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ function frame() {
|
|||||||
lance3d._end_frame()
|
lance3d._end_frame()
|
||||||
|
|
||||||
// Schedule next frame
|
// Schedule next frame
|
||||||
$_.delay(frame, 1/240)
|
$delay(frame, 1/240)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ var skin_mod = use('skin')
|
|||||||
var _backend = null
|
var _backend = null
|
||||||
|
|
||||||
// Texture original data storage for re-resizing on style change
|
// Texture original data storage for re-resizing on style change
|
||||||
var TEX_ORIGINAL = key("texture_original")
|
var TEX_ORIGINAL = "resources:texture_original"
|
||||||
|
|
||||||
// Default material prototype
|
// Default material prototype
|
||||||
var _default_material = {
|
var _default_material = {
|
||||||
|
|||||||
@@ -1,122 +1,566 @@
|
|||||||
# Lance 3D
|
☾
|
||||||
|
|
||||||
# Core API
|
# Lance 3D Manual (v1)
|
||||||
Set style ps1 / n64 / saturn
|
|
||||||
|
|
||||||
set_style("ps1" | "n64" | "saturn")
|
Lance 3D is a focused 3D fantasy-console engine for making games with a PS1 / N64 / Saturn kind of visual personality. It’s immediate-mode: you load some assets, set a camera, draw every frame, ship.
|
||||||
|
|
||||||
Get time since game boot, in seconds **time()**
|
Game code is written in **CellScript** (not JavaScript):
|
||||||
|
|
||||||
Get runtime statistics
|
|
||||||
|
|
||||||
stat(name: "draw_calls" | "triangles" | "fps" | "memory_bytes")
|
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://cell-lang.org/cellscript/
|
||||||
```
|
```
|
||||||
set_lighting({
|
|
||||||
sun_dir: [0.3,-1,0.2], // normalized internally
|
Networking is **v2**.
|
||||||
sun_color: [1,1,1], // rgb
|
|
||||||
ambient: [0.25,0.25,0.25] // rgb
|
---
|
||||||
|
|
||||||
|
## Specifications
|
||||||
|
|
||||||
|
* **Target FPS:** 60
|
||||||
|
* **VSync:** always on
|
||||||
|
* **Internal render size:** 320×240 (always)
|
||||||
|
* **Windowing:** letterboxed into whatever window size you choose
|
||||||
|
* **Screen coordinates:** origin at **bottom-left**
|
||||||
|
|
||||||
|
* Pixel addressable range: **x: 0..319**, **y: 0..239**
|
||||||
|
* Floats are allowed (subpixel placement), but the result is still “pixel-ish”
|
||||||
|
* **3D coordinates:** right-handed, **Z-up**
|
||||||
|
* **Units:** 1 unit = 1 meter
|
||||||
|
* **Angles:** radians (`pi` exists)
|
||||||
|
|
||||||
|
Assets:
|
||||||
|
|
||||||
|
* Models: `.glb`
|
||||||
|
* Textures: `.png` / `.jpg`
|
||||||
|
* Sounds: `.wav` / `.mp3` (converted to 16-bit PCM @ 44.1 kHz)
|
||||||
|
* Music: `.mid` / `.midi` via the embedded chip
|
||||||
|
|
||||||
|
Voices (maximum simultaneously):
|
||||||
|
|
||||||
|
* PS1: **24**
|
||||||
|
* N64: **24**
|
||||||
|
* Saturn: **32**
|
||||||
|
|
||||||
|
Voice cap includes *everything*: `play_sound()` voices + voices produced by the MIDI/chip.
|
||||||
|
|
||||||
|
Hot reload:
|
||||||
|
|
||||||
|
* The engine watches and reloads changed **models** and **textures** during development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The big rule: immediate mode
|
||||||
|
|
||||||
|
Lance 3D does not keep a “scene.” It draws what you ask it to draw **this frame**.
|
||||||
|
|
||||||
|
If you want:
|
||||||
|
|
||||||
|
* a scene tree → you build it
|
||||||
|
* a global “current material” → you build it
|
||||||
|
* an entity system → you build it
|
||||||
|
|
||||||
|
Lance 3D mostly retains **assets**, not **render intent**.
|
||||||
|
|
||||||
|
### What resets each frame
|
||||||
|
|
||||||
|
At the start of each `draw()` call, Lance 3D resets:
|
||||||
|
|
||||||
|
* camera
|
||||||
|
* lighting
|
||||||
|
* fog
|
||||||
|
* 2D clip/scissor
|
||||||
|
|
||||||
|
So if you want those, you set them in `draw()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
A Lance 3D “game” is a folder. Asset paths are **absolute-from-game-root**, and you **omit extensions**.
|
||||||
|
|
||||||
|
* `"man/model"` loads `"man/model.glb"`
|
||||||
|
* `"tex/brick"` loads `"tex/brick.png"` or `"tex/brick.jpg"`
|
||||||
|
* `"sfx/hit"` loads `"sfx/hit.wav"` or `"sfx/hit.mp3"`
|
||||||
|
|
||||||
|
Top-level code runs once at startup: load assets, create meshes, initialize state.
|
||||||
|
|
||||||
|
### The main loop
|
||||||
|
|
||||||
|
You don’t write the loop. You register callbacks.
|
||||||
|
|
||||||
|
```cell
|
||||||
|
var t = 0
|
||||||
|
|
||||||
|
register({
|
||||||
|
update(dt) { t += dt },
|
||||||
|
draw() { /* draw every frame */ }
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
set_fog({
|
* `update(dt)` once per frame, `dt` in seconds
|
||||||
enabled: false,
|
* `draw()` once per frame
|
||||||
color: [0.5,0.6,0.7],
|
|
||||||
near: 10,
|
|
||||||
far: 80
|
|
||||||
})
|
|
||||||
|
|
||||||
// lance3d material
|
---
|
||||||
{
|
|
||||||
color_map: texture | null, // optional
|
## Hello cube
|
||||||
paint: color, // required (default: white)
|
|
||||||
coverage: "opaque" | "cutoff" | "blend",
|
```cell
|
||||||
face: "single" | "double",
|
def cube = make_cube(1, 1, 1)
|
||||||
lamp: "lit" | "unlit"
|
def tex = load_texture("tex/brick")
|
||||||
|
|
||||||
|
def mat = {
|
||||||
|
color_map: tex,
|
||||||
|
paint: [1, 1, 1, 1],
|
||||||
|
coverage: "opaque",
|
||||||
|
face: "single",
|
||||||
|
lamp: "lit"
|
||||||
}
|
}
|
||||||
|
|
||||||
## Draw API
|
var a = 0
|
||||||
Loads a gltf model, returning an array of {mesh, material}, with materials compressed into a render3d material
|
|
||||||
load_model(path) -> array<{mesh, material}>
|
|
||||||
|
|
||||||
make_cube(w, h, d) -> mesh
|
register({
|
||||||
make_sphere(r, segments=12) -> mesh
|
update(dt) { a += dt },
|
||||||
make_cylinder(r, h, segments=12) -> mesh
|
|
||||||
make_plane(w, h) -> mesh
|
|
||||||
|
|
||||||
Gets information about the animations on a model
|
draw() {
|
||||||
anim_info(model)
|
set_style("ps1")
|
||||||
|
|
||||||
Generates a pose that can be applied to a model
|
camera_perspective(pi / 3, 0.1, 100)
|
||||||
sample_pose(model, name, time)
|
camera_look_at(3, 3, 2, 0, 0, 0)
|
||||||
|
|
||||||
Draws a model with a given base transform
|
set_lighting({
|
||||||
draw_model(model, transform=null, pose=null)
|
sun_dir: [0.3, -1, 0.2],
|
||||||
|
sun_color: [1, 1, 1],
|
||||||
|
ambient: [0.25, 0.25, 0.25]
|
||||||
|
})
|
||||||
|
|
||||||
Draws a mesh with a material
|
def trs = trs_matrix(0, 0, 0, 0, 0, a, 1, 1, 1)
|
||||||
draw_mesh(mesh, transform=null, material=null)
|
draw_mesh(cube, trs, mat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
load_sound(path) -> sound | null
|
If you see loud purple, you drew without a material. That’s intentional.
|
||||||
|
|
||||||
play_sound(sound, opts={
|
---
|
||||||
volume: 1.0, // 0..1
|
|
||||||
pitch: 1.0, // 1.0 = normal
|
|
||||||
pan: 0.0, // -1 = left, 0 = center, 1 = right
|
|
||||||
loop: false // boolean
|
|
||||||
}) -> voice
|
|
||||||
|
|
||||||
stop_sound(voice)
|
## Time, logging, stats
|
||||||
|
|
||||||
// when drawing, a provided material will override defaults for the system
|
> **`time()` → number**
|
||||||
load_texture(path) -> texture
|
> Seconds since boot.
|
||||||
draw_billboard(texture, x, y, z, size=1.0, mat=null)
|
|
||||||
draw_sprite(texture, x, y, size=1.0, mat=null)
|
|
||||||
|
|
||||||
## Camera API
|
> **`log(...args)` → void**
|
||||||
camera_look_at(
|
> Print to the log.
|
||||||
ex, ey, ez, // eye
|
|
||||||
tx, ty, tz, // target
|
|
||||||
upx = 0, upy = 1, upz = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
camera_perspective(fov_deg=60, near=0.1, far=1000)
|
> **`stat(name)` → number**
|
||||||
camera_ortho(left, right, bottom, top, near=-1, far=1)
|
> `name`: `"draw_calls" | "triangles" | "fps" | "memory_bytes"`
|
||||||
|
> Throws if unknown.
|
||||||
|
|
||||||
## Collision API
|
---
|
||||||
add_collider_sphere(transform, radius, opts={layer_mask:1, user:null}) -> collider
|
|
||||||
add_collider_box(transform, sx, sy, sz, opts={layer_mask:1, user:null}) -> collider
|
|
||||||
|
|
||||||
overlaps(layer_mask_a=null, layer_mask_b=null) -> array<{a: collider, b: collider}>
|
## Style (PS1 / N64 / Saturn)
|
||||||
|
|
||||||
raycast(ox,oy,oz, dx,dy,dz, opts={
|
Style is global and persists until changed.
|
||||||
max_dist: Infinity,
|
|
||||||
layer_mask: 0xFFFFFFFF
|
|
||||||
}) -> null | {x,y,z, nx,ny,nz, distance, collider}
|
|
||||||
|
|
||||||
retro3d.remove_collider(collider)
|
> **`set_style(style = "ps1")` → void**
|
||||||
|
> `style`: `"ps1" | "n64" | "saturn"`
|
||||||
|
|
||||||
|
Triangle limits are soft: warnings, not failures.
|
||||||
|
|
||||||
|
### Texture & model tiers (low / normal / hero)
|
||||||
|
|
||||||
|
Textures and models are treated as belonging to a tier and interpreted through the current style.
|
||||||
|
|
||||||
|
| Style | Low | Normal | Hero |
|
||||||
|
| ------ | --- | ------ | ---- |
|
||||||
|
| PS1 | 64 | 128 | 256 |
|
||||||
|
| N64 | — | 32 | 64 |
|
||||||
|
| Saturn | 32 | 64 | 128 |
|
||||||
|
|
||||||
|
Defaults:
|
||||||
|
|
||||||
|
* **Textures:** `normal`
|
||||||
|
* **Models:** `normal`
|
||||||
|
|
||||||
|
You can tag assets:
|
||||||
|
|
||||||
|
> **`tag_texture(texture, tier = "normal")` → void**
|
||||||
|
> **`tag_model(model, tier = "normal")` → void**
|
||||||
|
> `tier`: `"low" | "normal" | "hero"`
|
||||||
|
|
||||||
|
Tags are hints to the style system (budget + interpretation). They don’t invent LODs for you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment (camera, lighting, fog)
|
||||||
|
|
||||||
|
These reset each `draw()`. Set them in `draw()`.
|
||||||
|
|
||||||
|
### Camera
|
||||||
|
|
||||||
|
> **`camera_look_at(ex, ey, ez, tx, ty, tz, upx = 0, upy = 0, upz = 1)` → void**
|
||||||
|
|
||||||
|
> **`camera_perspective(fov = pi/3, near = 0.1, far = 1000)` → void**
|
||||||
|
> FOV is radians. Throws if planes are invalid.
|
||||||
|
|
||||||
|
> **`camera_ortho(left, right, bottom, top, near = -1, far = 1)` → void**
|
||||||
|
|
||||||
|
### Lighting
|
||||||
|
|
||||||
|
> **`set_lighting({ sun_dir, sun_color, ambient })` → void**
|
||||||
|
> All colors are 0..1. `sun_dir` is normalized internally.
|
||||||
|
|
||||||
|
### Fog
|
||||||
|
|
||||||
|
> **`set_fog({ enabled=false, color, near, far })` → void**
|
||||||
|
> Fog is a first-class style tool.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Transforms
|
||||||
|
|
||||||
|
Transforms are opaque TRS blobs created by `trs_matrix(...)`. Functions accept a transform blob or `null` for identity.
|
||||||
|
|
||||||
|
> **`trs_matrix(tx, ty, tz, rx, ry, rz, sx = 1, sy = 1, sz = 1)` → transform**
|
||||||
|
|
||||||
|
* translation in meters
|
||||||
|
* rotation is Euler **radians**, applied in **XYZ order**
|
||||||
|
* scale is unitless multiplier
|
||||||
|
|
||||||
|
(If you want quaternions, build your own helper and still feed the resulting matrix/blob shape the engine expects.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Materials
|
||||||
|
|
||||||
|
Materials are plain objects with a fixed shape. No shaders.
|
||||||
|
|
||||||
|
```cell
|
||||||
|
def brick = {
|
||||||
|
color_map: load_texture("tex/brick"),
|
||||||
|
paint: [1, 1, 1, 1],
|
||||||
|
coverage: "opaque",
|
||||||
|
face: "single",
|
||||||
|
lamp: "lit"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
|
||||||
|
* `color_map: texture | null`
|
||||||
|
* `paint: [r,g,b,a]` (0..1)
|
||||||
|
* `coverage: "opaque" | "cutoff" | "blend"`
|
||||||
|
|
||||||
|
* `"cutoff"` uses alpha threshold **0.5**
|
||||||
|
* `face: "single" | "double"`
|
||||||
|
|
||||||
|
* Front faces are **clockwise (CW)**
|
||||||
|
* `lamp: "lit" | "unlit"`
|
||||||
|
|
||||||
|
Transparency:
|
||||||
|
|
||||||
|
* `"blend"` draws are depth-sorted back-to-front (simple, stable, not perfect).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assets and paths
|
||||||
|
|
||||||
|
Loads are synchronous. Missing assets return `null` and log details.
|
||||||
|
|
||||||
|
> **`load_texture(path)` → texture | null**
|
||||||
|
> **`load_model(path)` → model | null**
|
||||||
|
> **`load_sound(path)` → sound | null**
|
||||||
|
> **`load_font(path)` → font | null**
|
||||||
|
|
||||||
|
Asset handles are stable by path:
|
||||||
|
|
||||||
|
* calling `load_texture("tex/brick")` again returns the same handle
|
||||||
|
* hot reload updates the content behind that handle in development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Drawing order
|
||||||
|
|
||||||
|
1. 3D world (meshes, models, billboards)
|
||||||
|
2. 2D overlay (sprites, text, 2D primitives)
|
||||||
|
|
||||||
|
**2D always draws over 3D.**
|
||||||
|
To get “sprites in 3D,” use **billboards**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Drawing 3D
|
||||||
|
|
||||||
|
> **`draw_model(model, transform = null, pose = null)` → void**
|
||||||
|
> Uses embedded model materials.
|
||||||
|
|
||||||
|
> **`draw_mesh(mesh, transform = null, material = null)` → void**
|
||||||
|
> If `material=null`, uses the missing-material purple.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Billboards (world space)
|
||||||
|
|
||||||
|
Billboards face the camera and are anchored at **bottom-center**.
|
||||||
|
|
||||||
|
> **`draw_billboard(texture, x, y, z, size = 1.0, material = null)` → void**
|
||||||
|
|
||||||
|
`size` is in **meters** (height). Aspect ratio comes from the texture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2D drawing (PICO-8 style)
|
||||||
|
|
||||||
|
Everything here is screen space (320×240), anchored at bottom-left, drawn over 3D.
|
||||||
|
|
||||||
|
### Sprites
|
||||||
|
|
||||||
|
> **`draw_sprite(texture, x, y, size = 1.0, material = null)` → void**
|
||||||
|
|
||||||
|
`size` is a pixel scale:
|
||||||
|
|
||||||
|
* `size=1.0` draws at the texture’s authored pixel size in 320×240 space
|
||||||
|
* `size=2.0` doubles it, etc.
|
||||||
|
|
||||||
|
### Text
|
||||||
|
|
||||||
|
Lance 3D ships with a built-in default font. You can also pass a font per draw.
|
||||||
|
|
||||||
|
> **`draw_text(text, x, y, scale = 1.0, color = [1,1,1,1], font = null)` → void**
|
||||||
|
|
||||||
|
* `\n` creates a new line
|
||||||
|
* unknown glyphs render as `?`
|
||||||
|
* `font=null` uses the built-in font
|
||||||
|
|
||||||
|
### Simple primitives
|
||||||
|
|
||||||
|
> **`pset(x, y, color = [1,1,1,1])` → void**
|
||||||
|
> **`line2(x0, y0, x1, y1, color = [1,1,1,1])` → void**
|
||||||
|
> **`rect(x, y, w, h, color = [1,1,1,1])` → void**
|
||||||
|
> **`rectfill(x, y, w, h, color = [1,1,1,1])` → void**
|
||||||
|
> **`circ(x, y, r, color = [1,1,1,1])` → void**
|
||||||
|
> **`circfill(x, y, r, color = [1,1,1,1])` → void**
|
||||||
|
> **`clip(x, y, w, h)` → void** (enable)
|
||||||
|
> **`clip()` → void** (disable; also resets each `draw()`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Procedural geometry
|
||||||
|
|
||||||
|
> **`make_cube(w = 1, h = 1, d = 1)` → mesh**
|
||||||
|
> **`make_sphere(r = 1, segments = 12)` → mesh**
|
||||||
|
> **`make_cylinder(r = 1, h = 1, segments = 12)` → mesh**
|
||||||
|
> **`make_plane(w = 1, h = 1)` → mesh**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Animation
|
||||||
|
|
||||||
|
> **`anim_info(model)` → any**
|
||||||
|
> Structured info about available animations.
|
||||||
|
|
||||||
|
> **`sample_pose(model, name, t)` → pose | null**
|
||||||
|
> `t` is seconds. Nothing advances unless you advance it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
Stereo output. Converted to 16-bit PCM @ 44.1kHz.
|
||||||
|
|
||||||
|
> **`play_sound(sound, { volume=1, pitch=1, pan=0, loop=false })` → voice**
|
||||||
|
> **`stop_sound(voice)` → void**
|
||||||
|
|
||||||
|
Embedded chip / MIDI:
|
||||||
|
|
||||||
|
> **`chip(channel, op, value)` → void**
|
||||||
|
> Examples:
|
||||||
|
|
||||||
|
* `chip(0, "play_midi", "music/theme")`
|
||||||
|
* `chip(0, "volume", 120)`
|
||||||
|
* `chip(2, "mute", 1)`
|
||||||
|
|
||||||
|
Voice stealing (when cap is hit) is deterministic:
|
||||||
|
|
||||||
|
* lowest priority loses first (chip voices are lowest by default)
|
||||||
|
* ties: oldest voice loses
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Collision (v1: simple queries)
|
||||||
|
|
||||||
|
Collision in v1 is **stateless queries**. No collider world, no layers, no retained physics state.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
Use plain objects:
|
||||||
|
|
||||||
|
```cell
|
||||||
|
def ray = { origin: [ox, oy, oz], dir: [dx, dy, dz] } // dir should be normalized
|
||||||
|
def box = { min: [minx, miny, minz], max: [maxx, maxy, maxz] } // AABB
|
||||||
|
```
|
||||||
|
|
||||||
|
Ray hits return `hit | null`:
|
||||||
|
|
||||||
|
```cell
|
||||||
|
// when hit:
|
||||||
|
{ dist: number, point: [x,y,z], normal: [nx,ny,nz] }
|
||||||
|
|
||||||
|
// when miss:
|
||||||
|
null
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shape checks
|
||||||
|
|
||||||
|
> **`check_collision_spheres(c1, r1, c2, r2)` → bool**
|
||||||
|
> **`check_collision_boxes(box1, box2)` → bool**
|
||||||
|
> **`check_collision_box_sphere(box, center, radius)` → bool**
|
||||||
|
|
||||||
|
### Ray queries
|
||||||
|
|
||||||
|
> **`get_ray_collision_sphere(ray, center, radius)` → hit | null**
|
||||||
|
> **`get_ray_collision_box(ray, box)` → hit | null**
|
||||||
|
> **`get_ray_collision_mesh(ray, mesh, transform = null)` → hit | null**
|
||||||
|
|
||||||
|
### Mesh collider (cached acceleration)
|
||||||
|
|
||||||
|
For repeated raycasts (picking, bullets, LOS checks), you can precompute a mesh collider once:
|
||||||
|
|
||||||
|
> **`make_mesh_collider(mesh)` → mesh_collider**
|
||||||
|
|
||||||
|
This builds an internal acceleration structure (BVH) in **mesh local space**.
|
||||||
|
|
||||||
|
Then use:
|
||||||
|
|
||||||
|
> **`get_ray_collision_mesh_collider(ray, mesh_collider, transform = null)` → hit | null**
|
||||||
|
|
||||||
|
Same result as `get_ray_collision_mesh`, but faster for repeated queries.
|
||||||
|
|
||||||
|
### Will this work?
|
||||||
|
|
||||||
|
Yes—this is a solid “fantasy-console collision” model **if you keep the expectations correct**:
|
||||||
|
|
||||||
|
* Great for: triggers, overlap tests, ray picking, hitscan weapons, simple AI line-of-sight.
|
||||||
|
* Not a physics engine: no solver, no continuous collision handling, no automatic contact resolution.
|
||||||
|
|
||||||
|
Mesh colliders specifically:
|
||||||
|
|
||||||
|
* Great when the mesh is **static** (level geometry) or the mesh itself doesn’t change.
|
||||||
|
* Fine for moving instances: pass a different `transform` per query.
|
||||||
|
* If the mesh deforms/changes topology, you must rebuild the collider (call `make_mesh_collider` again).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
btn(id, player=0) -> bool // held
|
|
||||||
btnp(id, player=0) -> bool // pressed this frame
|
|
||||||
|
|
||||||
## Math
|
> **`btn(id, player = 0)` → bool**
|
||||||
seed(seed_int)
|
> **`btnp(id, player = 0)` → bool**
|
||||||
rand() -> number // [0,1)
|
|
||||||
irand(min_inclusive, max_inclusive) -> int
|
|
||||||
|
|
||||||
## Debug API
|
Button IDs:
|
||||||
vertex {x, y, z, u=0, v=0, color}
|
|
||||||
point(vertex, size=1.0)
|
|
||||||
line(vertex_a, vertex_b, width=1.0)
|
|
||||||
grid(size, step, norm = {x:0,y:1,z:0}, color)
|
|
||||||
|
|
||||||
triangle(vertex_a, vertex_b, vertex_c, mat=null)
|
* 0 left, 1 right, 2 down, 3 up
|
||||||
ray(ox,oy,oz, dx,dy,dz, len, opts)
|
* 4 A, 5 B, 6 X, 7 Y
|
||||||
|
* 8 L, 9 R
|
||||||
|
* 10 Start, 11 Select
|
||||||
|
|
||||||
aabb(cx,cy,cz, ex,ey,ez, opts) // center+extents
|
---
|
||||||
obb(transform, sx,sy,sz, opts) // oriented via transform
|
|
||||||
frustum(camera_state_or_params, opts)
|
|
||||||
text3d(str, x,y,z, opts={size_px:12, depth:"always"|"test"})
|
|
||||||
|
|
||||||
Render a specific collider
|
## Debug drawing (world space)
|
||||||
collider(collider, color)
|
|
||||||
|
> **`dbg_line(a, b, color = [1,0,0])` → void**
|
||||||
|
> **`dbg_aabb(box, color = [1,0,0])` → void**
|
||||||
|
> **`dbg_ray(ray, color = [1,0,0])` → void**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cheat sheet (v1 API)
|
||||||
|
|
||||||
|
## Core
|
||||||
|
|
||||||
|
* `register({ update(dt), draw() })` // start the game loop
|
||||||
|
* `time()` // seconds since boot
|
||||||
|
* `log(...args)` // print to log
|
||||||
|
* `stat(name)` // runtime stats: draw_calls/triangles/fps/memory_bytes
|
||||||
|
|
||||||
|
## Style
|
||||||
|
|
||||||
|
* `set_style(style = "ps1")` // set visual style
|
||||||
|
* `tag_texture(texture, tier = "normal")` // tag texture as low/normal/hero
|
||||||
|
* `tag_model(model, tier = "normal")` // tag model as low/normal/hero
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
* `camera_look_at(ex, ey, ez, tx, ty, tz, upx=0, upy=0, upz=1)` // set view
|
||||||
|
* `camera_perspective(fov=pi/3, near=0.1, far=1000)` // set perspective projection
|
||||||
|
* `camera_ortho(left, right, bottom, top, near=-1, far=1)` // set ortho projection
|
||||||
|
* `set_lighting({ sun_dir, sun_color, ambient })` // set sun + ambient
|
||||||
|
* `set_fog({ enabled=false, color, near, far })` // set fog
|
||||||
|
|
||||||
|
## Transforms
|
||||||
|
|
||||||
|
* `trs_matrix(tx, ty, tz, rx, ry, rz, sx=1, sy=1, sz=1)` // build transform blob (TRS)
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
* `load_texture(path)` // load png/jpg, returns texture or null
|
||||||
|
* `load_model(path)` // load glb, returns model or null
|
||||||
|
* `load_sound(path)` // load wav/mp3, returns sound or null
|
||||||
|
* `load_font(path)` // load a font, returns font or null
|
||||||
|
|
||||||
|
## Procedural geometry
|
||||||
|
|
||||||
|
* `make_cube(w=1, h=1, d=1)` // cube mesh
|
||||||
|
* `make_sphere(r=1, segments=12)` // sphere mesh
|
||||||
|
* `make_cylinder(r=1, h=1, segments=12)` // cylinder mesh
|
||||||
|
* `make_plane(w=1, h=1)` // plane mesh
|
||||||
|
|
||||||
|
## Drawing 3D
|
||||||
|
|
||||||
|
* `draw_model(model, transform=null, pose=null)` // draw model with embedded materials
|
||||||
|
* `draw_mesh(mesh, transform=null, material=null)` // draw mesh with optional material override
|
||||||
|
* `draw_billboard(texture, x, y, z, size=1.0, material=null)` // camera-facing sprite in world
|
||||||
|
|
||||||
|
## Drawing 2D
|
||||||
|
|
||||||
|
* `draw_sprite(texture, x, y, size=1.0, material=null)` // screen-space sprite over 3D
|
||||||
|
* `draw_text(text, x, y, scale=1.0, color=[1,1,1,1], font=null)` // screen-space text
|
||||||
|
* `pset(x, y, color=[1,1,1,1])` // set pixel
|
||||||
|
* `line2(x0, y0, x1, y1, color=[1,1,1,1])` // draw line
|
||||||
|
* `rect(x, y, w, h, color=[1,1,1,1])` // rectangle outline
|
||||||
|
* `rectfill(x, y, w, h, color=[1,1,1,1])` // filled rectangle
|
||||||
|
* `circ(x, y, r, color=[1,1,1,1])` // circle outline
|
||||||
|
* `circfill(x, y, r, color=[1,1,1,1])` // filled circle
|
||||||
|
* `clip(x, y, w, h)` // enable 2D clipping
|
||||||
|
* `clip()` // disable 2D clipping
|
||||||
|
|
||||||
|
## Animation
|
||||||
|
|
||||||
|
* `anim_info(model)` // inspect available animations
|
||||||
|
* `sample_pose(model, name, t)` // sample pose at time t, returns pose or null
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
* `play_sound(sound, { volume=1, pitch=1, pan=0, loop=false })` // play a sound voice
|
||||||
|
* `stop_sound(voice)` // stop a playing voice
|
||||||
|
* `chip(channel, op, value)` // control embedded chip / MIDI
|
||||||
|
|
||||||
|
## Collision
|
||||||
|
|
||||||
|
* `check_collision_spheres(c1, r1, c2, r2)` // sphere-sphere overlap
|
||||||
|
* `check_collision_boxes(box1, box2)` // AABB-AABB overlap
|
||||||
|
* `check_collision_box_sphere(box, center, radius)` // AABB-sphere overlap
|
||||||
|
* `get_ray_collision_sphere(ray, center, radius)` // ray vs sphere, returns hit or null
|
||||||
|
* `get_ray_collision_box(ray, box)` // ray vs AABB, returns hit or null
|
||||||
|
* `get_ray_collision_mesh(ray, mesh, transform=null)` // ray vs mesh, returns hit or null
|
||||||
|
* `make_mesh_collider(mesh)` // build cached collider (BVH) from mesh
|
||||||
|
* `get_ray_collision_mesh_collider(ray, mesh_collider, transform=null)` // fast ray vs mesh collider
|
||||||
|
|
||||||
|
## Input
|
||||||
|
|
||||||
|
* `btn(id, player=0)` // button held
|
||||||
|
* `btnp(id, player=0)` // button pressed this frame
|
||||||
|
|
||||||
|
## Debug
|
||||||
|
|
||||||
|
* `dbg_line(a, b, color=[1,0,0])` // debug line
|
||||||
|
* `dbg_aabb(box, color=[1,0,0])` // debug AABB
|
||||||
|
* `dbg_ray(ray, color=[1,0,0])` // debug ray
|
||||||
|
|||||||
Reference in New Issue
Block a user