diff --git a/.gitignore b/.gitignore index a0090b46..78431c2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,11 @@ # macOS .DS_Store + +# Baked gametest bytecode. `gmx test` compiles `foo.gametest.json` to +# `foo.json` beside it; the source is the file worth versioning, and the +# baked one both goes stale and installs into consumer projects. +prefabs/*/demo.json +prefabs/*/thumbnail.json + +# `gmx run` writes its capture bookkeeping into the prefab it ran. +prefabs/*/.gmx/ diff --git a/README.md b/README.md index 59ace762..faee89cd 100644 --- a/README.md +++ b/README.md @@ -1 +1,142 @@ # GMX Prefabs + +The prefab catalog for [gmx](https://github.com/opera-gaming/assetcompiler). +`gmx prefab` commands resolve `gh:opera-gaming/prefabs` by default, so a +prefab published here is reachable as `gmx prefab add `. + +``` +prefabs// one prefab per directory +prefab-index.json generated — never hand-edit (see Publishing) +``` + +## What every prefab carries + +| File | | +|---|---| +| `prefab.toml` | The manifest. Identity comes from the directory name and git tags, not from this file. | +| `README.md` | What it is and how to use it. Stays in the repo — it is not installed into a project. | +| `LICENSE` | Required. MIT for code prefabs, Apache-2.0 for the art and audio packs. | +| `project.toml` | Makes the prefab runnable on its own: a `[room_order]` naming its demo rooms. Without it `gmx run ` refuses the directory, and there is no way to take a thumbnail. | +| `thumbnail.png` | Produced with `gmx run --screenshot thumbnail.png --at-frame `. Present wherever there is something to draw — see below. | + +A playable prefab also carries `demo.gametest.json` — a test that boots it +and asserts the loop it exists to demonstrate. Run the suite with +`just test-prefabs ` from the gmx repo; gmx's own CI does. + +Everything a thumbnail needs comes from a **demo**: an `obj_demo` and an +`rm_demo` that stand the prefab up on its own, both named in `exclude`. +`exclude` describes what a *dependency* hands its dependent, so a prefab +pulled in through someone else's `requires` never sees them — while +`gmx prefab add ` keeps them, because there the prefab is the subject +and its demo is the runnable form of it. A prefab with no demo cannot be run, +so it cannot be pictured — and, less obviously, nothing ever exercises it: +`main_menu` shipped with `[[content.items]]` that had no `id`, so `obj_menu` +died in its own Create the first time it was placed in a room. Writing the +demo is what found that. + +Two things the demo has to supply itself, because neither exists until a +consumer installs the prefab: + +- **The kernel is not booted.** Anything reaching `::kernel::` reads a + global that was never set, and dies naming a kernel script rather than + the caller. The demo calls `::kernel::kernel_boot()` in its Create. +- **`_tuning()` is generated at install**, not present in the + source tree. A demo run straight from the prefab directory cannot call + it; run it from an installed project instead. + +## Two kinds of prefab + +**Asset packs** declare `asset_bundle = true` and describe their resources +in `[assets."/"]` blocks so individual sprites and sounds are +searchable and independently importable. + +**Code prefabs** declare `schema_version = 1` and an `install` mode: + +- `install = "namespace"` (the default) — a **library**. Lands in + `prefabs//` in the target project, every symbol renamed to + `__`, called as `::::thing()`. Refreshed in place + when a fix ships, so it is not yours to edit. +- `install = "copy"` — a **game or component**. Lands in the project root, + unnamespaced; the project owns the files from the moment they arrive. + Rooms are appended to `[room_order]`, and a file the project already has + is never overwritten — the incoming copy goes to `.gmx/incoming/`. + +## Manifest fields + +All optional, but the ones a reader depends on are not really. + +| Field | | +|---|---| +| `name` | Display name. Falls back to the directory name. | +| `description` | One line, shown in `list` and `search`. | +| `category` | One grouping word. In use: `game`, `component`, `library`, `effects`, `character`, `world`, `online`. | +| `tags` | `facet:value` — `genre:puzzle`, `capability:movement`, `role:library`. A playable game carries a `genre:`; a component does not. | +| `install` | See above. | +| `requires` | Sibling prefabs this one composes, by bare name. Transitive. | +| `exclude` | Resource paths kept on disk so `gmx run` works while authoring, but dropped from the index and from anything a consumer pulls — demo objects, demo rooms, placeholder sprites. | +| `seam` | The one file to edit first. For a `copy` prefab; a library has no seam because you call it rather than edit it. | +| `steps` | What a reader does next, in order. | +| `traps` | Things that fail **quietly**. A trap earns its place by costing real time: writing `x` on a physics body, which the solver discards so the object simply never moves. Not a style guide — only what is both non-obvious and silent. | +| `[roles]` | What each object and room is *for*, keyed by bare resource name. Without it nothing says which object is the game and which room it plays in. | +| `[tuning]` | Scalar knobs, code-generated into `_tuning()`. | +| `[content]` | Data tables, code-generated into `_data(name)`. | +| `[art]` | Slots naming art elsewhere in this catalog, printed on install as `gmx prefab add` lines. | +| `[parent-link]` | Derive from another prefab: `source = "@"`, plus optional `unset`. Version-pinned. | + +`exclude` is a **top-level** key. Written after a `[tuning]` or `[content]` +header it becomes a *knob* instead — valid TOML that silently means nothing +and leaks into the generated GML. + +Four files never install regardless of `exclude`: `prefab.toml`, +`README.md`, `demo.gametest.json` and `expected.txt`. The gametest is then +deliberately re-added as `.gametest.json`, because in the target +project it is that project's test. Listing any of them in `exclude` does +nothing. + +## Publishing + +Two different tags are involved, and shipping only one of them publishes +nothing. + +```sh +gmx prefab reindex . # regenerate prefab-index.json +git commit -am "…" # commit the tree AND the index +git push origin main # the commit itself — `--tags` does not +git tag v.. # the catalog tag: this is the publish +git tag @ # …one per prefab whose version changed +git push --tags +``` + +**The catalog tag is what consumers read.** `gmx prefab list` and every +`prefab add` load `prefab-index.json` *at the newest tag that has no +`@` prefix* — so until a new `vX.Y.Z` exists, a consumer sees the +previous release's index no matter how many per-prefab tags were pushed, +and a prefab added since is simply not there. + +**The per-prefab tag is what pins a version.** `@` is how +`gmx prefab add @1.2.0` and a `[parent-link] source` resolve. A +prefab with no tag can be listed but not pinned. + +**Push the commit as well.** `git push --tags` uploads tags and nothing +else; a tag whose commit was never pushed resolves to nothing on a clean +clone. Push the branch first so the tags always land on a commit that +already exists on the remote. + +Tag only a commit that is on `main`. A tag on an abandoned or unmerged +branch keeps resolving — quietly, to the wrong tree — long after that +branch is gone. + +`gmx prefab reindex` measures every sprite for size and colour, so the +index is a build artifact: regenerate it, never hand-edit it, and commit +it in the same change as the tree it describes. + +## Checking + +```sh +gmx prefab check . # dead [art] slots, unsupported README claims +gmx prefab check . --gametests # also boots each prefab's demo test +``` + +`check` installs each prefab into a scratch project, so it catches what a +source diff cannot: an `[art]` slot naming a sprite the catalog no longer +has, or a README promising a resource that has since been renamed. diff --git a/prefab-index.json b/prefab-index.json index 2f5e6d83..d3e2e588 100644 --- a/prefab-index.json +++ b/prefab-index.json @@ -14,6 +14,7 @@ "style:shaded" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/aerial_combat/README.md", "thumbnail_path": "prefabs/aerial_combat/thumbnail.png", "resources": [ @@ -1703,7 +1704,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 193, + "height": 112 + }, + "colour": "#307f36" } }, { @@ -1727,7 +1733,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 13 + }, + "colour": "#174d96" } }, { @@ -1757,7 +1768,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 113 + }, + "colour": "#0e49a0" } }, { @@ -1819,7 +1835,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 119 + }, + "colour": "#0e49a0" } }, { @@ -1843,7 +1864,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 13 + }, + "colour": "#317e3c" } }, { @@ -1873,7 +1899,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 113 + }, + "colour": "#307f36" } }, { @@ -1935,7 +1966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 119 + }, + "colour": "#307f36" } }, { @@ -1959,7 +1995,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 13 + }, + "colour": "#83563a" } }, { @@ -1989,7 +2030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 113 + }, + "colour": "#8f5a24" } }, { @@ -2051,7 +2097,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 119 + }, + "colour": "#8f5a24" } }, { @@ -2075,7 +2126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 13 + }, + "colour": "#632396" } }, { @@ -2105,7 +2161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 113 + }, + "colour": "#6917a0" } }, { @@ -2167,7 +2228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 119 + }, + "colour": "#6917a0" } }, { @@ -2191,7 +2257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 13 + }, + "colour": "#773536" } }, { @@ -2221,7 +2292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 113 + }, + "colour": "#82292c" } }, { @@ -2283,7 +2359,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 119 + }, + "colour": "#82292c" } }, { @@ -2307,7 +2388,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 214, + "height": 88 + }, + "colour": "#3562cd" } }, { @@ -2351,7 +2437,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 225 + }, + "colour": "#3663cf" } }, { @@ -2384,7 +2475,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#3663cf" } }, { @@ -2413,7 +2509,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 89 + }, + "colour": "#3663cf" } }, { @@ -2437,7 +2538,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 225, + "height": 218 + }, + "colour": "#2959c9" } }, { @@ -2461,7 +2567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 214, + "height": 88 + }, + "colour": "#5da23b" } }, { @@ -2505,7 +2616,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 224 + }, + "colour": "#5fa43d" } }, { @@ -2538,7 +2654,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#5fa43d" } }, { @@ -2567,7 +2688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 90 + }, + "colour": "#5fa43d" } }, { @@ -2591,7 +2717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 224, + "height": 218 + }, + "colour": "#46ac2c" } }, { @@ -2615,7 +2746,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 215, + "height": 89 + }, + "colour": "#d59547" } }, { @@ -2659,7 +2795,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#d69748" } }, { @@ -2692,7 +2833,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#d69748" } }, { @@ -2721,7 +2867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 90 + }, + "colour": "#d79748" } }, { @@ -2745,7 +2896,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 218 + }, + "colour": "#d48937" } }, { @@ -2769,7 +2925,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 215, + "height": 88 + }, + "colour": "#7848d5" } }, { @@ -2813,7 +2974,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#7a49d7" } }, { @@ -2846,7 +3012,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 218, + "height": 223 + }, + "colour": "#7a49d7" } }, { @@ -2875,7 +3046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 89 + }, + "colour": "#7a49d7" } }, { @@ -2899,7 +3075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 218 + }, + "colour": "#8139d8" } }, { @@ -2923,7 +3104,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 214, + "height": 87 + }, + "colour": "#904768" } }, { @@ -2967,7 +3153,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 225 + }, + "colour": "#91496a" } }, { @@ -3000,7 +3191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 223 + }, + "colour": "#91496a" } }, { @@ -3029,7 +3225,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 89 + }, + "colour": "#91496a" } }, { @@ -3053,7 +3254,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 225, + "height": 217 + }, + "colour": "#a24054" } }, { @@ -3077,7 +3283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 341, + "height": 139 + }, + "colour": "#8c6483" } }, { @@ -3100,7 +3311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 216, + "height": 90 + }, + "colour": "#637fb6" } }, { @@ -3149,7 +3365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 87 + }, + "colour": "#d2e0eb" } }, { @@ -3173,7 +3394,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 74, + "height": 61 + }, + "colour": "#cdd9e3" } }, { @@ -3211,7 +3437,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 705, + "height": 689 + }, + "colour": "#ffdb0e" } }, { @@ -3234,7 +3465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 133, + "height": 131 + }, + "colour": "#8d7e1c" } }, { @@ -3272,7 +3508,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 705, + "height": 689 + }, + "colour": "#ffdb0e" } }, { @@ -3295,7 +3536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 133, + "height": 131 + }, + "colour": "#8d7e1c" } }, { @@ -3337,7 +3583,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 639, + "height": 946 + }, + "colour": "#fe9502" } }, { @@ -3360,7 +3611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 133, + "height": 131 + }, + "colour": "#635619" } }, { @@ -3397,7 +3653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 838, + "height": 820 + }, + "colour": "#fec20a" } }, { @@ -3420,7 +3681,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 136, + "height": 140 + }, + "colour": "#b49d10" } }, { @@ -3465,7 +3731,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 517, + "height": 388 + }, + "colour": "#bbe9fc" } }, { @@ -3489,7 +3760,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 58 + }, + "colour": "#78929d" } }, { @@ -3534,7 +3810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 438, + "height": 259 + }, + "colour": "#bce9fd" } }, { @@ -3558,7 +3839,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 58 + }, + "colour": "#78929d" } }, { @@ -3588,7 +3874,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 145, + "height": 46 + }, + "colour": "#ff9c04" } }, { @@ -3612,7 +3903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 84, + "height": 40 + }, + "colour": "#c67602" } }, { @@ -3645,7 +3941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 72, + "height": 71 + }, + "colour": "#feb90a" } }, { @@ -3669,7 +3970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 59 + }, + "colour": "#ba6802" } }, { @@ -3699,7 +4005,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 51, + "height": 46 + }, + "colour": "#ffdc1b" } }, { @@ -3723,7 +4034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 28 + }, + "colour": "#d2b918" } }, { @@ -3760,7 +4076,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 396, + "height": 237 + }, + "colour": "#fdb837" } }, { @@ -3784,7 +4105,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 114, + "height": 101 + }, + "colour": "#e2a933" } }, { @@ -3812,7 +4138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 89 + }, + "colour": "#faf2ad" } }, { @@ -3835,7 +4166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 76, + "height": 75 + }, + "colour": "#62614b" } }, { @@ -3863,7 +4199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 175, + "height": 131 + }, + "colour": "#fda40a" } }, { @@ -3886,7 +4227,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 112, + "height": 111 + }, + "colour": "#88761c" } }, { @@ -3935,7 +4281,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 87 + }, + "colour": "#a69992" } }, { @@ -3959,7 +4310,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 74, + "height": 61 + }, + "colour": "#a1948d" } }, { @@ -3982,7 +4338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 18 + }, + "colour": "#394958" } }, { @@ -4005,7 +4366,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 76, + "height": 299 + }, + "colour": "#4e5c6e" } }, { @@ -4028,7 +4394,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 518, + "height": 136 + }, + "colour": "#7394b9" } }, { @@ -4051,7 +4422,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 374, + "height": 149 + }, + "colour": "#dabe65" } }, { @@ -4075,7 +4451,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 510, + "height": 132 + }, + "colour": "#9e9aae" } }, { @@ -4098,7 +4479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 242, + "height": 296 + }, + "colour": "#d4889a" } }, { @@ -4121,7 +4507,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 150, + "height": 85 + }, + "colour": "#4d92d8" } }, { @@ -4163,7 +4554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 54, + "height": 52 + }, + "colour": "#468bcd" } }, { @@ -4220,7 +4616,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 87 + }, + "colour": "#4892df" } }, { @@ -4243,7 +4644,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 501, + "height": 254 + }, + "colour": "#abbcdc" } }, { @@ -4266,7 +4672,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 440, + "height": 213 + }, + "colour": "#e0ecfb" } }, { @@ -4289,7 +4700,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 439, + "height": 226 + }, + "colour": "#e2edfb" } }, { @@ -4312,7 +4728,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 410, + "height": 216 + }, + "colour": "#e2edfb" } }, { @@ -4336,7 +4757,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 15 + }, + "colour": "#64b349" } }, { @@ -4360,7 +4786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 15 + }, + "colour": "#c3a962" } }, { @@ -4383,7 +4814,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 27 + }, + "colour": "#46c750" } }, { @@ -4406,7 +4842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 37, + "height": 20 + }, + "colour": "#48c850" } }, { @@ -4429,7 +4870,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 40 + }, + "colour": "#5864aa" } }, { @@ -4452,7 +4898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 85, + "height": 44 + }, + "colour": "#5864ab" } }, { @@ -4475,7 +4926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 44 + }, + "colour": "#5561a8" } }, { @@ -4499,7 +4955,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 151, + "height": 49 + }, + "colour": "#55cd50" } }, { @@ -4523,7 +4984,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 173, + "height": 65 + }, + "colour": "#54c854" } }, { @@ -4547,7 +5013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 54 + }, + "colour": "#52c655" } }, { @@ -4570,7 +5041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1364 + }, + "colour": "#7fa7df" } }, { @@ -4593,7 +5069,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 319, + "height": 319 + }, + "colour": "#e8cec9" } }, { @@ -4618,7 +5099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#adb259" } }, { @@ -4643,7 +5129,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b0af56" } }, { @@ -4668,7 +5159,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#adb359" } }, { @@ -4693,7 +5189,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#d0b15d" } }, { @@ -4718,7 +5219,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ccad5b" } }, { @@ -4743,7 +5249,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#d1b25d" } }, { @@ -4766,7 +5277,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1595, + "height": 194 + }, + "colour": "#c1e5ff" } }, { @@ -4789,7 +5305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1680, + "height": 188 + }, + "colour": "#c1e4ff" } }, { @@ -4812,7 +5333,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 189, + "height": 165 + }, + "colour": "#48b14e" } }, { @@ -4835,7 +5361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 176, + "height": 168 + }, + "colour": "#49ae4d" } }, { @@ -4858,7 +5389,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 202, + "height": 181 + }, + "colour": "#49a94c" } }, { @@ -4881,7 +5417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 116 + }, + "colour": "#3ba550" } }, { @@ -4917,7 +5458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 95 + }, + "colour": "#4ab1f2" } }, { @@ -4941,7 +5487,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 86 + }, + "colour": "#3d77e0" } }, { @@ -4965,7 +5516,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 85 + }, + "colour": "#3c76df" } }, { @@ -5009,7 +5565,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 318 + }, + "colour": "#3d77e0" } }, { @@ -5042,7 +5603,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 250, + "height": 318 + }, + "colour": "#3d77e0" } }, { @@ -5066,7 +5632,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 252 + }, + "colour": "#316bd9" } }, { @@ -5090,7 +5661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 86 + }, + "colour": "#3dad3b" } }, { @@ -5114,7 +5690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 86 + }, + "colour": "#3dac3b" } }, { @@ -5158,7 +5739,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 318 + }, + "colour": "#3dad3b" } }, { @@ -5191,7 +5777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 250, + "height": 318 + }, + "colour": "#3dad3b" } }, { @@ -5215,7 +5806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 252 + }, + "colour": "#26a729" } }, { @@ -5239,7 +5835,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 86 + }, + "colour": "#d99e56" } }, { @@ -5263,7 +5864,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 86 + }, + "colour": "#d89e55" } }, { @@ -5307,7 +5913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 318 + }, + "colour": "#d99e56" } }, { @@ -5340,7 +5951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 250, + "height": 318 + }, + "colour": "#d99e56" } }, { @@ -5364,7 +5980,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 252 + }, + "colour": "#d38c43" } }, { @@ -5388,7 +6009,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 86 + }, + "colour": "#954ee6" } }, { @@ -5412,7 +6038,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 86 + }, + "colour": "#944de4" } }, { @@ -5456,7 +6087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 318 + }, + "colour": "#954ee6" } }, { @@ -5489,7 +6125,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 250, + "height": 318 + }, + "colour": "#954ee6" } }, { @@ -5513,7 +6154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 252 + }, + "colour": "#a645e8" } }, { @@ -5537,7 +6183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 86 + }, + "colour": "#b0545f" } }, { @@ -5561,7 +6212,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 86 + }, + "colour": "#af545e" } }, { @@ -5605,7 +6261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 318 + }, + "colour": "#b0545f" } }, { @@ -5638,7 +6299,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 250, + "height": 318 + }, + "colour": "#b0545f" } }, { @@ -5662,7 +6328,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 252 + }, + "colour": "#be484a" } }, { @@ -5685,7 +6356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 90, + "height": 175 + }, + "colour": "#e8cd6e" } }, { @@ -5708,7 +6384,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 185 + }, + "colour": "#e8cd6f" } }, { @@ -5740,7 +6421,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 110, + "height": 185 + }, + "colour": "#e8cd6f" } }, { @@ -5763,7 +6449,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 78 + }, + "colour": "#975015" } }, { @@ -5793,7 +6484,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 334, + "height": 286 + }, + "colour": "#965015" } }, { @@ -5816,7 +6512,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 244 + }, + "colour": "#fbefc2" } }, { @@ -5848,7 +6549,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 54, + "height": 311 + }, + "colour": "#fbefc2" } }, { @@ -5880,7 +6586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 53, + "height": 315 + }, + "colour": "#fbefc2" } }, { @@ -5903,7 +6614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 242 + }, + "colour": "#fbefc2" } }, { @@ -5926,7 +6642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 245 + }, + "colour": "#fbefc2" } }, { @@ -5950,7 +6671,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 202, + "height": 181 + }, + "colour": "#95afe5" } }, { @@ -5974,7 +6700,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 188 + }, + "colour": "#95b0e5" } }, { @@ -6001,7 +6732,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 215, + "height": 188 + }, + "colour": "#95b0e5" } }, { @@ -6024,6 +6760,10 @@ "speed": { "unit": "frames_per_second", "value": 30.0 + }, + "size": { + "width": 64, + "height": 64 } } }, @@ -6066,7 +6806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 89, + "height": 85 + }, + "colour": "#fef2aa" } }, { @@ -6095,7 +6840,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 97 + }, + "colour": "#fef0a8" } }, { @@ -6127,7 +6877,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 127, + "height": 122 + }, + "colour": "#fef2aa" } }, { @@ -6150,7 +6905,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 110, + "height": 46 + }, + "colour": "#6d6f6b" } }, { @@ -6173,7 +6933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 35, + "height": 13 + }, + "colour": "#3e508c" } }, { @@ -6196,7 +6961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 120, + "height": 49 + }, + "colour": "#b96368" } }, { @@ -6219,7 +6989,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 135, + "height": 40 + }, + "colour": "#40587c" } }, { @@ -6243,7 +7018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 264, + "height": 145 + }, + "colour": "#266d2f" } }, { @@ -6274,7 +7054,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 147 + }, + "colour": "#0a3f88" } }, { @@ -6337,7 +7122,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 149 + }, + "colour": "#0a3f89" } }, { @@ -6361,7 +7151,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#0044b3" } }, { @@ -6392,7 +7187,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 147 + }, + "colour": "#266e2f" } }, { @@ -6455,7 +7255,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 149 + }, + "colour": "#266e2f" } }, { @@ -6479,7 +7284,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#2d8b28" } }, { @@ -6510,7 +7320,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 147 + }, + "colour": "#7b4818" } }, { @@ -6573,7 +7388,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 149 + }, + "colour": "#7b4817" } }, { @@ -6597,7 +7417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#b35100" } }, { @@ -6628,7 +7453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 147 + }, + "colour": "#683294" } }, { @@ -6691,7 +7521,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 149 + }, + "colour": "#683294" } }, { @@ -6715,7 +7550,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#9535c8" } }, { @@ -6746,7 +7586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 147 + }, + "colour": "#701f2d" } }, { @@ -6809,7 +7654,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 149 + }, + "colour": "#711f2d" } }, { @@ -6833,7 +7683,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#a01325" } }, { @@ -6856,7 +7711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 103 + }, + "colour": "#346bd7" } }, { @@ -6879,7 +7739,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 102 + }, + "colour": "#5cbd4a" } }, { @@ -6902,7 +7767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 102 + }, + "colour": "#d5a157" } }, { @@ -6925,7 +7795,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 102 + }, + "colour": "#8453e3" } }, { @@ -6948,7 +7823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 102 + }, + "colour": "#9e5773" } }, { @@ -6991,7 +7871,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 299 + }, + "colour": "#346bd7" } }, { @@ -7023,7 +7908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 298 + }, + "colour": "#346bd7" } }, { @@ -7051,7 +7941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 103 + }, + "colour": "#356bd7" } }, { @@ -7074,7 +7969,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 299, + "height": 239 + }, + "colour": "#2967d3" } }, { @@ -7117,7 +8017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 241, + "height": 301 + }, + "colour": "#5cbd4b" } }, { @@ -7149,7 +8054,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 241, + "height": 300 + }, + "colour": "#5cbd4b" } }, { @@ -7177,7 +8087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 238, + "height": 103 + }, + "colour": "#5cbd4b" } }, { @@ -7200,7 +8115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 241 + }, + "colour": "#37b83e" } }, { @@ -7243,7 +8163,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 241, + "height": 301 + }, + "colour": "#d5a158" } }, { @@ -7275,7 +8200,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 241, + "height": 300 + }, + "colour": "#d5a158" } }, { @@ -7303,7 +8233,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 238, + "height": 103 + }, + "colour": "#d5a158" } }, { @@ -7326,7 +8261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 241 + }, + "colour": "#c9833e" } }, { @@ -7369,7 +8309,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 240, + "height": 298 + }, + "colour": "#8554e2" } }, { @@ -7401,7 +8346,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 240, + "height": 298 + }, + "colour": "#8554e2" } }, { @@ -7429,7 +8379,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 102 + }, + "colour": "#8554e2" } }, { @@ -7452,7 +8407,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 298, + "height": 240 + }, + "colour": "#963ddd" } }, { @@ -7495,7 +8455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 240, + "height": 298 + }, + "colour": "#9e5873" } }, { @@ -7527,7 +8492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 240, + "height": 298 + }, + "colour": "#9e5873" } }, { @@ -7555,7 +8525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 102 + }, + "colour": "#9e5873" } }, { @@ -7578,7 +8553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 298, + "height": 240 + }, + "colour": "#b54143" } }, { @@ -7601,7 +8581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 264, + "height": 123 + }, + "colour": "#6182b4" } }, { @@ -7624,7 +8609,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 123 + }, + "colour": "#607daf" } }, { @@ -7662,7 +8652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 366, + "height": 125 + }, + "colour": "#6180b2" } }, { @@ -7685,7 +8680,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 130 + }, + "colour": "#297c2b" } }, { @@ -7709,7 +8709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 17 + }, + "colour": "#193965" } }, { @@ -7732,7 +8737,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 132 + }, + "colour": "#1c469b" } }, { @@ -7814,7 +8824,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 135 + }, + "colour": "#1c469b" } }, { @@ -7838,7 +8853,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 17 + }, + "colour": "#1e522f" } }, { @@ -7861,7 +8881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 132 + }, + "colour": "#297c2b" } }, { @@ -7943,7 +8968,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 135 + }, + "colour": "#297c2b" } }, { @@ -7967,7 +8997,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 17 + }, + "colour": "#554429" } }, { @@ -7990,7 +9025,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 132 + }, + "colour": "#9a5b1c" } }, { @@ -8072,7 +9112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 135 + }, + "colour": "#9a5b1c" } }, { @@ -8096,7 +9141,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 17 + }, + "colour": "#472d67" } }, { @@ -8119,7 +9169,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 132 + }, + "colour": "#7b2a9e" } }, { @@ -8201,7 +9256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 135 + }, + "colour": "#7b2a9e" } }, { @@ -8225,7 +9285,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 17 + }, + "colour": "#4d2d30" } }, { @@ -8248,7 +9313,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 132 + }, + "colour": "#892c2d" } }, { @@ -8330,8 +9400,504 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 195, + "height": 135 + }, + "colour": "#892c2d" + } + } + ] + }, + { + "name": "boss", + "version": "", + "display_name": "Boss fights", + "description": "Health split into stages, attacks gated by stage so a later one escalates rather than repeating, and a wind-up/active/recover cycle for every move. The telegraph is the whole design: an attack that fires the instant it is chosen is not difficult but unfair, and the fight reads as random. `boss_step` returns one event per frame rather than setting several flags, because a flag left set is how an attack fires twice.", + "category": "library", + "tags": [ + "capability:boss", + "capability:combat", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/boss/README.md", + "thumbnail_path": "prefabs/boss/thumbnail.png", + "resources": [ + { + "prefab": "boss", + "kind": "script", + "name": "scr_boss", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "boss", + "kind": "script", + "name": "scr_boss_move", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "boss", + "kind": "script", + "name": "scr_boss_phase", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "breakout", + "version": "", + "display_name": "Breakout", + "description": "Clear the bricks, do not drop the ball — and steer it with the paddle.", + "category": "game", + "tags": [ + "genre:arcade", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/breakout/README.md", + "thumbnail_path": "prefabs/breakout/thumbnail.png", + "resources": [ + { + "prefab": "breakout", + "kind": "object", + "name": "obj_ball", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "object", + "name": "obj_breakout", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "object", + "name": "obj_brick", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "object", + "name": "obj_paddle", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "script", + "name": "scr_breakout_serve", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "breakout", + "kind": "sprite", + "name": "spr_ball", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/breakout/sprites/spr_ball/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 14, + "height": 14 + }, + "colour": "#fff0c8" + } + }, + { + "prefab": "breakout", + "kind": "sprite", + "name": "spr_brick", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/breakout/sprites/spr_brick/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 76, + "height": 26 + }, + "colour": "#e6786e" } + }, + { + "prefab": "breakout", + "kind": "sprite", + "name": "spr_paddle", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/breakout/sprites/spr_paddle/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 110, + "height": 18 + }, + "colour": "#96d2ff" + } + } + ] + }, + { + "name": "bullet_hell", + "version": "", + "display_name": "Bullet hell", + "description": "Thread the pattern. One hit ends it, so the hitbox is smaller than you are.", + "category": "game", + "tags": [ + "genre:shmup", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/bullet_hell/README.md", + "thumbnail_path": "prefabs/bullet_hell/thumbnail.png", + "resources": [ + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_boss", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_bullet", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_hell", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_ship", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_shot", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "script", + "name": "scr_hell_patterns", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "bullet_hell", + "kind": "sprite", + "name": "spr_boss", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/bullet_hell/sprites/spr_boss/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 72, + "height": 72 + }, + "colour": "#dc6e5a" + } + }, + { + "prefab": "bullet_hell", + "kind": "sprite", + "name": "spr_bullet", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/bullet_hell/sprites/spr_bullet/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 12, + "height": 12 + }, + "colour": "#ff82a0" + } + }, + { + "prefab": "bullet_hell", + "kind": "sprite", + "name": "spr_ship", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/bullet_hell/sprites/spr_ship/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 20, + "height": 20 + }, + "colour": "#96f0c8" + } + }, + { + "prefab": "bullet_hell", + "kind": "sprite", + "name": "spr_shot", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/bullet_hell/sprites/spr_shot/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 6, + "height": 6 + }, + "colour": "#78dcff" + } + } + ] + }, + { + "name": "camera", + "version": "", + "display_name": "Follow camera", + "description": "A camera that follows a target with a deadzone it can move inside freely, eases toward it rather than snapping, and never shows outside the room. Three rules that are individually obvious and collectively fiddly — especially the clamp, which has to win over the ease or the view judders at the edges.", + "category": "library", + "tags": [ + "capability:camera", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/camera/README.md", + "thumbnail_path": "prefabs/camera/thumbnail.png", + "resources": [ + { + "prefab": "camera", + "kind": "script", + "name": "scr_camera", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "camera3d", + "version": "", + "display_name": "3D orbit camera", + "description": "Orbit camera as two vectors rather than Euler angles: Rodrigues rotation, an orthonormalised basis, and ray-sphere picking that reuses that basis.", + "category": "library", + "tags": [ + "capability:camera", + "capability:3d", + "capability:picking", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/camera3d/README.md", + "thumbnail_path": "prefabs/camera3d/thumbnail.png", + "resources": [ + { + "prefab": "camera3d", + "kind": "script", + "name": "scr_camera3d", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "camera3d", + "kind": "script", + "name": "scr_camera3d_math", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "camera3d", + "kind": "script", + "name": "scr_camera3d_pick", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "camera3d_basics", + "version": "", + "display_name": "3D orbit camera", + "description": "A trackball camera you drag to orbit, with mouse picking that agrees with what you see.", + "category": "component", + "tags": [ + "capability:camera", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/camera3d_basics/README.md", + "thumbnail_path": "prefabs/camera3d_basics/thumbnail.png", + "resources": [ + { + "prefab": "camera3d_basics", + "kind": "object", + "name": "obj_orbit_scene", + "description": null, + "tags": [], + "preview": null } ] }, @@ -8347,6 +9913,7 @@ "role:vfx" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/camera_shake/README.md", "thumbnail_path": "prefabs/camera_shake/thumbnail.png", "resources": [ @@ -8399,6 +9966,7 @@ "capability:touch" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/character_2d/README.md", "thumbnail_path": "prefabs/character_2d/thumbnail.png", "resources": [ @@ -8551,11 +10119,325 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 889, + "height": 1050 + }, + "colour": "#909092" } } ] }, + { + "name": "custom_mouse_pointer", + "version": "", + "display_name": "Custom mouse pointer", + "description": "Replace the OS cursor with something you draw in GUI space.", + "category": "component", + "tags": [ + "capability:ui", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/custom_mouse_pointer/README.md", + "thumbnail_path": "prefabs/custom_mouse_pointer/thumbnail.png", + "resources": [ + { + "prefab": "custom_mouse_pointer", + "kind": "object", + "name": "obj_pointer", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "dialogue", + "version": "", + "display_name": "Conversations", + "description": "Lines with a speaker, revealed at a characters-per-second rate, advanced by a key. The first press completes the line and the second advances it, so an impatient player never skips text unread. Includes a drawn box and an active flag to gate the game's own input on, or the player walks around while talking.", + "category": "library", + "tags": [ + "capability:dialogue", + "capability:ui", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/dialogue/README.md", + "thumbnail_path": "prefabs/dialogue/thumbnail.png", + "resources": [ + { + "prefab": "dialogue", + "kind": "script", + "name": "scr_dialogue", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "dialogue", + "kind": "script", + "name": "scr_dialogue_draw", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "dialogue", + "kind": "script", + "name": "scr_dialogue_read", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "economy", + "version": "", + "display_name": "Currency and prices", + "description": "A purse where spending checks affordability and deducts in one step, so a balance cannot go negative between the check and the charge. Plus a price that grows per purchase, how many more are affordable right now for a buy-max button, and thousands separators — a raw seven-digit number is unreadable at exactly the moment a player looks at it.", + "category": "library", + "tags": [ + "capability:economy", + "capability:currency", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/economy/README.md", + "thumbnail_path": "prefabs/economy/thumbnail.png", + "resources": [ + { + "prefab": "economy", + "kind": "script", + "name": "scr_economy", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "economy", + "kind": "script", + "name": "scr_economy_price", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "endless_runner", + "version": "", + "display_name": "Endless runner", + "description": "Jump the gaps on ground that never stops, and get further than last time.", + "category": "game", + "tags": [ + "genre:runner", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/endless_runner/README.md", + "thumbnail_path": "prefabs/endless_runner/thumbnail.png", + "resources": [ + { + "prefab": "endless_runner", + "kind": "object", + "name": "obj_ground", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "object", + "name": "obj_player", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "object", + "name": "obj_runner", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "script", + "name": "scr_runner_world", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "endless_runner", + "kind": "sprite", + "name": "spr_ground", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/endless_runner/sprites/spr_ground/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#465678" + } + }, + { + "prefab": "endless_runner", + "kind": "sprite", + "name": "spr_player", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/endless_runner/sprites/spr_player/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 30, + "height": 42 + }, + "colour": "#8cdcff" + } + } + ] + }, + { + "name": "enemy_ai", + "version": "", + "display_name": "Enemy behaviour", + "description": "One state field and the movements that go with it: chase, flee, wander with a hold time, patrol a route, and a sight radius with an optional line-of-sight check so walls hide you. Includes separation, without which a group of chasers converges into a single stack that reads as one enemy and lands every hit at once.", + "category": "library", + "tags": [ + "capability:ai", + "capability:enemies", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/enemy_ai/README.md", + "thumbnail_path": "prefabs/enemy_ai/thumbnail.png", + "resources": [ + { + "prefab": "enemy_ai", + "kind": "script", + "name": "scr_enemy_ai", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "enemy_ai", + "kind": "script", + "name": "scr_enemy_ai_move", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "enemy_ai", + "kind": "script", + "name": "scr_enemy_ai_patrol", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "falling_block", + "version": "", + "display_name": "Falling block", + "description": "Stack falling pieces, clear full rows, survive the speed ramp.", + "category": "game", + "tags": [ + "genre:puzzle", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/falling_block/README.md", + "thumbnail_path": "prefabs/falling_block/thumbnail.png", + "resources": [ + { + "prefab": "falling_block", + "kind": "object", + "name": "obj_stack", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "falling_block", + "kind": "room", + "name": "rm_stack", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "fantasy_platformer", "version": "", @@ -8571,6 +10453,7 @@ "projection:side-view" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/fantasy_platformer/README.md", "thumbnail_path": "prefabs/fantasy_platformer/thumbnail.png", "resources": [ @@ -10244,7 +12127,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#254b82" } }, { @@ -10267,7 +12155,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#2079b4" } }, { @@ -10290,7 +12183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#0ea4e3" } }, { @@ -10313,7 +12211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#0de4fa" } }, { @@ -10336,7 +12239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#015169" } }, { @@ -10359,7 +12267,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#0492b3" } }, { @@ -10382,7 +12295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1080 + }, + "colour": "#16a9c5" } }, { @@ -10405,7 +12323,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#58d6e9" } }, { @@ -10427,7 +12350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 335, + "height": 214 + }, + "colour": "#65186e" } }, { @@ -10484,7 +12412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 401, + "height": 300 + }, + "colour": "#792159" } }, { @@ -10533,7 +12466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 264, + "height": 401 + }, + "colour": "#792159" } }, { @@ -10570,7 +12508,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 271, + "height": 140 + }, + "colour": "#7a2257" } }, { @@ -10641,7 +12584,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 170 + }, + "colour": "#510699" } }, { @@ -10673,7 +12621,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 282, + "height": 165 + }, + "colour": "#792159" } }, { @@ -10710,7 +12663,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 262, + "height": 141 + }, + "colour": "#792159" } }, { @@ -10732,7 +12690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 54 + }, + "colour": "#467f8d" } }, { @@ -10754,7 +12717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 256 + }, + "colour": "#3f737f" } }, { @@ -10776,7 +12744,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 216, + "height": 100 + }, + "colour": "#457c8a" } }, { @@ -10798,7 +12771,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 168, + "height": 189 + }, + "colour": "#975e34" } }, { @@ -10820,7 +12798,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 527, + "height": 286 + }, + "colour": "#495250" } }, { @@ -10842,7 +12825,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 526, + "height": 290 + }, + "colour": "#342d3e" } }, { @@ -10864,7 +12852,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 254, + "height": 235 + }, + "colour": "#2b5662" } }, { @@ -10886,7 +12879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 254, + "height": 235 + }, + "colour": "#2a4851" } }, { @@ -10908,7 +12906,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 271, + "height": 237 + }, + "colour": "#6d3314" } }, { @@ -10930,7 +12933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 134, + "height": 164 + }, + "colour": "#974810" } }, { @@ -10952,7 +12960,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 166 + }, + "colour": "#9d4e16" } }, { @@ -10974,7 +12987,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 174 + }, + "colour": "#9a4c14" } }, { @@ -10996,7 +13014,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 156 + }, + "colour": "#9c4e16" } }, { @@ -11018,7 +13041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 159 + }, + "colour": "#9e4f16" } }, { @@ -11040,7 +13068,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a36d3c" } }, { @@ -11062,7 +13095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 256 + }, + "colour": "#a06837" } }, { @@ -11084,7 +13122,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 204, + "height": 256 + }, + "colour": "#9f6737" } }, { @@ -11106,7 +13149,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 150, + "height": 145 + }, + "colour": "#ad6127" } }, { @@ -11128,7 +13176,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 150, + "height": 145 + }, + "colour": "#743815" } }, { @@ -11150,7 +13203,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 151 + }, + "colour": "#9e5621" } }, { @@ -11172,7 +13230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 151 + }, + "colour": "#683112" } }, { @@ -11194,7 +13257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 142 + }, + "colour": "#a95e26" } }, { @@ -11216,7 +13284,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 142 + }, + "colour": "#703615" } }, { @@ -11238,7 +13311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 148 + }, + "colour": "#9b5321" } }, { @@ -11260,7 +13338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 148 + }, + "colour": "#653012" } }, { @@ -11282,7 +13365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 46 + }, + "colour": "#a7571b" } }, { @@ -11304,7 +13392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 46 + }, + "colour": "#6c320e" } }, { @@ -11326,7 +13419,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 55 + }, + "colour": "#9c4f18" } }, { @@ -11348,7 +13446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 55 + }, + "colour": "#642d0d" } }, { @@ -11370,7 +13473,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 46 + }, + "colour": "#a7571b" } }, { @@ -11392,7 +13500,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 46 + }, + "colour": "#6c320e" } }, { @@ -11414,7 +13527,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 55 + }, + "colour": "#9b4f18" } }, { @@ -11436,7 +13554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 55 + }, + "colour": "#642d0d" } }, { @@ -11458,7 +13581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 46 + }, + "colour": "#a7571b" } }, { @@ -11480,7 +13608,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 46 + }, + "colour": "#6d320e" } }, { @@ -11502,7 +13635,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 55 + }, + "colour": "#9d5019" } }, { @@ -11524,7 +13662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 55 + }, + "colour": "#652e0d" } }, { @@ -11546,7 +13689,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 47, + "height": 265 + }, + "colour": "#a5551b" } }, { @@ -11568,7 +13716,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 47, + "height": 265 + }, + "colour": "#6a310e" } }, { @@ -11590,7 +13743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 256 + }, + "colour": "#9b4f18" } }, { @@ -11612,7 +13770,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 256 + }, + "colour": "#632d0c" } }, { @@ -11634,7 +13797,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 49, + "height": 256 + }, + "colour": "#a4551a" } }, { @@ -11656,7 +13824,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 49, + "height": 256 + }, + "colour": "#6a310e" } }, { @@ -11678,7 +13851,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 256 + }, + "colour": "#984d18" } }, { @@ -11700,7 +13878,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 256 + }, + "colour": "#612c0c" } }, { @@ -11722,7 +13905,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 154 + }, + "colour": "#964b16" } }, { @@ -11744,7 +13932,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 166 + }, + "colour": "#924915" } }, { @@ -11766,7 +13959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 156 + }, + "colour": "#9d521b" } }, { @@ -11788,7 +13986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 169 + }, + "colour": "#994f1a" } }, { @@ -11810,7 +14013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 156 + }, + "colour": "#914612" } }, { @@ -11832,7 +14040,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 252, + "height": 167 + }, + "colour": "#8d4411" } }, { @@ -11854,7 +14067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 272, + "height": 373 + }, + "colour": "#655641" } }, { @@ -11876,7 +14094,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 275, + "height": 367 + }, + "colour": "#883f0f" } }, { @@ -11898,7 +14121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#d7b981" } }, { @@ -11920,7 +14148,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 256 + }, + "colour": "#cfad75" } }, { @@ -11942,7 +14175,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 204, + "height": 256 + }, + "colour": "#ceac75" } }, { @@ -11964,7 +14202,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#35616d" } }, { @@ -11986,7 +14229,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#17313b" } }, { @@ -12008,7 +14256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#325d68" } }, { @@ -12030,7 +14283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#162e38" } }, { @@ -12052,7 +14310,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#2c545e" } }, { @@ -12074,7 +14337,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#132a33" } }, { @@ -12096,7 +14364,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 158, + "height": 256 + }, + "colour": "#3c6d79" } }, { @@ -12118,7 +14391,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 158, + "height": 256 + }, + "colour": "#1b3844" } }, { @@ -12140,7 +14418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 164, + "height": 256 + }, + "colour": "#3b6c78" } }, { @@ -12162,7 +14445,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 164, + "height": 256 + }, + "colour": "#1a3743" } }, { @@ -12184,7 +14472,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c3407" } }, { @@ -12206,7 +14499,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#451b04" } }, { @@ -12228,7 +14526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 192, + "height": 256 + }, + "colour": "#7e3609" } }, { @@ -12250,7 +14553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 192, + "height": 256 + }, + "colour": "#471c04" } }, { @@ -12272,7 +14580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 189, + "height": 256 + }, + "colour": "#833808" } }, { @@ -12294,7 +14607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 189, + "height": 256 + }, + "colour": "#4a1d04" } }, { @@ -12316,7 +14634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 169, + "height": 143 + }, + "colour": "#3d727f" } }, { @@ -12338,7 +14661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 177, + "height": 150 + }, + "colour": "#385d66" } }, { @@ -12360,7 +14688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 169, + "height": 143 + }, + "colour": "#3b6069" } }, { @@ -12382,7 +14715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 131 + }, + "colour": "#85431c" } }, { @@ -12404,7 +14742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 159, + "height": 136 + }, + "colour": "#82411b" } }, { @@ -12426,7 +14769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 135, + "height": 232 + }, + "colour": "#9e7441" } }, { @@ -12469,7 +14817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 257, + "height": 255 + }, + "colour": "#9e7441" } }, { @@ -12530,7 +14883,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 241 + }, + "colour": "#9e7441" } }, { @@ -12552,7 +14910,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 201, + "height": 229 + }, + "colour": "#a9893f" } }, { @@ -12574,7 +14937,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 28 + }, + "colour": "#285675" } }, { @@ -12602,7 +14970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 238 + }, + "colour": "#a77f2f" } }, { @@ -12653,7 +15026,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 649, + "height": 257 + }, + "colour": "#a98032" } }, { @@ -12683,7 +15061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 336, + "height": 359 + }, + "colour": "#a9873e" } }, { @@ -12749,7 +15132,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 374, + "height": 276 + }, + "colour": "#aa7e2d" } }, { @@ -12816,7 +15204,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 374, + "height": 276 + }, + "colour": "#aa802c" } }, { @@ -12877,7 +15270,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 346 + }, + "colour": "#ab7f2e" } }, { @@ -12939,7 +15337,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 346 + }, + "colour": "#ac802c" } }, { @@ -13001,7 +15404,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 308, + "height": 346 + }, + "colour": "#aa863e" } }, { @@ -13068,7 +15476,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 374, + "height": 276 + }, + "colour": "#a9863e" } }, { @@ -13102,7 +15515,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 204, + "height": 242 + }, + "colour": "#a77e32" } }, { @@ -13137,7 +15555,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 249, + "height": 256 + }, + "colour": "#a97f30" } }, { @@ -13172,7 +15595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 293, + "height": 242 + }, + "colour": "#a78643" } }, { @@ -13215,7 +15643,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 234 + }, + "colour": "#aa7e2d" } }, { @@ -13259,7 +15692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 248, + "height": 234 + }, + "colour": "#aa802c" } }, { @@ -13303,7 +15741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 248, + "height": 234 + }, + "colour": "#a9873e" } }, { @@ -13331,7 +15774,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 225 + }, + "colour": "#a77e30" } }, { @@ -13360,7 +15808,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 239 + }, + "colour": "#aa802c" } }, { @@ -13389,7 +15842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 234, + "height": 283 + }, + "colour": "#a88640" } }, { @@ -13414,7 +15872,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 198, + "height": 383 + }, + "colour": "#a77e30" } }, { @@ -13440,7 +15903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 243, + "height": 383 + }, + "colour": "#aa802c" } }, { @@ -13466,7 +15934,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 230, + "height": 383 + }, + "colour": "#a88640" } }, { @@ -13493,7 +15966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 207, + "height": 339 + }, + "colour": "#a98131" } }, { @@ -13521,7 +15999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 248, + "height": 339 + }, + "colour": "#aa822f" } }, { @@ -13549,7 +16032,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 238, + "height": 379 + }, + "colour": "#a98841" } }, { @@ -13584,7 +16072,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 256 + }, + "colour": "#ad7f2c" } }, { @@ -13625,7 +16118,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 214, + "height": 269 + }, + "colour": "#ad7f2c" } }, { @@ -13667,7 +16165,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 269 + }, + "colour": "#ae802b" } }, { @@ -13709,7 +16212,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 260, + "height": 269 + }, + "colour": "#ac873c" } }, { @@ -13758,7 +16266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 164, + "height": 267 + }, + "colour": "#a97e2f" } }, { @@ -13808,7 +16321,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 226, + "height": 274 + }, + "colour": "#aa802e" } }, { @@ -13858,7 +16376,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 226, + "height": 267 + }, + "colour": "#a98740" } }, { @@ -13903,7 +16426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#928032" } }, { @@ -13925,7 +16453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 103, + "height": 144 + }, + "colour": "#938132" } }, { @@ -13978,7 +16511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 66 + }, + "colour": "#cc7d30" } }, { @@ -14031,7 +16569,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 66 + }, + "colour": "#e9be29" } }, { @@ -14084,7 +16627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 66 + }, + "colour": "#7bb4ce" } }, { @@ -14106,7 +16654,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 65, + "height": 64 + }, + "colour": "#e8be29" } }, { @@ -14151,7 +16704,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 159, + "height": 146 + }, + "colour": "#eec923" } }, { @@ -14196,7 +16754,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 159, + "height": 146 + }, + "colour": "#97bad6" } }, { @@ -14218,7 +16781,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 135 + }, + "colour": "#eec923" } }, { @@ -14263,7 +16831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 86, + "height": 156 + }, + "colour": "#a5b7a1" } }, { @@ -14285,7 +16858,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 146 + }, + "colour": "#a5b7a2" } }, { @@ -14307,7 +16885,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 333, + "height": 128 + }, + "colour": "#7a6357" } }, { @@ -14329,7 +16912,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 138, + "height": 119 + }, + "colour": "#74340e" } }, { @@ -14351,7 +16939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 59, + "height": 47 + }, + "colour": "#796652" } }, { @@ -14373,7 +16966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 196, + "height": 126 + }, + "colour": "#bbac78" } }, { @@ -14395,7 +16993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 105, + "height": 90 + }, + "colour": "#cab77a" } }, { @@ -14417,7 +17020,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 148, + "height": 120 + }, + "colour": "#bbad76" } }, { @@ -14439,7 +17047,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 32 + }, + "colour": "#6b463d" } }, { @@ -14461,7 +17074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 78, + "height": 47 + }, + "colour": "#5a4e4f" } }, { @@ -14483,7 +17101,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 75, + "height": 58 + }, + "colour": "#53372f" } }, { @@ -14505,7 +17128,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 95, + "height": 63 + }, + "colour": "#484051" } }, { @@ -14528,7 +17156,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 274 + }, + "colour": "#6a2f11" } }, { @@ -14550,7 +17183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 30, + "height": 84 + }, + "colour": "#f2d570" } }, { @@ -14596,7 +17234,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 94 + }, + "colour": "#f1d373" } }, { @@ -14618,7 +17261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 64 + }, + "colour": "#eece78" } }, { @@ -14664,7 +17312,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 75 + }, + "colour": "#f2d372" } }, { @@ -14686,7 +17339,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 45 + }, + "colour": "#eecd77" } }, { @@ -14732,7 +17390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 62 + }, + "colour": "#f3d671" } }, { @@ -14754,7 +17417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 34 + }, + "colour": "#edcd79" } }, { @@ -14776,7 +17444,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 52, + "height": 75 + }, + "colour": "#603d1f" } }, { @@ -14798,7 +17471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 212 + }, + "colour": "#5c3a1d" } }, { @@ -14820,7 +17498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 212 + }, + "colour": "#5f89a9" } }, { @@ -14842,7 +17525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 52, + "height": 75 + }, + "colour": "#618ba7" } }, { @@ -14864,7 +17552,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 136, + "height": 171 + }, + "colour": "#954a17" } }, { @@ -14886,7 +17579,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 126, + "height": 170 + }, + "colour": "#954a16" } }, { @@ -14909,7 +17607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 255 + }, + "colour": "#56341a" } }, { @@ -14932,7 +17635,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 213, + "height": 255 + }, + "colour": "#6089a3" } }, { @@ -14975,7 +17683,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 321, + "height": 306 + }, + "colour": "#5b6d66" } }, { @@ -14997,7 +17710,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 301 + }, + "colour": "#5b6d66" } }, { @@ -15019,7 +17737,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 321, + "height": 304 + }, + "colour": "#48666b" } }, { @@ -15041,7 +17764,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 185, + "height": 117 + }, + "colour": "#f1ebd4" } }, { @@ -15063,7 +17791,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 276, + "height": 71 + }, + "colour": "#813c10" } }, { @@ -15085,7 +17818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 353, + "height": 117 + }, + "colour": "#813b10" } }, { @@ -15107,7 +17845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 142, + "height": 89 + }, + "colour": "#9e6b22" } }, { @@ -15129,7 +17872,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 47 + }, + "colour": "#9ab5b5" } }, { @@ -15151,7 +17899,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 44, + "height": 45 + }, + "colour": "#96aba5" } }, { @@ -15173,7 +17926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 48 + }, + "colour": "#91adae" } }, { @@ -15195,7 +17953,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 48 + }, + "colour": "#a58d96" } }, { @@ -15217,7 +17980,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 44, + "height": 45 + }, + "colour": "#b7a475" } }, { @@ -15239,7 +18007,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 47 + }, + "colour": "#8ba6bc" } }, { @@ -15261,7 +18034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 69 + }, + "colour": "#9e7dc1" } }, { @@ -15283,7 +18061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 52, + "height": 72 + }, + "colour": "#8e6cea" } }, { @@ -15305,7 +18088,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 58 + }, + "colour": "#989899" } }, { @@ -15327,7 +18115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 29, + "height": 55 + }, + "colour": "#68a794" } }, { @@ -15349,7 +18142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 47, + "height": 69 + }, + "colour": "#6eaede" } }, { @@ -15371,7 +18169,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 52, + "height": 72 + }, + "colour": "#71b1e2" } }, { @@ -15393,7 +18196,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 56 + }, + "colour": "#72a9d3" } }, { @@ -15415,7 +18223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 29, + "height": 55 + }, + "colour": "#77acd5" } }, { @@ -15437,7 +18250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 108, + "height": 153 + }, + "colour": "#5f6f1c" } }, { @@ -15459,7 +18277,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 152 + }, + "colour": "#69731c" } }, { @@ -15481,7 +18304,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 53, + "height": 116 + }, + "colour": "#a98340" } }, { @@ -15503,7 +18331,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 74, + "height": 133 + }, + "colour": "#a5705c" } }, { @@ -15525,7 +18358,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 106, + "height": 33 + }, + "colour": "#d6b05d" } }, { @@ -15547,7 +18385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 127, + "height": 29 + }, + "colour": "#f0cc6e" } }, { @@ -15569,7 +18412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 115, + "height": 151 + }, + "colour": "#ecca70" } }, { @@ -15591,7 +18439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 64 + }, + "colour": "#8f4614" } }, { @@ -15613,7 +18466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 100, + "height": 64 + }, + "colour": "#8c4413" } }, { @@ -15635,7 +18493,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 64 + }, + "colour": "#8f4614" } }, { @@ -15657,7 +18520,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 24 + }, + "colour": "#476e7a" } }, { @@ -15679,7 +18547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 54, + "height": 14 + }, + "colour": "#406471" } }, { @@ -15701,7 +18574,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 14 + }, + "colour": "#486e7a" } }, { @@ -15723,7 +18601,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 69, + "height": 21 + }, + "colour": "#436774" } }, { @@ -15745,7 +18628,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 69 + }, + "colour": "#5a565a" } }, { @@ -15767,7 +18655,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 69 + }, + "colour": "#903c24" } }, { @@ -15789,7 +18682,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 102, + "height": 198 + }, + "colour": "#55829e" } }, { @@ -15811,7 +18709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 116 + }, + "colour": "#7d390f" } }, { @@ -15854,7 +18757,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 75, + "height": 208 + }, + "colour": "#9b7e4a" } }, { @@ -15876,7 +18784,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 187 + }, + "colour": "#9b7e4a" } }, { @@ -15898,7 +18811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 101 + }, + "colour": "#606d6d" } }, { @@ -15920,7 +18838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 207, + "height": 196 + }, + "colour": "#814b26" } }, { @@ -15942,7 +18865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 58, + "height": 78 + }, + "colour": "#8c9c40" } }, { @@ -15964,7 +18892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 99 + }, + "colour": "#869b3f" } }, { @@ -15986,7 +18919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 165, + "height": 76 + }, + "colour": "#577b88" } }, { @@ -16008,7 +18946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 151, + "height": 153 + }, + "colour": "#835635" } }, { @@ -16055,7 +18998,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 189 + }, + "colour": "#845735" } }, { @@ -16077,7 +19025,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 148, + "height": 151 + }, + "colour": "#845735" } }, { @@ -16099,7 +19052,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 314, + "height": 181 + }, + "colour": "#9d5523" } }, { @@ -16121,7 +19079,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 305, + "height": 256 + }, + "colour": "#b98450" } }, { @@ -16143,7 +19106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 393, + "height": 395 + }, + "colour": "#3d8095" } }, { @@ -16165,7 +19133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 393, + "height": 395 + }, + "colour": "#2c3e71" } }, { @@ -16187,7 +19160,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 105, + "height": 143 + }, + "colour": "#79624d" } }, { @@ -16220,7 +19198,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 78, + "height": 118 + }, + "colour": "#c238a6" } }, { @@ -16281,7 +19264,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 76, + "height": 65 + }, + "colour": "#c63da1" } }, { @@ -16303,7 +19291,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 74, + "height": 60 + }, + "colour": "#c43ba4" } }, { @@ -16346,7 +19339,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 379, + "height": 295 + }, + "colour": "#557675" } }, { @@ -16368,7 +19366,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 376, + "height": 292 + }, + "colour": "#567675" } }, { @@ -16390,7 +19393,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 379, + "height": 295 + }, + "colour": "#416e7a" } }, { @@ -16412,7 +19420,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 140, + "height": 121 + }, + "colour": "#984f1e" } }, { @@ -16474,7 +19487,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 244, + "height": 182 + }, + "colour": "#9a511f" } }, { @@ -16496,7 +19514,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 119 + }, + "colour": "#9a501e" } }, { @@ -16518,7 +19541,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 221, + "height": 269 + }, + "colour": "#0b75c5" } }, { @@ -16540,7 +19568,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 314, + "height": 239 + }, + "colour": "#1074d4" } }, { @@ -16562,7 +19595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 143, + "height": 220 + }, + "colour": "#0f75d3" } }, { @@ -16584,7 +19622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 188, + "height": 223 + }, + "colour": "#1c78e2" } }, { @@ -16606,7 +19649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 81, + "height": 96 + }, + "colour": "#9a8d8a" } }, { @@ -16628,7 +19676,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 94 + }, + "colour": "#8ad266" } }, { @@ -16650,7 +19703,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 365 + }, + "colour": "#8e481b" } }, { @@ -16672,7 +19730,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 198, + "height": 372 + }, + "colour": "#9b5322" } }, { @@ -16694,7 +19757,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 20, + "height": 18 + }, + "colour": "#3be20e" } }, { @@ -16716,7 +19784,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 51, + "height": 17 + }, + "colour": "#3de605" } }, { @@ -16738,7 +19811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 27, + "height": 25 + }, + "colour": "#39e00d" } }, { @@ -16760,7 +19838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 32 + }, + "colour": "#38e00d" } }, { @@ -16782,7 +19865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 47, + "height": 27 + }, + "colour": "#3be10c" } }, { @@ -16804,7 +19892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 55, + "height": 25 + }, + "colour": "#3ae10d" } }, { @@ -16826,7 +19919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 35, + "height": 18 + }, + "colour": "#3de30f" } }, { @@ -16848,7 +19946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 196 + }, + "colour": "#6d5542" } }, { @@ -16922,7 +20025,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 162, + "height": 208 + }, + "colour": "#99846b" } }, { @@ -16944,7 +20052,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 462, + "height": 215 + }, + "colour": "#37a90c" } }, { @@ -16966,7 +20079,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 460, + "height": 215 + }, + "colour": "#095e00" } }, { @@ -16988,7 +20106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 593, + "height": 234 + }, + "colour": "#35a309" } }, { @@ -17010,7 +20133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 580, + "height": 234 + }, + "colour": "#126801" } }, { @@ -17032,7 +20160,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 553, + "height": 255 + }, + "colour": "#36a30a" } }, { @@ -17054,7 +20187,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 553, + "height": 255 + }, + "colour": "#0f6100" } }, { @@ -17076,7 +20214,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 219, + "height": 338 + }, + "colour": "#ab7f53" } }, { @@ -17098,7 +20241,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 264, + "height": 164 + }, + "colour": "#5e5b56" } }, { @@ -17121,7 +20269,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 264, + "height": 201 + }, + "colour": "#585551" } }, { @@ -17143,7 +20296,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 67, + "height": 70 + }, + "colour": "#eb8e36" } }, { @@ -17165,7 +20323,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 85, + "height": 58 + }, + "colour": "#d87463" } }, { @@ -17187,7 +20350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 56, + "height": 64 + }, + "colour": "#be5fcd" } }, { @@ -17209,7 +20377,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 56, + "height": 64 + }, + "colour": "#4789e5" } }, { @@ -17231,7 +20404,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 61, + "height": 49 + }, + "colour": "#dd7465" } }, { @@ -17253,7 +20431,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 223 + }, + "colour": "#339a19" } }, { @@ -17275,7 +20458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 207, + "height": 183 + }, + "colour": "#20860c" } }, { @@ -17297,7 +20485,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 102 + }, + "colour": "#0ca418" } }, { @@ -17319,7 +20512,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 122, + "height": 100 + }, + "colour": "#0f9118" } }, { @@ -17341,7 +20539,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 284 + }, + "colour": "#3b7d93" } }, { @@ -17363,7 +20566,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 284 + }, + "colour": "#2a3d6f" } }, { @@ -17385,7 +20593,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 306 + }, + "colour": "#3d8096" } }, { @@ -17407,7 +20620,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 318, + "height": 306 + }, + "colour": "#2b3e72" } }, { @@ -17429,7 +20647,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 483, + "height": 217 + }, + "colour": "#3c8097" } }, { @@ -17451,7 +20674,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 483, + "height": 217 + }, + "colour": "#2b3d73" } }, { @@ -17473,7 +20701,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 270, + "height": 77 + }, + "colour": "#3b7c91" } }, { @@ -17495,7 +20728,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 270, + "height": 77 + }, + "colour": "#2a3c6e" } }, { @@ -17517,7 +20755,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 76 + }, + "colour": "#3d7f93" } }, { @@ -17539,7 +20782,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 76 + }, + "colour": "#2c3e70" } }, { @@ -17561,7 +20809,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 387, + "height": 121 + }, + "colour": "#408498" } }, { @@ -17583,7 +20836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 387, + "height": 121 + }, + "colour": "#2e4174" } }, { @@ -17605,7 +20863,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 270, + "height": 84 + }, + "colour": "#3c7e93" } }, { @@ -17627,7 +20890,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 270, + "height": 84 + }, + "colour": "#2a3d6f" } }, { @@ -17649,7 +20917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 214 + }, + "colour": "#a05825" } }, { @@ -17671,7 +20944,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 227, + "height": 341 + }, + "colour": "#824e2b" } }, { @@ -17745,7 +21023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 341 + }, + "colour": "#966842" } }, { @@ -17767,7 +21050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 246, + "height": 191 + }, + "colour": "#684029" } }, { @@ -17789,7 +21077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 258, + "height": 53 + }, + "colour": "#604d40" } }, { @@ -17811,7 +21104,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 247, + "height": 191 + }, + "colour": "#68402a" } }, { @@ -17865,7 +21163,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 242, + "height": 227 + }, + "colour": "#b67018" } }, { @@ -17887,7 +21190,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 184, + "height": 121 + }, + "colour": "#b67018" } }, { @@ -17910,7 +21218,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 229, + "height": 195 + }, + "colour": "#ab6813" } }, { @@ -17964,7 +21277,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 240, + "height": 227 + }, + "colour": "#75655b" } }, { @@ -17986,7 +21304,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 184, + "height": 121 + }, + "colour": "#75655b" } }, { @@ -18009,7 +21332,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 228, + "height": 195 + }, + "colour": "#615a59" } }, { @@ -18031,7 +21359,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 226, + "height": 193 + }, + "colour": "#ad6a13" } }, { @@ -18053,7 +21386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 179, + "height": 48 + }, + "colour": "#feef3d" } }, { @@ -18075,7 +21413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 741, + "height": 745 + }, + "colour": "#33900d" } }, { @@ -18097,7 +21440,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 685, + "height": 853 + }, + "colour": "#2b7e09" } }, { @@ -18119,7 +21467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 691, + "height": 804 + }, + "colour": "#2f8508" } }, { @@ -18142,7 +21495,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 562, + "height": 186 + }, + "colour": "#356b02" } }, { @@ -18164,7 +21522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 364, + "height": 339 + }, + "colour": "#775538" } }, { @@ -18186,7 +21549,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 192, + "height": 215 + }, + "colour": "#9d8152" } }, { @@ -18208,7 +21576,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 28 + }, + "colour": "#995016" } }, { @@ -18265,7 +21638,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 315, + "height": 252 + }, + "colour": "#998052" } }, { @@ -18327,7 +21705,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 550, + "height": 201 + }, + "colour": "#bfa86e" } }, { @@ -18374,7 +21757,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 409, + "height": 365 + }, + "colour": "#bfa86e" } }, { @@ -18424,7 +21812,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 345, + "height": 284 + }, + "colour": "#998052" } }, { @@ -18473,7 +21866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 393, + "height": 305 + }, + "colour": "#bfa86e" } }, { @@ -18510,7 +21908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 230, + "height": 238 + }, + "colour": "#977b4e" } }, { @@ -18547,7 +21950,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 276, + "height": 212 + }, + "colour": "#bea76d" } }, { @@ -18591,7 +21999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 204, + "height": 218 + }, + "colour": "#997e51" } }, { @@ -18635,7 +22048,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 235, + "height": 199 + }, + "colour": "#bfa86e" } }, { @@ -18681,7 +22099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 228, + "height": 251 + }, + "colour": "#967c4f" } }, { @@ -18727,7 +22150,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 270, + "height": 234 + }, + "colour": "#c3ad72" } }, { @@ -18781,7 +22209,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 193, + "height": 241 + }, + "colour": "#997f51" } }, { @@ -18835,7 +22268,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 230, + "height": 223 + }, + "colour": "#bfa86e" } }, { @@ -18857,7 +22295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 140, + "height": 80 + }, + "colour": "#973f31" } }, { @@ -18906,7 +22349,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 101 + }, + "colour": "#983f31" } }, { @@ -18948,7 +22396,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 113 + }, + "colour": "#983f31" } }, { @@ -18982,7 +22435,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 173, + "height": 97 + }, + "colour": "#983f31" } }, { @@ -19023,7 +22481,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 151, + "height": 90 + }, + "colour": "#983f31" } }, { @@ -19064,7 +22527,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 147, + "height": 88 + }, + "colour": "#974032" } }, { @@ -19086,7 +22554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 2304 + }, + "colour": "#4f382c" } }, { @@ -19109,7 +22582,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 2304 + }, + "colour": "#424108" } }, { @@ -19132,7 +22610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 2304 + }, + "colour": "#2a3d46" } }, { @@ -19154,7 +22637,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 2304 + }, + "colour": "#5f321b" } }, { @@ -19226,6 +22714,7 @@ "role:building" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/fantasy_town/README.md", "thumbnail_path": "prefabs/fantasy_town/thumbnail.png", "resources": [ @@ -19249,7 +22738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8d625b" } }, { @@ -19272,7 +22766,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#967271" } }, { @@ -19295,7 +22794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#937072" } }, { @@ -19318,7 +22822,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b68676" } }, { @@ -19341,7 +22850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#885c61" } }, { @@ -19364,7 +22878,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b0807b" } }, { @@ -19387,7 +22906,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#926d64" } }, { @@ -19410,7 +22934,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#927476" } }, { @@ -19433,7 +22962,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a66469" } }, { @@ -19456,7 +22990,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#95767f" } }, { @@ -19479,7 +23018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#915b66" } }, { @@ -19502,7 +23046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#916360" } }, { @@ -19525,7 +23074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#af6869" } }, { @@ -19548,7 +23102,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#83686b" } }, { @@ -19571,7 +23130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8b5e63" } }, { @@ -19594,7 +23158,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a3715b" } }, { @@ -19617,7 +23186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7c7d89" } }, { @@ -19640,7 +23214,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8d8285" } }, { @@ -19663,7 +23242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#797484" } }, { @@ -19686,7 +23270,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#878589" } }, { @@ -19709,7 +23298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b3664b" } }, { @@ -19732,7 +23326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c57154" } }, { @@ -19755,7 +23354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c47b5f" } }, { @@ -19778,7 +23382,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b27559" } }, { @@ -19801,7 +23410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#50719f" } }, { @@ -19824,7 +23438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#506088" } }, { @@ -19847,7 +23466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#898b9b" } }, { @@ -19870,7 +23494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#637089" } }, { @@ -19893,7 +23522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bf886d" } }, { @@ -19916,7 +23550,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b27666" } }, { @@ -19939,7 +23578,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a47267" } }, { @@ -19962,7 +23606,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bf816a" } }, { @@ -19985,7 +23634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#876150" } }, { @@ -20008,7 +23662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9e6c50" } }, { @@ -20031,7 +23690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9b5e49" } }, { @@ -20054,7 +23718,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a1634c" } }, { @@ -20077,7 +23746,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#857163" } }, { @@ -20100,7 +23774,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#997765" } }, { @@ -20123,7 +23802,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9c7a65" } }, { @@ -20146,7 +23830,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7f6760" } }, { @@ -20169,7 +23858,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#995b52" } }, { @@ -20192,7 +23886,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ab7563" } }, { @@ -20215,7 +23914,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a16e62" } }, { @@ -20238,7 +23942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9a5a4a" } }, { @@ -20261,7 +23970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#4e68a8" } }, { @@ -20284,7 +23998,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#87717a" } }, { @@ -20307,7 +24026,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#726677" } }, { @@ -20330,7 +24054,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#6f6679" } }, { @@ -20353,7 +24082,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#5e633b" } }, { @@ -20376,7 +24110,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#4c5a39" } }, { @@ -20399,7 +24138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#56603a" } }, { @@ -20422,7 +24166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#4d5738" } }, { @@ -20445,7 +24194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bd8163" } }, { @@ -20468,7 +24222,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b67863" } }, { @@ -20491,7 +24250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b47e6f" } }, { @@ -20514,7 +24278,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b88067" } }, { @@ -20537,7 +24306,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c59771" } }, { @@ -20560,7 +24334,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c89a82" } }, { @@ -20583,7 +24362,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ce9c83" } }, { @@ -20606,7 +24390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c3977c" } }, { @@ -20629,7 +24418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c29c8b" } }, { @@ -20652,7 +24446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bc9789" } }, { @@ -20675,7 +24474,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb9685" } }, { @@ -20698,7 +24502,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c39885" } }, { @@ -20721,7 +24530,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8e859c" } }, { @@ -20744,7 +24558,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8b7f9f" } }, { @@ -20767,7 +24586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9e798a" } }, { @@ -20790,7 +24614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ae7c8a" } }, { @@ -20813,7 +24642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b77366" } }, { @@ -20836,7 +24670,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a77573" } }, { @@ -20859,7 +24698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a37572" } }, { @@ -20882,7 +24726,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b8736a" } }, { @@ -20905,7 +24754,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7b8093" } }, { @@ -20928,7 +24782,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7e7d8c" } }, { @@ -20951,7 +24810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#808192" } }, { @@ -20974,7 +24838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#838392" } }, { @@ -20997,7 +24866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb7354" } }, { @@ -21020,7 +24894,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bf6f57" } }, { @@ -21043,7 +24922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c57654" } }, { @@ -21066,7 +24950,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bd715a" } }, { @@ -21089,7 +24978,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7287a0" } }, { @@ -21112,7 +25006,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#6e7b9a" } }, { @@ -21135,7 +25034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#69779a" } }, { @@ -21158,7 +25062,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#667993" } }, { @@ -21181,7 +25090,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a47267" } }, { @@ -21204,7 +25118,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a06d64" } }, { @@ -21227,7 +25146,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#aa7366" } }, { @@ -21250,7 +25174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b3776b" } }, { @@ -21273,7 +25202,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b57158" } }, { @@ -21296,7 +25230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb7b5e" } }, { @@ -21319,7 +25258,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb755e" } }, { @@ -21342,7 +25286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b5705a" } }, { @@ -21365,7 +25314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a07b66" } }, { @@ -21388,7 +25342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9d7b71" } }, { @@ -21411,7 +25370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a48478" } }, { @@ -21434,7 +25398,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a87f6d" } }, { @@ -21457,7 +25426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#cd7760" } }, { @@ -21480,7 +25454,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bd856f" } }, { @@ -21503,7 +25482,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ca7b65" } }, { @@ -21526,7 +25510,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b87b65" } }, { @@ -21549,7 +25538,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#6889b9" } }, { @@ -21572,7 +25566,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#98807e" } }, { @@ -21595,7 +25594,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#6986ba" } }, { @@ -21618,7 +25622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#887c87" } }, { @@ -21641,7 +25650,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#687541" } }, { @@ -21664,7 +25678,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#667441" } }, { @@ -21687,7 +25706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#5e6b44" } }, { @@ -21710,7 +25734,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#616e46" } }, { @@ -21733,7 +25762,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c38365" } }, { @@ -21756,7 +25790,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b87e6e" } }, { @@ -21779,7 +25818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bd7f6a" } }, { @@ -21802,7 +25846,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ba816c" } }, { @@ -21825,7 +25874,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c59771" } }, { @@ -21848,7 +25902,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c89a82" } }, { @@ -21871,7 +25930,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ce9c83" } }, { @@ -21894,7 +25958,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c3977c" } }, { @@ -21917,7 +25986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b87267" } }, { @@ -21940,7 +26014,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a57875" } }, { @@ -21963,7 +26042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b2786b" } }, { @@ -21986,7 +26070,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b37a72" } }, { @@ -22009,7 +26098,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c59c85" } }, { @@ -22032,7 +26126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb9688" } }, { @@ -22055,7 +26154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c29e89" } }, { @@ -22078,7 +26182,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#cd9f87" } }, { @@ -22101,7 +26210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ad7885" } }, { @@ -22124,7 +26238,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#89839c" } }, { @@ -22147,7 +26266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9c8a97" } }, { @@ -22170,7 +26294,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#95829e" } }, { @@ -22193,7 +26322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#757b8f" } }, { @@ -22216,7 +26350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#757b8f" } }, { @@ -22239,7 +26378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#817f8f" } }, { @@ -22262,7 +26406,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#818292" } }, { @@ -22285,7 +26434,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#d07d55" } }, { @@ -22308,7 +26462,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#d17759" } }, { @@ -22331,7 +26490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bf785d" } }, { @@ -22354,7 +26518,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bc765d" } }, { @@ -22377,7 +26546,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#738ca7" } }, { @@ -22400,7 +26574,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7182a3" } }, { @@ -22423,7 +26602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#6f7e96" } }, { @@ -22446,7 +26630,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#707c97" } }, { @@ -22469,7 +26658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bf7d6d" } }, { @@ -22492,7 +26686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b77969" } }, { @@ -22515,7 +26714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b87f6e" } }, { @@ -22538,7 +26742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#986a65" } }, { @@ -22561,7 +26770,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b4725c" } }, { @@ -22584,7 +26798,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b8785d" } }, { @@ -22607,7 +26826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ca8161" } }, { @@ -22630,7 +26854,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb6e55" } }, { @@ -22653,7 +26882,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a57f6e" } }, { @@ -22676,7 +26910,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ac8678" } }, { @@ -22699,7 +26938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a07b6f" } }, { @@ -22722,7 +26966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a17a68" } }, { @@ -22745,7 +26994,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#d37d67" } }, { @@ -22768,7 +27022,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bb8973" } }, { @@ -22791,7 +27050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c6826b" } }, { @@ -22814,7 +27078,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c2755d" } }, { @@ -22837,7 +27106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#698ebf" } }, { @@ -22860,7 +27134,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9f8076" } }, { @@ -22883,7 +27162,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7a84a2" } }, { @@ -22906,7 +27190,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#7e84a3" } }, { @@ -22929,7 +27218,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#687740" } }, { @@ -22952,7 +27246,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#66753d" } }, { @@ -22975,7 +27274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#67763d" } }, { @@ -22998,7 +27302,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#637040" } }, { @@ -23021,7 +27330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c38369" } }, { @@ -23044,7 +27358,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#b48071" } }, { @@ -23067,7 +27386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#bd7c66" } }, { @@ -23090,11 +27414,218 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#c68562" } } ] }, + { + "name": "feel", + "version": "", + "display_name": "Feel", + "description": "Tweens and easing, hit-stop, screen shake, floating score popups and squash-and-stretch. Deliberately depends on nothing, so it can be retuned and re-released without moving the kernel.", + "category": "library", + "tags": [ + "capability:tween", + "capability:screenshake", + "role:vfx", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/feel/README.md", + "thumbnail_path": "prefabs/feel/thumbnail.png", + "resources": [ + { + "prefab": "feel", + "kind": "object", + "name": "obj_feel", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "feel", + "kind": "script", + "name": "scr_feel", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "feel", + "kind": "script", + "name": "scr_feel_pop", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "feel", + "kind": "script", + "name": "scr_feel_shake", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "font_system", + "version": "", + "display_name": "Font from a host family", + "description": "Reference an installed font by family name — no TTF in the repo.", + "category": "component", + "tags": [ + "capability:text", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/font_system/README.md", + "thumbnail_path": "prefabs/font_system/thumbnail.png", + "resources": [ + { + "prefab": "font_system", + "kind": "font", + "name": "fnt_main", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "grid", + "version": "", + "display_name": "Grid board", + "description": "A cell container with cell/pixel conversion and neighbour queries, occupancy tests, row detection and collapse, and snapshot/restore for undo. Four small concepts in one pack because a puzzle is not real without undo.", + "category": "library", + "tags": [ + "capability:grid", + "capability:pathfinding", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/grid/README.md", + "thumbnail_path": "prefabs/grid/thumbnail.png", + "resources": [ + { + "prefab": "grid", + "kind": "script", + "name": "scr_grid", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "grid", + "kind": "script", + "name": "scr_grid_rows", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "grid", + "kind": "script", + "name": "scr_grid_space", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "grid", + "kind": "script", + "name": "scr_grid_undo", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "hazard", + "version": "", + "display_name": "Hazards and checkpoints", + "description": "Respawn points that move as the player passes checkpoints, a respawn that stops the instance dead rather than putting it back at speed, a grace window after it, knockback for hazards that hurt instead of killing, and a fell-out-of-the-room check for the pit that is not an object.", + "category": "library", + "tags": [ + "capability:hazards", + "capability:checkpoints", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/hazard/README.md", + "thumbnail_path": "prefabs/hazard/thumbnail.png", + "resources": [ + { + "prefab": "hazard", + "kind": "script", + "name": "scr_hazard", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "hazard", + "kind": "script", + "name": "scr_hazard_checkpoint", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "health", + "version": "", + "display_name": "Health, damage and invulnerability", + "description": "Hit points with a maximum, damage that respects a window of invulnerability after each hit, healing that cannot overfill, and a bar to draw it with. The window is the whole point: without one an enemy that stays overlapping drains a full bar in three frames, and the run ends before the player understands they were touched — a game that validates, builds and boots perfectly while being unplayable.", + "category": "library", + "tags": [ + "capability:health", + "capability:damage", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/health/README.md", + "thumbnail_path": "prefabs/health/thumbnail.png", + "resources": [ + { + "prefab": "health", + "kind": "script", + "name": "scr_health", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "health", + "kind": "script", + "name": "scr_health_draw", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "health", + "kind": "script", + "name": "scr_health_query", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "heros_trail_extended", "version": "", @@ -23111,6 +27642,7 @@ "projection:top-down" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/heros_trail_extended/README.md", "thumbnail_path": "prefabs/heros_trail_extended/thumbnail.png", "resources": [ @@ -23136,7 +27668,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e1e25" } }, { @@ -23161,7 +27698,12 @@ "speed": { "unit": "frames_per_second", "value": 0.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1d2427" } }, { @@ -23186,7 +27728,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#20232a" } }, { @@ -23211,7 +27758,12 @@ "speed": { "unit": "frames_per_second", "value": 5.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e3c35" } }, { @@ -23242,7 +27794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 76, + "height": 141 + }, + "colour": "#25664e" } }, { @@ -23273,7 +27830,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 141 + }, + "colour": "#174e3b" } }, { @@ -23304,7 +27866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 141 + }, + "colour": "#21282f" } }, { @@ -23335,7 +27902,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 141 + }, + "colour": "#1d2529" } }, { @@ -23363,7 +27935,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 135 + }, + "colour": "#2b240b" } }, { @@ -23394,7 +27971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 141 + }, + "colour": "#1e1706" } }, { @@ -23416,7 +27998,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 96, + "height": 101 + }, + "colour": "#2a2f38" } }, { @@ -23438,7 +28025,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 101 + }, + "colour": "#282936" } }, { @@ -23489,7 +28081,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1b1e24" } }, { @@ -23525,7 +28122,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1b1e24" } }, { @@ -23547,7 +28149,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 33, + "height": 33 + }, + "colour": "#5effda" } }, { @@ -23593,7 +28200,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#262e22" } }, { @@ -23639,7 +28251,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#252b22" } }, { @@ -23661,7 +28278,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#003116" } }, { @@ -23683,7 +28305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#003116" } }, { @@ -23705,7 +28332,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#003116" } }, { @@ -23727,7 +28359,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#003116" } }, { @@ -23749,7 +28386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#013419" } }, { @@ -23771,7 +28413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#013418" } }, { @@ -23793,7 +28440,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#013419" } }, { @@ -23815,7 +28467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#013419" } }, { @@ -23837,7 +28494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#142c23" } }, { @@ -23859,7 +28521,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#132c23" } }, { @@ -23881,7 +28548,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#0b301e" } }, { @@ -23903,7 +28575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#0c2e1e" } }, { @@ -23925,7 +28602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 256 + }, + "colour": "#2e4b4c" } }, { @@ -23950,7 +28632,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 128 + }, + "colour": "#26384b" } }, { @@ -23972,7 +28659,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 128 + }, + "colour": "#25384b" } }, { @@ -23994,7 +28686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 128 + }, + "colour": "#26384a" } }, { @@ -24016,7 +28713,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 128 + }, + "colour": "#26384b" } }, { @@ -24038,7 +28740,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 128 + }, + "colour": "#26384b" } }, { @@ -24060,7 +28767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 201, + "height": 201 + }, + "colour": "#fe9907" } }, { @@ -24082,7 +28794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 201, + "height": 201 + }, + "colour": "#0cffd8" } }, { @@ -24104,7 +28821,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#0a0604" } }, { @@ -24126,7 +28848,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#22150e" } }, { @@ -24148,7 +28875,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#4d2c1f" } }, { @@ -24170,7 +28902,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#2a1610" } }, { @@ -24192,7 +28929,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 16 + }, + "colour": "#332a1b" } }, { @@ -24325,7 +29067,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#16844e" } }, { @@ -24360,7 +29107,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#147b49" } }, { @@ -24395,7 +29147,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#127d4a" } }, { @@ -24430,7 +29187,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#0f8c4a" } }, { @@ -24461,7 +29223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#107a49" } }, { @@ -24506,7 +29273,12 @@ "speed": { "unit": "frames_per_second", "value": 16.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#127d4a" } }, { @@ -24539,7 +29311,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#12804a" } }, { @@ -24572,7 +29349,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#117e4a" } }, { @@ -24717,7 +29499,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 180, + "height": 180 + }, + "colour": "#16844e" } }, { @@ -24739,7 +29526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 49, + "height": 91 + }, + "colour": "#273234" } }, { @@ -24761,7 +29553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 40, + "height": 81 + }, + "colour": "#313c3e" } }, { @@ -24783,7 +29580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 40, + "height": 103 + }, + "colour": "#354242" } }, { @@ -24805,7 +29607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#341917" } }, { @@ -24827,7 +29634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#321716" } }, { @@ -24849,7 +29661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#331816" } }, { @@ -24871,7 +29688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 100, + "height": 109 + }, + "colour": "#413f3c" } }, { @@ -24893,7 +29715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#32363e" } }, { @@ -24915,7 +29742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#32363e" } }, { @@ -24937,7 +29769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#4d515c" } }, { @@ -24959,7 +29796,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#464953" } }, { @@ -24981,7 +29823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#464a54" } }, { @@ -25003,7 +29850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 50 + }, + "colour": "#222327" } }, { @@ -25025,7 +29877,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 50 + }, + "colour": "#202226" } }, { @@ -25047,7 +29904,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 50 + }, + "colour": "#212226" } }, { @@ -25069,7 +29931,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 50 + }, + "colour": "#202226" } }, { @@ -25091,7 +29958,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 51 + }, + "colour": "#222126" } }, { @@ -25113,7 +29985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 51 + }, + "colour": "#252429" } }, { @@ -25135,7 +30012,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#314147" } }, { @@ -25157,7 +30039,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#2d4145" } }, { @@ -25179,7 +30066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 84 + }, + "colour": "#443016" } }, { @@ -25201,7 +30093,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 84 + }, + "colour": "#5c3b16" } }, { @@ -25223,7 +30120,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 141 + }, + "colour": "#341716" } }, { @@ -25245,7 +30147,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 141 + }, + "colour": "#371c18" } }, { @@ -25267,7 +30174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 20, + "height": 16 + }, + "colour": "#252a34" } }, { @@ -25289,7 +30201,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 9, + "height": 20 + }, + "colour": "#303441" } }, { @@ -25311,7 +30228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 13, + "height": 11 + }, + "colour": "#2f3044" } }, { @@ -25333,7 +30255,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 6, + "height": 14 + }, + "colour": "#2b393f" } }, { @@ -25355,7 +30282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 12, + "height": 12 + }, + "colour": "#1a1f26" } }, { @@ -25377,7 +30309,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 10, + "height": 16 + }, + "colour": "#1f212d" } }, { @@ -25399,7 +30336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 11, + "height": 13 + }, + "colour": "#383b4c" } }, { @@ -25421,7 +30363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 17 + }, + "colour": "#1a2228" } }, { @@ -25443,7 +30390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 30, + "height": 57 + }, + "colour": "#252a35" } }, { @@ -25465,7 +30417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#42621d" } }, { @@ -25487,7 +30444,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1d522e" } }, { @@ -25509,7 +30471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#314c0f" } }, { @@ -25531,7 +30498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 141 + }, + "colour": "#373a0f" } }, { @@ -25553,7 +30525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#22242b" } }, { @@ -25575,7 +30552,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#202329" } }, { @@ -25597,7 +30579,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 89 + }, + "colour": "#291f23" } }, { @@ -25619,7 +30606,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e2127" } }, { @@ -25641,7 +30633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e2328" } }, { @@ -25663,7 +30660,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e2328" } }, { @@ -25685,7 +30687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e2227" } }, { @@ -25707,7 +30714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1c2126" } }, { @@ -25729,7 +30741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1f272a" } }, { @@ -25751,7 +30768,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 72 + }, + "colour": "#1c2024" } }, { @@ -25773,7 +30795,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 89 + }, + "colour": "#2a1b1e" } }, { @@ -25795,7 +30822,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#0e1014" } }, { @@ -25817,7 +30849,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#08090b" } }, { @@ -25854,7 +30891,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#413029" } }, { @@ -25876,7 +30918,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#1e2a2e" } }, { @@ -25898,7 +30945,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#265a4f" } }, { @@ -25916,6 +30968,48 @@ } ] }, + { + "name": "hud", + "version": "", + "display_name": "On-screen numbers", + "description": "Bars with an optional lagging chase bar behind them so a big hit is legible, discrete pips for counts small enough to read at a glance, corner anchoring that survives a window resize, and a label helper that restores the previous alignment. All in GUI space, because a HUD in room coordinates scrolls away the moment the game gets a camera.", + "category": "library", + "tags": [ + "capability:hud", + "capability:ui", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/hud/README.md", + "thumbnail_path": "prefabs/hud/thumbnail.png", + "resources": [ + { + "prefab": "hud", + "kind": "script", + "name": "scr_hud", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "hud", + "kind": "script", + "name": "scr_hud_bar", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "hud", + "kind": "script", + "name": "scr_hud_text", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "iconic_animals", "version": "", @@ -25930,6 +31024,7 @@ "theme:nature" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/iconic_animals/README.md", "thumbnail_path": "prefabs/iconic_animals/thumbnail.png", "resources": [ @@ -25951,7 +31046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e19b92" } }, { @@ -25972,7 +31072,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc827a" } }, { @@ -25992,7 +31097,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#767275" } }, { @@ -26013,7 +31123,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#615e61" } }, { @@ -26033,7 +31148,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#93827a" } }, { @@ -26054,7 +31174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#796c65" } }, { @@ -26074,7 +31199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#73462e" } }, { @@ -26094,7 +31224,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#abacaa" } }, { @@ -26115,7 +31250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8e8e8c" } }, { @@ -26135,7 +31275,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cbccca" } }, { @@ -26156,7 +31301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a8a9a7" } }, { @@ -26177,7 +31327,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5f3a26" } }, { @@ -26197,7 +31352,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa7e53" } }, { @@ -26218,7 +31378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8b6844" } }, { @@ -26238,7 +31403,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4c3f40" } }, { @@ -26259,7 +31429,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#3c3234" } }, { @@ -26279,7 +31454,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#50455d" } }, { @@ -26300,7 +31480,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#40374a" } }, { @@ -26320,7 +31505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#985237" } }, { @@ -26341,7 +31531,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a412c" } }, { @@ -26361,7 +31556,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dfac5d" } }, { @@ -26382,7 +31582,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b78d4c" } }, { @@ -26402,7 +31607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e2ccc9" } }, { @@ -26423,7 +31633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b9a7a4" } }, { @@ -26443,7 +31658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b1a39e" } }, { @@ -26464,7 +31684,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#928682" } }, { @@ -26484,7 +31709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8d5c4a" } }, { @@ -26505,7 +31735,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#724a3c" } }, { @@ -26525,7 +31760,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#805044" } }, { @@ -26546,7 +31786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6a4238" } }, { @@ -26566,7 +31811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8da54f" } }, { @@ -26587,7 +31837,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#758841" } }, { @@ -26607,7 +31862,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8e583e" } }, { @@ -26627,7 +31887,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#976147" } }, { @@ -26648,7 +31913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6f4835" } }, { @@ -26669,7 +31939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6e4530" } }, { @@ -26689,7 +31964,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ddb082" } }, { @@ -26710,7 +31990,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b5906b" } }, { @@ -26730,7 +32015,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9ea2a4" } }, { @@ -26751,7 +32041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#828586" } }, { @@ -26771,7 +32066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7b4c26" } }, { @@ -26792,7 +32092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#674020" } }, { @@ -26812,7 +32117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e1dedd" } }, { @@ -26833,7 +32143,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b8b6b5" } }, { @@ -26853,7 +32168,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e3d7d3" } }, { @@ -26874,7 +32194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bbb0ad" } }, { @@ -26894,7 +32219,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#eb723b" } }, { @@ -26915,7 +32245,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c15e31" } }, { @@ -26935,7 +32270,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#be7351" } }, { @@ -26956,7 +32296,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9c5e42" } }, { @@ -26976,7 +32321,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8e833c" } }, { @@ -26996,7 +32346,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#645256" } }, { @@ -27017,7 +32372,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#544548" } }, { @@ -27037,7 +32397,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a09c4e" } }, { @@ -27058,7 +32423,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#858341" } }, { @@ -27079,7 +32449,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#776e32" } }, { @@ -27099,7 +32474,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7c522d" } }, { @@ -27120,7 +32500,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#664425" } }, { @@ -27140,7 +32525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c18745" } }, { @@ -27161,7 +32551,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b6c37" } }, { @@ -27181,7 +32576,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#817b6e" } }, { @@ -27202,7 +32602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#686359" } }, { @@ -27222,7 +32627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ddc19d" } }, { @@ -27243,7 +32653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b29c7e" } }, { @@ -27263,7 +32678,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc8667" } }, { @@ -27284,7 +32704,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a07258" } }, { @@ -27304,7 +32729,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#754d3b" } }, { @@ -27325,7 +32755,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#603f30" } }, { @@ -27345,7 +32780,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cf9567" } }, { @@ -27366,7 +32806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b48259" } }, { @@ -27386,7 +32831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c19d7a" } }, { @@ -27407,7 +32857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9f8164" } }, { @@ -27427,7 +32882,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a0999a" } }, { @@ -27448,7 +32908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#827c7d" } }, { @@ -27468,7 +32933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#735033" } }, { @@ -27489,7 +32959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5c4029" } }, { @@ -27509,7 +32984,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8f959b" } }, { @@ -27530,7 +33010,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#72777c" } }, { @@ -27550,7 +33035,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cab8a7" } }, { @@ -27571,7 +33061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a7988b" } }, { @@ -27591,7 +33086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#939092" } }, { @@ -27612,7 +33112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a7879" } }, { @@ -27632,7 +33137,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#db9c8d" } }, { @@ -27653,7 +33163,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b17e72" } }, { @@ -27673,7 +33188,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#322728" } }, { @@ -27694,7 +33214,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#282020" } }, { @@ -27714,7 +33239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a56f56" } }, { @@ -27735,7 +33265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#845945" } }, { @@ -27755,7 +33290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ebdbd7" } }, { @@ -27776,7 +33316,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bdb0ad" } }, { @@ -27796,7 +33341,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#867f82" } }, { @@ -27817,7 +33367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6f696b" } }, { @@ -27837,7 +33392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#989090" } }, { @@ -27858,7 +33418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7e7878" } }, { @@ -27878,7 +33443,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#859531" } }, { @@ -27899,7 +33469,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6c7927" } }, { @@ -27919,7 +33494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a29e9c" } }, { @@ -27940,7 +33520,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7f7c7a" } }, { @@ -27960,7 +33545,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc6443" } }, { @@ -27981,7 +33571,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#934e34" } }, { @@ -28001,7 +33596,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb8c6a" } }, { @@ -28022,7 +33622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9a7458" } }, { @@ -28042,7 +33647,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cecbc9" } }, { @@ -28063,7 +33673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aaa7a6" } }, { @@ -28083,7 +33698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e2a097" } }, { @@ -28104,7 +33724,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9e716a" } }, { @@ -28124,7 +33749,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6b6b6c" } }, { @@ -28145,7 +33775,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#525253" } }, { @@ -28165,7 +33800,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#968781" } }, { @@ -28186,7 +33826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6f645f" } }, { @@ -28206,7 +33851,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#764930" } }, { @@ -28226,7 +33876,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b4b4b2" } }, { @@ -28247,7 +33902,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#858583" } }, { @@ -28267,7 +33927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d8d9d6" } }, { @@ -28288,7 +33953,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a0a09e" } }, { @@ -28309,7 +33979,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#573623" } }, { @@ -28329,7 +34004,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b08559" } }, { @@ -28350,7 +34030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#866544" } }, { @@ -28370,7 +34055,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#484458" } }, { @@ -28391,7 +34081,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#343240" } }, { @@ -28411,7 +34106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#463f3e" } }, { @@ -28432,7 +34132,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#332e2d" } }, { @@ -28452,7 +34157,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#965432" } }, { @@ -28473,7 +34183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6e3e25" } }, { @@ -28493,7 +34208,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e3b160" } }, { @@ -28514,7 +34234,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ae8849" } }, { @@ -28534,7 +34259,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e3d1ce" } }, { @@ -28555,7 +34285,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ab9d9b" } }, { @@ -28575,7 +34310,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b9a7a0" } }, { @@ -28596,7 +34336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#897c77" } }, { @@ -28616,7 +34361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a5c4d" } }, { @@ -28637,7 +34387,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#664439" } }, { @@ -28657,7 +34412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa7259" } }, { @@ -28678,7 +34438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a5240" } }, { @@ -28698,7 +34463,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9aa853" } }, { @@ -28719,7 +34489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#757f3f" } }, { @@ -28739,7 +34514,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#925c43" } }, { @@ -28760,7 +34540,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#653f2e" } }, { @@ -28780,7 +34565,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dbb081" } }, { @@ -28801,7 +34591,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a2825f" } }, { @@ -28821,7 +34616,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aaaeae" } }, { @@ -28842,7 +34642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7f8282" } }, { @@ -28862,7 +34667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7b4f29" } }, { @@ -28883,7 +34693,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5e3c1f" } }, { @@ -28903,7 +34718,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dedbdb" } }, { @@ -28924,7 +34744,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a4a1a1" } }, { @@ -28944,7 +34769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e4ded9" } }, { @@ -28965,7 +34795,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#afaaa7" } }, { @@ -28985,7 +34820,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bf7d5a" } }, { @@ -29006,7 +34846,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8d5c42" } }, { @@ -29026,7 +34871,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d9043" } }, { @@ -29046,7 +34896,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6f5e5f" } }, { @@ -29067,7 +34922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#534647" } }, { @@ -29087,7 +34947,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a09548" } }, { @@ -29108,7 +34973,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#787036" } }, { @@ -29129,7 +34999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#766c32" } }, { @@ -29149,7 +35024,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9f6a3b" } }, { @@ -29170,7 +35050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a512d" } }, { @@ -29190,7 +35075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c69351" } }, { @@ -29211,7 +35101,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8e693a" } }, { @@ -29231,7 +35126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#807b6b" } }, { @@ -29252,7 +35152,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5d5a50" } }, { @@ -29272,7 +35177,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dfc8ab" } }, { @@ -29293,7 +35203,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa9882" } }, { @@ -29313,7 +35228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b88565" } }, { @@ -29334,7 +35254,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a634c" } }, { @@ -29354,7 +35279,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#785242" } }, { @@ -29375,7 +35305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5a3e32" } }, { @@ -29395,7 +35330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d19a64" } }, { @@ -29416,7 +35356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa7d51" } }, { @@ -29436,7 +35381,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c09a79" } }, { @@ -29457,7 +35407,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#91755b" } }, { @@ -29477,7 +35432,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aea9a9" } }, { @@ -29498,7 +35458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#817d7d" } }, { @@ -29518,7 +35483,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e87943" } }, { @@ -29539,7 +35509,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ae5b32" } }, { @@ -29559,7 +35534,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#93999f" } }, { @@ -29580,7 +35560,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6c7075" } }, { @@ -29600,7 +35585,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a573b" } }, { @@ -29621,7 +35611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5a402b" } }, { @@ -29641,7 +35636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ccbcaa" } }, { @@ -29662,7 +35662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d9183" } }, { @@ -29682,7 +35687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#96979a" } }, { @@ -29703,7 +35713,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#737476" } }, { @@ -29723,7 +35738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dfa593" } }, { @@ -29744,7 +35764,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a3796b" } }, { @@ -29764,7 +35789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#925c41" } }, { @@ -29785,7 +35815,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6a432f" } }, { @@ -29805,7 +35840,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ede4e2" } }, { @@ -29826,7 +35866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aca5a4" } }, { @@ -29846,7 +35891,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#222123" } }, { @@ -29867,7 +35917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#191719" } }, { @@ -29887,7 +35942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#838282" } }, { @@ -29908,7 +35968,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#626161" } }, { @@ -29928,7 +35993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9e9d9b" } }, { @@ -29949,7 +36019,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#757473" } }, { @@ -29969,7 +36044,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#819134" } }, { @@ -29990,7 +36070,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#606c27" } }, { @@ -30010,7 +36095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba7254" } }, { @@ -30031,7 +36121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#87533d" } }, { @@ -30051,7 +36146,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a5a4a3" } }, { @@ -30072,7 +36172,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#787776" } }, { @@ -30092,7 +36197,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d674f" } }, { @@ -30113,7 +36223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#694434" } }, { @@ -30133,7 +36248,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#be8961" } }, { @@ -30154,7 +36274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8f6749" } }, { @@ -30174,7 +36299,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cbc8c7" } }, { @@ -30195,7 +36325,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#999796" } } ] @@ -30212,6 +36347,7 @@ "projection:ui-icon" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/incidentals_fantasy/README.md", "thumbnail_path": "prefabs/incidentals_fantasy/thumbnail.png", "resources": [ @@ -30234,7 +36370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ccb298" } }, { @@ -30257,7 +36398,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a48b73" } }, { @@ -30279,7 +36425,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b6a392" } }, { @@ -30302,7 +36453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a18c7a" } }, { @@ -30324,7 +36480,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c7af8f" } }, { @@ -30347,7 +36508,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a0886d" } }, { @@ -30369,7 +36535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6165a3" } }, { @@ -30392,7 +36563,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#53568b" } }, { @@ -30414,7 +36590,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#50a6a5" } }, { @@ -30437,7 +36618,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#458d8d" } }, { @@ -30459,7 +36645,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#799238" } }, { @@ -30482,7 +36673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#677c30" } }, { @@ -30504,7 +36700,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c96f35" } }, { @@ -30527,7 +36728,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa5e2e" } }, { @@ -30549,7 +36755,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cb5172" } }, { @@ -30572,7 +36783,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ac4562" } }, { @@ -30594,7 +36810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b74fa9" } }, { @@ -30617,7 +36838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b4490" } }, { @@ -30639,7 +36865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c44f3c" } }, { @@ -30662,7 +36893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a64434" } }, { @@ -30684,7 +36920,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c49a39" } }, { @@ -30707,7 +36948,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a68331" } }, { @@ -30729,7 +36975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a29593" } }, { @@ -30752,7 +37003,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#857a7a" } }, { @@ -30774,7 +37030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a89e94" } }, { @@ -30797,7 +37058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a827b" } }, { @@ -30819,7 +37085,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a89b84" } }, { @@ -30842,7 +37113,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a806e" } }, { @@ -30864,7 +37140,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b59b85" } }, { @@ -30887,7 +37168,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#957f6f" } }, { @@ -30909,7 +37195,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b2938a" } }, { @@ -30932,7 +37223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#927a73" } }, { @@ -30954,7 +37250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b19390" } }, { @@ -30977,7 +37278,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#917978" } }, { @@ -30999,7 +37305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#af9484" } }, { @@ -31022,7 +37333,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#907a6e" } }, { @@ -31044,7 +37360,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b39e84" } }, { @@ -31067,7 +37388,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#93826e" } }, { @@ -31089,7 +37415,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#da8224" } }, { @@ -31111,7 +37442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cf6c23" } }, { @@ -31133,7 +37469,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d87e35" } }, { @@ -31156,7 +37497,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c16f2f" } }, { @@ -31179,7 +37525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b45d20" } }, { @@ -31202,7 +37553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba6f1e" } }, { @@ -31224,7 +37580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#806b79" } }, { @@ -31246,7 +37607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a6374" } }, { @@ -31268,7 +37634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#987776" } }, { @@ -31291,7 +37662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#886868" } }, { @@ -31314,7 +37690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6d5765" } }, { @@ -31337,7 +37718,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6d5b68" } }, { @@ -31359,7 +37745,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a66149" } }, { @@ -31381,7 +37772,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b5239" } }, { @@ -31403,7 +37799,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b16947" } }, { @@ -31426,7 +37827,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d5b3e" } }, { @@ -31449,7 +37855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#894732" } }, { @@ -31472,7 +37883,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8d533e" } }, { @@ -31494,7 +37910,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8890a0" } }, { @@ -31517,7 +37938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#757d8a" } }, { @@ -31539,7 +37965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b55933" } }, { @@ -31562,7 +37993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a74f2c" } }, { @@ -31584,7 +38020,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e18d24" } }, { @@ -31607,7 +38048,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cc7a20" } }, { @@ -31629,7 +38075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6d7687" } }, { @@ -31652,7 +38103,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#636c7c" } }, { @@ -31674,7 +38130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a9a3a1" } }, { @@ -31697,7 +38158,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8d8989" } }, { @@ -31719,7 +38185,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a7f6e" } }, { @@ -31742,7 +38213,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a6d63" } }, { @@ -31764,7 +38240,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#908f99" } }, { @@ -31787,7 +38268,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a7a83" } }, { @@ -31809,7 +38295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b8633e" } }, { @@ -31831,7 +38322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa694c" } }, { @@ -31854,7 +38350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b5d43" } }, { @@ -31877,7 +38378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a65737" } }, { @@ -31899,7 +38405,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb6f4b" } }, { @@ -31922,7 +38433,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aa6242" } }, { @@ -31944,7 +38460,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b26c47" } }, { @@ -31967,7 +38488,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d5c3d" } }, { @@ -31989,7 +38515,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a7a1ad" } }, { @@ -32012,7 +38543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#796782" } }, { @@ -32034,7 +38570,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8fa9cf" } }, { @@ -32057,7 +38598,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7488a6" } }, { @@ -32079,7 +38625,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ac9fca" } }, { @@ -32102,7 +38653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8c7aa5" } }, { @@ -32124,7 +38680,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b7b3be" } }, { @@ -32147,7 +38708,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b91a4" } }, { @@ -32169,7 +38735,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32191,7 +38762,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32213,7 +38789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32235,7 +38816,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32257,7 +38843,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32279,7 +38870,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32301,7 +38897,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32323,7 +38924,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32345,7 +38951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32367,7 +38978,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32389,7 +39005,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32411,7 +39032,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32433,7 +39059,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32455,7 +39086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32477,7 +39113,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32499,7 +39140,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32521,7 +39167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32543,7 +39194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32565,7 +39221,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32587,7 +39248,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32609,7 +39275,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32631,7 +39302,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32653,7 +39329,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32675,7 +39356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32697,7 +39383,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32719,7 +39410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32741,7 +39437,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32763,7 +39464,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32785,7 +39491,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32807,7 +39518,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32829,7 +39545,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32851,7 +39572,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32873,7 +39599,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32895,7 +39626,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32917,7 +39653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32939,7 +39680,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32961,7 +39707,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -32983,7 +39734,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33005,7 +39761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33027,7 +39788,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33049,7 +39815,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33071,7 +39842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33093,7 +39869,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33115,7 +39896,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33137,7 +39923,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33159,7 +39950,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33181,7 +39977,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33203,7 +40004,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#0c131c" } }, { @@ -33225,7 +40031,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33247,7 +40058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33269,7 +40085,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33291,7 +40112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33313,7 +40139,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33335,7 +40166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33357,7 +40193,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33379,7 +40220,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33401,7 +40247,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33423,7 +40274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33445,7 +40301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33467,7 +40328,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33489,7 +40355,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33511,7 +40382,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -33533,7 +40409,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33555,7 +40436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33577,7 +40463,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33599,7 +40490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33621,7 +40517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33643,7 +40544,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33665,7 +40571,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33687,7 +40598,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33709,7 +40625,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33731,7 +40652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33753,7 +40679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33775,7 +40706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33797,7 +40733,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33819,7 +40760,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33841,7 +40787,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33863,7 +40814,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33885,7 +40841,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33907,7 +40868,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33929,7 +40895,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33951,7 +40922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33973,7 +40949,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -33995,7 +40976,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34017,7 +41003,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34039,7 +41030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34061,7 +41057,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34083,7 +41084,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34105,7 +41111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34127,7 +41138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34149,7 +41165,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34171,7 +41192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34193,7 +41219,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34215,7 +41246,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34237,7 +41273,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34259,7 +41300,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34281,7 +41327,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34303,7 +41354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34325,7 +41381,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34347,7 +41408,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34369,7 +41435,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34391,7 +41462,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34413,7 +41489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34435,7 +41516,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34457,7 +41543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34479,7 +41570,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34501,7 +41597,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34523,7 +41624,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34545,7 +41651,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34567,7 +41678,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34589,7 +41705,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34611,7 +41732,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34633,7 +41759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34655,7 +41786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34677,7 +41813,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34699,7 +41840,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34721,7 +41867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34743,7 +41894,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34765,7 +41921,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34787,7 +41948,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34809,7 +41975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34831,7 +42002,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34853,7 +42029,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34875,7 +42056,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ffffff" } }, { @@ -34897,7 +42083,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc6538" } }, { @@ -34920,7 +42111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9e532f" } }, { @@ -34942,7 +42138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c4b8a6" } }, { @@ -34965,7 +42166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ac998b" } }, { @@ -34987,7 +42193,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba6222" } }, { @@ -35010,7 +42221,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a65220" } }, { @@ -35032,7 +42248,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#387dec" } }, { @@ -35055,7 +42276,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#2e6ed6" } }, { @@ -35077,7 +42303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#23c2e8" } }, { @@ -35100,7 +42331,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#1da8cd" } }, { @@ -35122,7 +42358,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#2fc821" } }, { @@ -35145,7 +42386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#26ad1b" } }, { @@ -35167,7 +42413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#fd9324" } }, { @@ -35190,7 +42441,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e37f1d" } }, { @@ -35212,7 +42468,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ee5fce" } }, { @@ -35235,7 +42496,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d94eb8" } }, { @@ -35257,7 +42523,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#da38ec" } }, { @@ -35280,7 +42551,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c22dd2" } }, { @@ -35302,7 +42578,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f8486c" } }, { @@ -35325,7 +42606,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#df3a57" } }, { @@ -35347,7 +42633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#fecd27" } }, { @@ -35370,7 +42661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e7b51f" } }, { @@ -35392,7 +42688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5c6d89" } }, { @@ -35414,7 +42715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9a845f" } }, { @@ -35437,7 +42743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#887556" } }, { @@ -35460,7 +42771,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4d5b73" } }, { @@ -35482,7 +42798,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bbc5cf" } }, { @@ -35505,7 +42826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#99a0a9" } }, { @@ -35527,7 +42853,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d92e7c" } }, { @@ -35550,7 +42881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c1286c" } }, { @@ -35572,7 +42908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8f80a0" } }, { @@ -35595,7 +42936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#726279" } }, { @@ -35617,7 +42963,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9183a0" } }, { @@ -35640,7 +42991,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#73647a" } }, { @@ -35662,7 +43018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9585a2" } }, { @@ -35685,7 +43046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#76667c" } }, { @@ -35707,7 +43073,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b25739" } }, { @@ -35730,7 +43101,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8e432c" } }, { @@ -35752,7 +43128,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#fc9932" } }, { @@ -35775,7 +43156,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d57226" } }, { @@ -35797,7 +43183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#767188" } }, { @@ -35820,7 +43211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#615d71" } }, { @@ -35842,7 +43238,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8c5b4a" } }, { @@ -35865,7 +43266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#774c3e" } }, { @@ -35887,7 +43293,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bf8651" } }, { @@ -35910,7 +43321,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a37044" } }, { @@ -35932,7 +43348,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8b6153" } }, { @@ -35955,7 +43376,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#754e42" } }, { @@ -35977,7 +43403,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb8653" } }, { @@ -36000,7 +43431,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#976842" } }, { @@ -36022,7 +43458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7c8190" } }, { @@ -36044,7 +43485,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#748898" } }, { @@ -36067,7 +43513,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#636b77" } }, { @@ -36090,7 +43541,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#676c79" } }, { @@ -36112,7 +43568,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#50494f" } }, { @@ -36134,7 +43595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#3d3a3f" } }, { @@ -36156,7 +43622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b84d3c" } }, { @@ -36179,7 +43650,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#943e31" } }, { @@ -36202,7 +43678,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#323034" } }, { @@ -36225,7 +43706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#453f44" } }, { @@ -36247,7 +43733,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ad522e" } }, { @@ -36269,7 +43760,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba4b21" } }, { @@ -36292,7 +43788,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#973c1b" } }, { @@ -36315,7 +43816,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#934628" } }, { @@ -36337,7 +43843,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#eaa61c" } }, { @@ -36359,7 +43870,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f48e13" } }, { @@ -36382,7 +43898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c37010" } }, { @@ -36405,7 +43926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d28f18" } }, { @@ -36427,7 +43953,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5f6c7a" } }, { @@ -36449,7 +43980,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a598a5" } }, { @@ -36472,7 +44008,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#877f89" } }, { @@ -36495,7 +44036,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#515b67" } }, { @@ -36517,7 +44063,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#857c3f" } }, { @@ -36540,7 +44091,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6c562f" } }, { @@ -36562,7 +44118,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7b9146" } }, { @@ -36585,7 +44146,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#645f32" } }, { @@ -36607,7 +44173,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9da157" } }, { @@ -36630,7 +44201,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#64512e" } }, { @@ -36652,7 +44228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#517d45" } }, { @@ -36675,7 +44256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#755f3f" } }, { @@ -36697,7 +44283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#939e66" } }, { @@ -36720,7 +44311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6f5c41" } }, { @@ -36743,7 +44339,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4c6b3e" } }, { @@ -36765,7 +44366,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4ca2d2" } }, { @@ -36788,7 +44394,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4488b0" } }, { @@ -36810,7 +44421,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a6a4ca" } }, { @@ -36832,7 +44448,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a5a2c5" } }, { @@ -36855,7 +44476,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#88829d" } }, { @@ -36878,7 +44504,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8984a1" } }, { @@ -36900,7 +44531,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6d99d3" } }, { @@ -36923,7 +44559,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5e7cac" } }, { @@ -36945,7 +44586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6492ce" } }, { @@ -36968,7 +44614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#506893" } }, { @@ -36990,7 +44641,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#858eb7" } }, { @@ -37013,7 +44669,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6a7192" } }, { @@ -37035,7 +44696,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#84b0b7" } }, { @@ -37058,7 +44724,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#698c92" } }, { @@ -37080,7 +44751,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#82b185" } }, { @@ -37103,7 +44779,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#688d6a" } }, { @@ -37125,7 +44806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cd9474" } }, { @@ -37148,7 +44834,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a3765c" } }, { @@ -37170,7 +44861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c77ca7" } }, { @@ -37193,7 +44889,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9f6385" } }, { @@ -37215,7 +44916,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a175b6" } }, { @@ -37238,7 +44944,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#805e91" } }, { @@ -37260,7 +44971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cd7881" } }, { @@ -37283,7 +44999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a36067" } }, { @@ -37305,7 +45026,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8b869a" } }, { @@ -37328,7 +45054,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#615e6c" } }, { @@ -37350,7 +45081,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8b9999" } }, { @@ -37373,7 +45109,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#616b6b" } }, { @@ -37395,7 +45136,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#979675" } }, { @@ -37418,7 +45164,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#6a6952" } }, { @@ -37440,7 +45191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c98671" } }, { @@ -37463,7 +45219,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8d5e4f" } }, { @@ -37485,7 +45246,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c37890" } }, { @@ -37508,7 +45274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#895465" } }, { @@ -37530,7 +45301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b67ca3" } }, { @@ -37553,7 +45329,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#805772" } }, { @@ -37575,7 +45356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c47a78" } }, { @@ -37598,7 +45384,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#895554" } }, { @@ -37620,7 +45411,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c89471" } }, { @@ -37643,7 +45439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8c684f" } }, { @@ -37665,7 +45466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c9ac76" } }, { @@ -37688,7 +45494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a0895e" } }, { @@ -37710,7 +45521,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#406cbb" } }, { @@ -37733,7 +45549,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#375898" } }, { @@ -37755,7 +45576,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cbb834" } }, { @@ -37778,7 +45604,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a7952a" } }, { @@ -37800,7 +45631,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#27b3c4" } }, { @@ -37823,7 +45659,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#21909e" } }, { @@ -37845,7 +45686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5aae4d" } }, { @@ -37868,7 +45714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#49883d" } }, { @@ -37890,7 +45741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cd7336" } }, { @@ -37913,7 +45769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a95d2c" } }, { @@ -37935,7 +45796,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c95b97" } }, { @@ -37958,7 +45824,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a64a7a" } }, { @@ -37980,7 +45851,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c536d3" } }, { @@ -38003,7 +45879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a32cab" } }, { @@ -38025,7 +45906,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c1444b" } }, { @@ -38048,7 +45934,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9f373c" } }, { @@ -38070,7 +45961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c26742" } }, { @@ -38093,7 +45989,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a95838" } }, { @@ -38115,7 +46016,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#88504c" } }, { @@ -38137,7 +46043,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b3603a" } }, { @@ -38160,7 +46071,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#965131" } }, { @@ -38183,7 +46099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#764643" } }, { @@ -38205,7 +46126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b75c3d" } }, { @@ -38227,7 +46153,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba4d36" } }, { @@ -38249,7 +46180,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9a565a" } }, { @@ -38272,7 +46208,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7e484c" } }, { @@ -38294,7 +46235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#95645a" } }, { @@ -38317,7 +46263,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a534c" } }, { @@ -38339,7 +46290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9c6837" } }, { @@ -38362,7 +46318,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#805631" } }, { @@ -38384,7 +46345,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bd5831" } }, { @@ -38407,7 +46373,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#99492c" } }, { @@ -38429,7 +46400,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b95146" } }, { @@ -38452,7 +46428,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#96443c" } }, { @@ -38474,7 +46455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b74e58" } }, { @@ -38497,7 +46483,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#95424a" } }, { @@ -38520,7 +46511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#974130" } }, { @@ -38542,7 +46538,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc6934" } }, { @@ -38565,7 +46566,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#99572e" } }, { @@ -38588,7 +46594,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#954d35" } }, { @@ -38610,7 +46621,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dd8458" } }, { @@ -38633,7 +46649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b06b4a" } }, { @@ -38655,7 +46676,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -38678,7 +46704,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#000000" } }, { @@ -38700,7 +46731,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c7d1e5" } }, { @@ -38723,7 +46759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9aa1b1" } }, { @@ -38745,7 +46786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#835a4d" } }, { @@ -38768,7 +46814,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#67483f" } }, { @@ -38790,7 +46841,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#825e6a" } }, { @@ -38813,7 +46869,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#644954" } }, { @@ -38835,7 +46896,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c6988b" } }, { @@ -38858,7 +46924,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#94736b" } }, { @@ -38880,7 +46951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#909bb1" } }, { @@ -38903,7 +46979,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#666d7d" } }, { @@ -38925,7 +47006,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a15d3e" } }, { @@ -38948,7 +47034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#814931" } }, { @@ -38970,7 +47061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cc7932" } }, { @@ -38993,7 +47089,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a15e27" } }, { @@ -39015,7 +47116,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#94787d" } }, { @@ -39038,7 +47144,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#766065" } } ] @@ -39060,6 +47171,7 @@ "theme:nature" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/incidentals_food/README.md", "thumbnail_path": "prefabs/incidentals_food/thumbnail.png", "resources": [ @@ -39077,7 +47189,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba593b" } }, { @@ -39096,7 +47213,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a24e35" } }, { @@ -39113,7 +47235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c06540" } }, { @@ -39132,7 +47259,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9a5235" } }, { @@ -39149,7 +47281,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d09754" } }, { @@ -39168,7 +47305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ad7e46" } }, { @@ -39185,7 +47327,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dbb74a" } }, { @@ -39204,7 +47351,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ad903c" } }, { @@ -39221,7 +47373,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cb9a61" } }, { @@ -39240,7 +47397,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b28756" } }, { @@ -39257,7 +47419,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e0b25b" } }, { @@ -39276,7 +47443,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c0994f" } }, { @@ -39293,7 +47465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9c6248" } }, { @@ -39312,7 +47489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a5741" } }, { @@ -39329,7 +47511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cc8c51" } }, { @@ -39348,7 +47535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#98693e" } }, { @@ -39365,7 +47557,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dab26d" } }, { @@ -39382,7 +47579,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d5a34d" } }, { @@ -39401,7 +47603,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b08741" } }, { @@ -39420,7 +47627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b9975d" } }, { @@ -39437,7 +47649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a16255" } }, { @@ -39456,7 +47673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#73473e" } }, { @@ -39473,7 +47695,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb8f67" } }, { @@ -39492,7 +47719,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9a7655" } }, { @@ -39509,7 +47741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c28855" } }, { @@ -39528,7 +47765,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a37248" } }, { @@ -39545,7 +47787,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b8543c" } }, { @@ -39564,7 +47811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#843e2d" } }, { @@ -39581,7 +47833,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ae7050" } }, { @@ -39600,7 +47857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#925f44" } }, { @@ -39617,7 +47879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a45945" } }, { @@ -39636,7 +47903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#884b3a" } }, { @@ -39653,7 +47925,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cab281" } }, { @@ -39672,7 +47949,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#807052" } }, { @@ -39689,7 +47971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c7824b" } }, { @@ -39708,7 +47995,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ae7342" } }, { @@ -39725,7 +48017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#afa24c" } }, { @@ -39744,7 +48041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#958a42" } }, { @@ -39761,7 +48063,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bf593f" } }, { @@ -39780,7 +48087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b4935" } }, { @@ -39797,7 +48109,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c0563e" } }, { @@ -39816,7 +48133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7e3a2b" } }, { @@ -39833,7 +48155,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc7846" } }, { @@ -39852,7 +48179,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a1673d" } }, { @@ -39869,7 +48201,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5f8246" } }, { @@ -39888,7 +48225,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4d6839" } }, { @@ -39907,7 +48249,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#784c34" } }, { @@ -39924,7 +48271,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8c583c" } }, { @@ -39941,7 +48293,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c6897a" } }, { @@ -39960,7 +48317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a97568" } }, { @@ -39977,7 +48339,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#deb89b" } }, { @@ -39996,7 +48363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#be9e85" } }, { @@ -40013,7 +48385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cd9084" } }, { @@ -40032,7 +48409,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b17d73" } }, { @@ -40049,7 +48431,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d6ae91" } }, { @@ -40068,7 +48455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b8957d" } }, { @@ -40085,7 +48477,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc8055" } }, { @@ -40102,7 +48499,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d2cbb9" } }, { @@ -40121,7 +48523,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b7b1a2" } }, { @@ -40140,7 +48547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a6714c" } }, { @@ -40157,7 +48569,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#716f45" } }, { @@ -40176,7 +48593,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#595738" } }, { @@ -40193,7 +48615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c7c9c9" } }, { @@ -40212,7 +48639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7b7c7c" } }, { @@ -40229,7 +48661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cbc0af" } }, { @@ -40248,7 +48685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b0a698" } }, { @@ -40265,7 +48707,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d5dcdc" } }, { @@ -40284,7 +48731,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b3b9b9" } }, { @@ -40301,7 +48753,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cad7d8" } }, { @@ -40320,7 +48777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aab5b6" } }, { @@ -40337,7 +48799,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#884874" } }, { @@ -40356,7 +48823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#723d61" } }, { @@ -40373,7 +48845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c9bf85" } }, { @@ -40390,7 +48867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c6b980" } }, { @@ -40409,7 +48891,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a89d6d" } }, { @@ -40428,7 +48915,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aba372" } }, { @@ -40445,7 +48937,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e0bf7c" } }, { @@ -40462,7 +48959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dbb87c" } }, { @@ -40481,7 +48983,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb9d6a" } }, { @@ -40500,7 +49007,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bfa26a" } }, { @@ -40517,7 +49029,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7a8dac" } }, { @@ -40536,7 +49053,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#677690" } }, { @@ -40553,7 +49075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5a8d88" } }, { @@ -40572,7 +49099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5a8d88" } }, { @@ -40589,7 +49121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b6c1c2" } }, { @@ -40608,7 +49145,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#99a1a2" } }, { @@ -40625,7 +49167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#71a684" } }, { @@ -40644,7 +49191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#5f8b6f" } }, { @@ -40661,7 +49213,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c7967b" } }, { @@ -40680,7 +49237,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a77d67" } }, { @@ -40697,7 +49259,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ad8194" } }, { @@ -40716,7 +49283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#916d7c" } }, { @@ -40733,7 +49305,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9c7fa1" } }, { @@ -40752,7 +49329,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#836b87" } }, { @@ -40769,7 +49351,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#af7b7a" } }, { @@ -40788,7 +49375,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#926766" } }, { @@ -40805,7 +49397,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c8bd7f" } }, { @@ -40824,7 +49421,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a79e6b" } }, { @@ -40841,7 +49443,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ced1d0" } }, { @@ -40860,7 +49467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#838383" } }, { @@ -40877,7 +49489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8ea372" } }, { @@ -40896,7 +49513,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#72825b" } }, { @@ -40913,7 +49535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dab34c" } }, { @@ -40932,7 +49559,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c19f44" } }, { @@ -40949,7 +49581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8ead72" } }, { @@ -40968,7 +49605,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7f9a66" } }, { @@ -40985,7 +49627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c6995d" } }, { @@ -41004,7 +49651,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#af8853" } }, { @@ -41021,7 +49673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb8f54" } }, { @@ -41040,7 +49697,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a27c49" } }, { @@ -41057,7 +49719,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bd8e51" } }, { @@ -41076,7 +49743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a37b47" } }, { @@ -41093,7 +49765,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cfb394" } }, { @@ -41112,7 +49789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b59c82" } }, { @@ -41129,7 +49811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ca8e55" } }, { @@ -41148,7 +49835,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b07c4b" } }, { @@ -41165,7 +49857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#dba34c" } }, { @@ -41184,7 +49881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c19044" } }, { @@ -41201,7 +49903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#aea749" } }, { @@ -41220,7 +49927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#989241" } }, { @@ -41237,7 +49949,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d69385" } }, { @@ -41256,7 +49973,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b47c70" } }, { @@ -41273,7 +49995,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c78063" } }, { @@ -41292,7 +50019,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ac6f56" } }, { @@ -41309,7 +50041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ba735b" } }, { @@ -41328,7 +50065,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a56651" } }, { @@ -41345,7 +50087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d3f49" } }, { @@ -41364,7 +50111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8a3841" } }, { @@ -41381,7 +50133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b4654f" } }, { @@ -41400,7 +50157,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#985543" } }, { @@ -41417,7 +50179,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d0885e" } }, { @@ -41436,7 +50203,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b57753" } }, { @@ -41453,7 +50225,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c29174" } }, { @@ -41472,7 +50249,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a37a62" } }, { @@ -41489,7 +50271,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cdcfcf" } }, { @@ -41508,7 +50295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#949594" } }, { @@ -41525,7 +50317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ced0cf" } }, { @@ -41544,7 +50341,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#909190" } }, { @@ -41561,7 +50363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#713c29" } }, { @@ -41578,7 +50385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c2b7ac" } }, { @@ -41597,7 +50409,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#888078" } }, { @@ -41616,7 +50433,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#623525" } }, { @@ -41633,7 +50455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a86149" } }, { @@ -41652,7 +50479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#90543f" } }, { @@ -41669,7 +50501,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#c2946c" } }, { @@ -41688,7 +50525,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a57e5d" } }, { @@ -41705,7 +50547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b9b8a9" } }, { @@ -41724,7 +50571,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a09e91" } }, { @@ -41741,7 +50593,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#505045" } }, { @@ -41760,7 +50617,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#47473d" } }, { @@ -41777,7 +50639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9ea0a1" } }, { @@ -41796,7 +50663,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#888a8a" } }, { @@ -41813,7 +50685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b18473" } }, { @@ -41832,7 +50709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#977462" } }, { @@ -41849,7 +50731,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#948b8f" } }, { @@ -41868,7 +50755,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#80787b" } }, { @@ -41885,7 +50777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b48778" } }, { @@ -41904,7 +50801,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9b7468" } }, { @@ -41921,7 +50823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8ba4a8" } }, { @@ -41940,7 +50847,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#778d90" } }, { @@ -41957,7 +50869,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d8a262" } }, { @@ -41974,7 +50891,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d1865a" } }, { @@ -41993,7 +50915,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b5744f" } }, { @@ -42012,7 +50939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bb8d56" } }, { @@ -42029,7 +50961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bd6444" } }, { @@ -42048,7 +50985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#a1563b" } }, { @@ -42065,7 +51007,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b2504c" } }, { @@ -42084,7 +51031,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#9d4744" } }, { @@ -42101,7 +51053,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#787167" } }, { @@ -42120,7 +51077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#635e57" } }, { @@ -42137,7 +51099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ad9b9a" } }, { @@ -42156,7 +51123,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#867978" } } ] @@ -42178,6 +51150,7 @@ "state:static" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/incidentals_jewels_and_gems/README.md", "thumbnail_path": "prefabs/incidentals_jewels_and_gems/thumbnail.png", "resources": [ @@ -42197,7 +51170,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#5496eb" } }, { @@ -42216,7 +51194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#87d4e0" } }, { @@ -42235,7 +51218,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -42254,7 +51242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#9dd554" } }, { @@ -42273,7 +51266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#fea93e" } }, { @@ -42292,7 +51290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f97cb3" } }, { @@ -42311,7 +51314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#a772cd" } }, { @@ -42330,7 +51338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f2644d" } }, { @@ -42349,7 +51362,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#fdd73c" } }, { @@ -42368,7 +51386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d1954e" } }, { @@ -42387,7 +51410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d49b4e" } }, { @@ -42406,7 +51434,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -42425,7 +51458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d5993f" } }, { @@ -42444,7 +51482,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e4973c" } }, { @@ -42463,7 +51506,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e39049" } }, { @@ -42482,7 +51530,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d58d4a" } }, { @@ -42501,7 +51554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#de8c3c" } }, { @@ -42520,7 +51578,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e49c3f" } }, { @@ -42539,7 +51602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#548adb" } }, { @@ -42558,7 +51626,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7ec2cf" } }, { @@ -42577,7 +51650,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -42596,7 +51674,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#8cc54f" } }, { @@ -42615,7 +51698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f69940" } }, { @@ -42634,7 +51722,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#ed8597" } }, { @@ -42653,7 +51746,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#9966bc" } }, { @@ -42672,7 +51770,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e25c4a" } }, { @@ -42691,7 +51794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f4c340" } }, { @@ -42710,7 +51818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#1964d7" } }, { @@ -42729,7 +51842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#53b4c7" } }, { @@ -42748,7 +51866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -42767,7 +51890,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#3974d0" } }, { @@ -42786,7 +51914,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#66b1c2" } }, { @@ -42805,7 +51938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -42824,7 +51962,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#75b436" } }, { @@ -42843,7 +51986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#ec8526" } }, { @@ -42862,7 +52010,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d95a90" } }, { @@ -42881,7 +52034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#844dac" } }, { @@ -42900,7 +52058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d54332" } }, { @@ -42919,7 +52082,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#eeb329" } }, { @@ -42938,7 +52106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#69b915" } }, { @@ -42957,7 +52130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#fa7804" } }, { @@ -42976,7 +52154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e84286" } }, { @@ -42995,7 +52178,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7930ac" } }, { @@ -43014,7 +52202,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e12517" } }, { @@ -43033,7 +52226,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f6b712" } }, { @@ -43052,7 +52250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#407bd7" } }, { @@ -43071,7 +52274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6db9ca" } }, { @@ -43090,7 +52298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -43109,7 +52322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7ebd3c" } }, { @@ -43128,7 +52346,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f28d2b" } }, { @@ -43147,7 +52370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e26198" } }, { @@ -43166,7 +52394,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#8c55b5" } }, { @@ -43185,7 +52418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#dc4a37" } }, { @@ -43204,7 +52442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f2bd2d" } }, { @@ -43223,7 +52466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#cea063" } }, { @@ -43242,7 +52490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d3a762" } }, { @@ -43261,7 +52514,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -43280,7 +52538,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d3a34c" } }, { @@ -43299,7 +52562,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e79f4a" } }, { @@ -43318,7 +52586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e69759" } }, { @@ -43337,7 +52610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d3935b" } }, { @@ -43356,7 +52634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#df9249" } }, { @@ -43375,7 +52658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e5a84f" } }, { @@ -43394,7 +52682,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#4078d1" } }, { @@ -43413,7 +52706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6ab3c4" } }, { @@ -43432,7 +52730,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -43451,7 +52754,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7bb63c" } }, { @@ -43470,7 +52778,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e88a2b" } }, { @@ -43489,7 +52802,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d96094" } }, { @@ -43508,7 +52826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#8854af" } }, { @@ -43527,7 +52850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d14a38" } }, { @@ -43546,7 +52874,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#ebb62e" } }, { @@ -43565,7 +52898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7f90a8" } }, { @@ -43584,7 +52922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#8ea7a6" } }, { @@ -43603,7 +52946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -43622,7 +52970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#90a472" } }, { @@ -43641,7 +52994,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#bf9a72" } }, { @@ -43660,7 +53018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#b68892" } }, { @@ -43679,7 +53042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#94809b" } }, { @@ -43698,7 +53066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#b07c6d" } }, { @@ -43717,7 +53090,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#bfae79" } }, { @@ -43736,7 +53114,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6f84a4" } }, { @@ -43755,7 +53138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#7e9ea2" } }, { @@ -43774,7 +53162,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -43793,7 +53186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#829a6b" } }, { @@ -43812,7 +53210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#b78f68" } }, { @@ -43831,7 +53234,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#ac7b8b" } }, { @@ -43850,7 +53258,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#847394" } }, { @@ -43869,7 +53282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#a57065" } }, { @@ -43888,7 +53306,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#b6a46f" } }, { @@ -43907,7 +53330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#407ddb" } }, { @@ -43926,7 +53354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6ebdce" } }, { @@ -43945,7 +53378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -43964,7 +53402,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#82c03c" } }, { @@ -43983,7 +53426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f28f27" } }, { @@ -44002,7 +53450,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e6619a" } }, { @@ -44021,7 +53474,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#8e56b8" } }, { @@ -44040,7 +53498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#de4b3a" } }, { @@ -44059,7 +53522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f3c02d" } }, { @@ -44078,7 +53546,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6095da" } }, { @@ -44097,7 +53570,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#6fcbd5" } }, { @@ -44116,7 +53594,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -44135,7 +53618,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#9ec632" } }, { @@ -44154,7 +53642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e7992e" } }, { @@ -44173,7 +53666,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e46b9c" } }, { @@ -44192,7 +53690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#a262b0" } }, { @@ -44211,7 +53714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d45434" } }, { @@ -44230,7 +53738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e7d247" } }, { @@ -44249,7 +53762,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#4681dc" } }, { @@ -44268,7 +53786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#73bfcf" } }, { @@ -44287,7 +53810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -44306,7 +53834,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#86c143" } }, { @@ -44325,7 +53858,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f29431" } }, { @@ -44344,7 +53882,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e6689f" } }, { @@ -44363,7 +53906,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#925dbb" } }, { @@ -44382,7 +53930,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#df513e" } }, { @@ -44401,7 +53954,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f4c333" } }, { @@ -44420,7 +53978,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#81b0ec" } }, { @@ -44439,7 +54002,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#a5d9e4" } }, { @@ -44458,7 +54026,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -44477,7 +54050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#b3da88" } }, { @@ -44496,7 +54074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f9bd78" } }, { @@ -44515,7 +54098,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f29fc4" } }, { @@ -44534,7 +54122,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#bb98d6" } }, { @@ -44553,7 +54146,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#ed8e81" } }, { @@ -44572,7 +54170,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f9db72" } }, { @@ -44591,7 +54194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#326fbc" } }, { @@ -44610,7 +54218,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#53aac1" } }, { @@ -44629,7 +54242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#909090" } }, { @@ -44648,7 +54266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#57922b" } }, { @@ -44667,7 +54290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#dc6c25" } }, { @@ -44686,7 +54314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#cd467d" } }, { @@ -44705,7 +54338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#522787" } }, { @@ -44724,7 +54362,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#a6221a" } }, { @@ -44743,7 +54386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#d3aa3d" } }, { @@ -44762,7 +54410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#4880da" } }, { @@ -44781,7 +54434,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#71bbcd" } }, { @@ -44800,7 +54458,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -44819,7 +54482,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#82be47" } }, { @@ -44838,7 +54506,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f09435" } }, { @@ -44857,7 +54530,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e16a9e" } }, { @@ -44876,7 +54554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#905fb9" } }, { @@ -44895,7 +54578,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#da5443" } }, { @@ -44914,7 +54602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f1bf38" } }, { @@ -44933,7 +54626,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#5a8fde" } }, { @@ -44952,7 +54650,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#83c6d3" } }, { @@ -44971,7 +54674,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#919191" } }, { @@ -44990,7 +54698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#93c955" } }, { @@ -45009,7 +54722,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f69e44" } }, { @@ -45028,7 +54746,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e977a7" } }, { @@ -45047,7 +54770,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#9e6cc1" } }, { @@ -45066,7 +54794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#e6624f" } }, { @@ -45085,7 +54818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 300 + }, + "colour": "#f6c845" } } ] @@ -45103,6 +54841,7 @@ "state:static" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/incidentals_scifi/README.md", "thumbnail_path": "prefabs/incidentals_scifi/thumbnail.png", "resources": [ @@ -45123,7 +54862,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#618b55" } }, { @@ -45144,7 +54888,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#567b4b" } }, { @@ -45164,7 +54913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#414f7a" } }, { @@ -45185,7 +54939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3a466d" } }, { @@ -45205,7 +54964,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7d47c5" } }, { @@ -45226,7 +54990,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6e3fac" } }, { @@ -45246,7 +55015,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4c7916" } }, { @@ -45267,7 +55041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3b5d11" } }, { @@ -45287,7 +55066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9f9d37" } }, { @@ -45308,7 +55092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#969530" } }, { @@ -45328,7 +55117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a35c55" } }, { @@ -45349,7 +55143,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9f694d" } }, { @@ -45369,7 +55168,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b37655" } }, { @@ -45390,7 +55194,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8e514c" } }, { @@ -45411,7 +55220,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7f798b" } }, { @@ -45433,7 +55247,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6a6674" } }, { @@ -45453,7 +55272,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#906340" } }, { @@ -45474,7 +55298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#81593b" } }, { @@ -45494,7 +55323,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b031b4" } }, { @@ -45515,7 +55349,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9a2b9d" } }, { @@ -45535,7 +55374,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#cbbb50" } }, { @@ -45556,7 +55400,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#afa245" } }, { @@ -45576,7 +55425,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#97ac52" } }, { @@ -45597,7 +55451,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#829446" } }, { @@ -45617,7 +55476,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#777f80" } }, { @@ -45638,7 +55502,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#666c6e" } }, { @@ -45659,7 +55528,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#596ca5" } }, { @@ -45681,7 +55555,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#516296" } }, { @@ -45702,7 +55581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5987a5" } }, { @@ -45724,7 +55608,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#517b96" } }, { @@ -45745,7 +55634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5b8c57" } }, { @@ -45767,7 +55661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#52804f" } }, { @@ -45788,7 +55687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#af7049" } }, { @@ -45810,7 +55714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9f6543" } }, { @@ -45831,7 +55740,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ab5b80" } }, { @@ -45853,7 +55767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9b5375" } }, { @@ -45874,7 +55793,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#894f92" } }, { @@ -45896,7 +55820,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7d4884" } }, { @@ -45917,7 +55846,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b2524b" } }, { @@ -45939,7 +55873,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a14a45" } }, { @@ -45960,7 +55899,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a98c52" } }, { @@ -45982,7 +55926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9a7f4b" } }, { @@ -46003,7 +55952,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3e73a3" } }, { @@ -46025,7 +55979,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3a6994" } }, { @@ -46046,7 +56005,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3e97a3" } }, { @@ -46068,7 +56032,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#398993" } }, { @@ -46089,7 +56058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#42784b" } }, { @@ -46111,7 +56085,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3d6d45" } }, { @@ -46132,7 +56111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a3633e" } }, { @@ -46154,7 +56138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#946839" } }, { @@ -46175,7 +56164,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a04286" } }, { @@ -46197,7 +56191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#913c83" } }, { @@ -46218,7 +56217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#814899" } }, { @@ -46240,7 +56244,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#76428c" } }, { @@ -46261,7 +56270,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a0414d" } }, { @@ -46283,7 +56297,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#913c48" } }, { @@ -46304,7 +56323,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a38c3e" } }, { @@ -46326,7 +56350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#947f3a" } }, { @@ -46346,7 +56375,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6e93a7" } }, { @@ -46367,7 +56401,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#618294" } }, { @@ -46387,7 +56426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6e94a6" } }, { @@ -46408,7 +56452,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#638494" } }, { @@ -46429,7 +56478,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a58480" } }, { @@ -46451,7 +56505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#917470" } }, { @@ -46472,7 +56531,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b68b87" } }, { @@ -46494,7 +56558,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9e7875" } }, { @@ -46514,7 +56583,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46534,7 +56608,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46554,7 +56633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46574,7 +56658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46594,7 +56683,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46614,7 +56708,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46635,7 +56734,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46655,7 +56759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46675,7 +56784,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46695,7 +56809,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46715,7 +56834,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46735,7 +56859,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46756,7 +56885,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46777,7 +56911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46797,7 +56936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46817,7 +56961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46838,7 +56987,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46859,7 +57013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46879,7 +57038,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46899,7 +57063,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46920,7 +57089,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46941,7 +57115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#040404" } }, { @@ -46961,7 +57140,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -46982,7 +57166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47003,7 +57192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47023,7 +57217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47043,7 +57242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47064,7 +57268,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47085,7 +57294,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47105,7 +57319,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47125,7 +57344,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47145,7 +57369,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47166,7 +57395,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47187,7 +57421,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47207,7 +57446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47227,7 +57471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47247,7 +57496,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47268,7 +57522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47289,7 +57548,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47309,7 +57573,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47330,7 +57599,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47350,7 +57624,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -47370,7 +57649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47390,7 +57674,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47410,7 +57699,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47430,7 +57724,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47450,7 +57749,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47470,7 +57774,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47491,7 +57800,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47511,7 +57825,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47531,7 +57850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47551,7 +57875,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47571,7 +57900,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47591,7 +57925,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47612,7 +57951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47633,7 +57977,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47653,7 +58002,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47673,7 +58027,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47694,7 +58053,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47715,7 +58079,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47735,7 +58104,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47755,7 +58129,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47776,7 +58155,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47797,7 +58181,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47817,7 +58206,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47838,7 +58232,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47859,7 +58258,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47879,7 +58283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47899,7 +58308,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47920,7 +58334,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47941,7 +58360,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47961,7 +58385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -47981,7 +58410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48001,7 +58435,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48022,7 +58461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48043,7 +58487,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48063,7 +58512,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48083,7 +58537,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48103,7 +58562,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48124,7 +58588,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48145,7 +58614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48165,7 +58639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48186,7 +58665,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48206,7 +58690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -48226,7 +58715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#93a1ba" } }, { @@ -48247,7 +58741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#838fa5" } }, { @@ -48267,7 +58766,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8c7971" } }, { @@ -48288,7 +58792,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7a6962" } }, { @@ -48309,7 +58818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#50646b" } }, { @@ -48331,7 +58845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#46585e" } }, { @@ -48352,7 +58871,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#674b80" } }, { @@ -48374,7 +58898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5a4270" } }, { @@ -48395,7 +58924,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#605ae6" } }, { @@ -48417,7 +58951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4d4ec6" } }, { @@ -48437,7 +58976,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#77898b" } }, { @@ -48458,7 +59002,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#555c7f" } }, { @@ -48480,7 +59029,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#474d6a" } }, { @@ -48501,7 +59055,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#677273" } }, { @@ -48522,7 +59081,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#d02ba0" } }, { @@ -48544,7 +59108,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b8268e" } }, { @@ -48564,7 +59133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#757d77" } }, { @@ -48585,7 +59159,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5f6661" } }, { @@ -48605,7 +59184,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b2626e" } }, { @@ -48626,7 +59210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#67668e" } }, { @@ -48646,7 +59235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7777a5" } }, { @@ -48667,7 +59261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#98555f" } }, { @@ -48688,7 +59287,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ce9673" } }, { @@ -48710,7 +59314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b08062" } }, { @@ -48731,7 +59340,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ad7d57" } }, { @@ -48753,7 +59367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#99704f" } }, { @@ -48774,7 +59393,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#d98d4c" } }, { @@ -48796,7 +59420,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#bf7d43" } }, { @@ -48817,7 +59446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8993a2" } }, { @@ -48839,7 +59473,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7a8390" } }, { @@ -48859,7 +59498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6b5462" } }, { @@ -48880,7 +59524,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5f4b57" } }, { @@ -48900,7 +59549,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5e6662" } }, { @@ -48921,7 +59575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#555c59" } }, { @@ -48941,7 +59600,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b7923f" } }, { @@ -48962,7 +59626,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a28137" } }, { @@ -48983,7 +59652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7b8db3" } }, { @@ -49005,7 +59679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4b6b8e" } }, { @@ -49026,7 +59705,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#82a7b6" } }, { @@ -49048,7 +59732,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4b7e8d" } }, { @@ -49069,7 +59758,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#73a573" } }, { @@ -49091,7 +59785,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#567c58" } }, { @@ -49112,7 +59811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6d788b" } }, { @@ -49134,7 +59838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#596272" } }, { @@ -49155,7 +59864,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#aa9177" } }, { @@ -49177,7 +59891,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c6c50" } }, { @@ -49198,7 +59917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a57cb2" } }, { @@ -49220,7 +59944,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c5d85" } }, { @@ -49241,7 +59970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9a6ab0" } }, { @@ -49263,7 +59997,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#745084" } }, { @@ -49284,7 +60023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a56e83" } }, { @@ -49306,7 +60050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c5362" } }, { @@ -49327,7 +60076,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a3a57f" } }, { @@ -49349,7 +60103,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7a7c60" } }, { @@ -49369,7 +60128,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#988179" } }, { @@ -49390,7 +60154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#86726b" } }, { @@ -49410,7 +60179,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5b8cac" } }, { @@ -49431,7 +60205,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4e7892" } }, { @@ -49451,7 +60230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9d8684" } }, { @@ -49472,7 +60256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8b7775" } }, { @@ -49493,7 +60282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#76878e" } }, { @@ -49515,7 +60309,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#647379" } }, { @@ -49536,7 +60335,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#76978e" } }, { @@ -49558,7 +60362,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#648179" } }, { @@ -49579,7 +60388,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#91985f" } }, { @@ -49601,7 +60415,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7b8251" } }, { @@ -49622,7 +60441,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ac8a61" } }, { @@ -49644,7 +60468,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#937553" } }, { @@ -49665,7 +60494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b17694" } }, { @@ -49687,7 +60521,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#96657e" } }, { @@ -49708,7 +60547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#926a98" } }, { @@ -49730,7 +60574,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c5a81" } }, { @@ -49751,7 +60600,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a96d6c" } }, { @@ -49773,7 +60627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#905d5c" } }, { @@ -49794,7 +60653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#78919f" } }, { @@ -49816,7 +60680,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#657a87" } }, { @@ -49837,7 +60706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#75a0a2" } }, { @@ -49859,7 +60733,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#638788" } }, { @@ -49880,7 +60759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#829b7b" } }, { @@ -49902,7 +60786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6e8368" } }, { @@ -49923,7 +60812,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a08e76" } }, { @@ -49945,7 +60839,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#877863" } }, { @@ -49966,7 +60865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a07597" } }, { @@ -49988,7 +60892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#87627f" } }, { @@ -50009,7 +60918,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8d75a0" } }, { @@ -50031,7 +60945,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#776387" } }, { @@ -50052,7 +60971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9f7678" } }, { @@ -50074,7 +60998,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#866465" } }, { @@ -50095,7 +61024,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8f9094" } }, { @@ -50117,7 +61051,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#78797d" } }, { @@ -50138,7 +61077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a09b76" } }, { @@ -50160,7 +61104,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#878363" } }, { @@ -50181,7 +61130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#90948b" } }, { @@ -50203,7 +61157,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7a7d76" } }, { @@ -50224,7 +61183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ac9861" } }, { @@ -50246,7 +61210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#938252" } }, { @@ -50266,7 +61235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b96e96" } }, { @@ -50287,7 +61261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a66386" } }, { @@ -50308,7 +61287,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3770b1" } }, { @@ -50330,7 +61314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#32609c" } }, { @@ -50351,7 +61340,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3dacac" } }, { @@ -50373,7 +61367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#399596" } }, { @@ -50394,7 +61393,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#40914d" } }, { @@ -50416,7 +61420,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#387a47" } }, { @@ -50437,7 +61446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b46b40" } }, { @@ -50459,7 +61473,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a2613b" } }, { @@ -50480,7 +61499,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b53ba2" } }, { @@ -50502,7 +61526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9d3a8e" } }, { @@ -50523,7 +61552,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8e3eab" } }, { @@ -50545,7 +61579,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#80399b" } }, { @@ -50566,7 +61605,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b24a40" } }, { @@ -50588,7 +61632,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a0443b" } }, { @@ -50609,7 +61658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b49b3a" } }, { @@ -50631,7 +61685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#a18b36" } }, { @@ -50651,7 +61710,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7fa4b7" } }, { @@ -50672,7 +61736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#698897" } } ] @@ -50690,6 +61759,7 @@ "state:static" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/incidentals_weapons/README.md", "thumbnail_path": "prefabs/incidentals_weapons/thumbnail.png", "resources": [ @@ -50710,7 +61780,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#626160" } }, { @@ -50731,7 +61806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#49494c" } }, { @@ -50751,7 +61831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#645a59" } }, { @@ -50772,7 +61857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4c4444" } }, { @@ -50792,7 +61882,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5f5f63" } }, { @@ -50813,7 +61908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#484747" } }, { @@ -50833,7 +61933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#615b57" } }, { @@ -50854,7 +61959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#494542" } }, { @@ -50874,7 +61984,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5c594e" } }, { @@ -50895,7 +62010,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#46433b" } }, { @@ -50915,7 +62035,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#64534f" } }, { @@ -50936,7 +62061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#493c3a" } }, { @@ -50956,7 +62086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#947b50" } }, { @@ -50977,7 +62112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#695839" } }, { @@ -50997,7 +62137,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#b65141" } }, { @@ -51018,7 +62163,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8b3d31" } }, { @@ -51038,7 +62188,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8f7e6d" } }, { @@ -51059,7 +62214,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#756759" } }, { @@ -51079,7 +62239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4a7354" } }, { @@ -51100,7 +62265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#395840" } }, { @@ -51120,7 +62290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51140,7 +62315,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51160,7 +62340,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51180,7 +62365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51200,7 +62390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51220,7 +62415,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51240,7 +62440,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51260,7 +62465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51280,7 +62490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51300,7 +62515,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51320,7 +62540,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51340,7 +62565,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51360,7 +62590,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51380,7 +62615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51400,7 +62640,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51420,7 +62665,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51440,7 +62690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51460,7 +62715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51480,7 +62740,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51500,7 +62765,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51520,7 +62790,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51540,7 +62815,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51560,7 +62840,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51580,7 +62865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51600,7 +62890,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51620,7 +62915,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51640,7 +62940,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51660,7 +62965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51680,7 +62990,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#000000" } }, { @@ -51700,7 +63015,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51720,7 +63040,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51740,7 +63065,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51760,7 +63090,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51780,7 +63115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51800,7 +63140,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51820,7 +63165,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51840,7 +63190,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51860,7 +63215,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51880,7 +63240,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51900,7 +63265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51920,7 +63290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51940,7 +63315,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51960,7 +63340,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -51980,7 +63365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52000,7 +63390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52020,7 +63415,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52040,7 +63440,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52060,7 +63465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52080,7 +63490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52100,7 +63515,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52120,7 +63540,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52140,7 +63565,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52160,7 +63590,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52180,7 +63615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52200,7 +63640,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52220,7 +63665,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52240,7 +63690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52260,7 +63715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#ffffff" } }, { @@ -52280,7 +63740,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5d4e4b" } }, { @@ -52301,7 +63766,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#443937" } }, { @@ -52321,7 +63791,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5c6468" } }, { @@ -52342,7 +63817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#474e50" } }, { @@ -52362,7 +63842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#636871" } }, { @@ -52383,7 +63868,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4d5158" } }, { @@ -52403,7 +63893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#654956" } }, { @@ -52424,7 +63919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4a353f" } }, { @@ -52444,7 +63944,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#76564b" } }, { @@ -52465,7 +63970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5c433a" } }, { @@ -52485,7 +63995,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#897572" } }, { @@ -52506,7 +64021,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#605250" } }, { @@ -52526,7 +64046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8e7c7f" } }, { @@ -52547,7 +64072,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5b5052" } }, { @@ -52567,7 +64097,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#77888f" } }, { @@ -52588,7 +64123,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#525d62" } }, { @@ -52608,7 +64148,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#648591" } }, { @@ -52629,7 +64174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#40555d" } }, { @@ -52649,7 +64199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6a8686" } }, { @@ -52670,7 +64225,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#475a59" } }, { @@ -52690,7 +64250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#95766b" } }, { @@ -52711,7 +64276,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#715a51" } }, { @@ -52731,7 +64301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6f5b44" } }, { @@ -52752,7 +64327,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#534433" } }, { @@ -52772,7 +64352,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#637573" } }, { @@ -52793,7 +64378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#495755" } }, { @@ -52813,7 +64403,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#945941" } }, { @@ -52834,7 +64429,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6b402e" } }, { @@ -52854,7 +64454,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#556663" } }, { @@ -52875,7 +64480,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#3a4544" } }, { @@ -52895,7 +64505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#97735c" } }, { @@ -52916,7 +64531,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6c5242" } }, { @@ -52936,7 +64556,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8e877e" } }, { @@ -52957,7 +64582,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#67625b" } }, { @@ -52977,7 +64607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7b7e86" } }, { @@ -52998,7 +64633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#56595e" } }, { @@ -53018,7 +64658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8f7e6d" } }, { @@ -53039,7 +64684,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5f5348" } }, { @@ -53059,7 +64709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#878c87" } }, { @@ -53080,11 +64735,294 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5a5d5a" } } ] }, + { + "name": "inventory", + "version": "", + "display_name": "Carrying things", + "description": "Slots holding stacks of one item each, with add and remove that report how many actually moved rather than assuming it all fit. Also room-for, so a pickup prompt can be honest before the player walks into it. The returned count is the whole design: silently discarding the overflow is the bug this exists to prevent.", + "category": "library", + "tags": [ + "capability:inventory", + "capability:items", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/inventory/README.md", + "thumbnail_path": "prefabs/inventory/thumbnail.png", + "resources": [ + { + "prefab": "inventory", + "kind": "script", + "name": "scr_inventory", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "inventory", + "kind": "script", + "name": "scr_inventory_query", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "iso", + "version": "", + "display_name": "Isometric projection", + "description": "Grid-to-screen isometric projection, the inverse, and the depth key that makes a tile skirt occlude correctly.", + "category": "library", + "tags": [ + "capability:isometric", + "capability:projection", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/iso/README.md", + "thumbnail_path": "prefabs/iso/thumbnail.png", + "resources": [ + { + "prefab": "iso", + "kind": "script", + "name": "scr_iso", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "isometric_basics", + "version": "", + "display_name": "Isometric scene", + "description": "A diamond grid you can hover and pan, with correct depth sorting.", + "category": "component", + "tags": [ + "capability:iso", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/isometric_basics/README.md", + "thumbnail_path": "prefabs/isometric_basics/thumbnail.png", + "resources": [ + { + "prefab": "isometric_basics", + "kind": "object", + "name": "obj_iso_scene", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "kernel", + "version": "", + "display_name": "Kernel", + "description": "The floor every game shares: run lifecycle on a seeded RNG, an action-based input map, score and combo, a save store with flags, HUD helpers, audio buses, and the data/tuning accessors a generated template feeds.", + "category": "library", + "tags": [ + "capability:lifecycle", + "capability:save", + "capability:input", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/kernel/README.md", + "thumbnail_path": "prefabs/kernel/thumbnail.png", + "resources": [ + { + "prefab": "kernel", + "kind": "font", + "name": "fnt_kernel", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "object", + "name": "obj_actor", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "object", + "name": "obj_kernel", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_data", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_hud", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_input", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_save", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_score", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "kernel", + "kind": "script", + "name": "scr_kernel_state", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "levels", + "version": "", + "display_name": "Several levels, in order", + "description": "A run through several rooms in order. Which level you are on and how many there are is derived by walking `[room_order]`, so it cannot disagree with the order the game plays. Advancing goes through the cover so the room changes on the frame the screen is opaque, and never re-boots the kernel — a run's score belongs to the run, and a level that refunds its score on death is one you can farm by dying. The exit carries a count so it can wait for the coins, keys or switches a level asks for.", + "category": "library", + "tags": [ + "capability:levels", + "capability:progression", + "capability:rooms", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/levels/README.md", + "thumbnail_path": "prefabs/levels/thumbnail.png", + "resources": [ + { + "prefab": "levels", + "kind": "script", + "name": "scr_levels", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "levels", + "kind": "script", + "name": "scr_levels_gate", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "levels", + "kind": "script", + "name": "scr_levels_order", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "lighting", + "version": "", + "display_name": "Darkness and lights", + "description": "A surface filled with night and lights subtracted from it, with a gradient falloff so a light reads as light rather than as a hole in a sheet of paper. Lights are declared every frame by whatever owns them, so one that is destroyed simply stops adding itself. Every call re-checks the surface: a surface is volatile, and the frame after the OS frees one draws nothing — a game that looks perfect until it does not, on a machine you do not have.", + "category": "library", + "tags": [ + "capability:lighting", + "capability:rendering", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/lighting/README.md", + "thumbnail_path": "prefabs/lighting/thumbnail.png", + "resources": [ + { + "prefab": "lighting", + "kind": "script", + "name": "scr_lighting", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "lighting", + "kind": "script", + "name": "scr_lighting_draw", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "main_menu", + "version": "", + "display_name": "Main menu", + "description": "Keyboard and mouse menu — arrows or hover to select, Space or click to confirm.", + "category": "component", + "tags": [ + "capability:ui", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/main_menu/README.md", + "thumbnail_path": "prefabs/main_menu/thumbnail.png", + "resources": [ + { + "prefab": "main_menu", + "kind": "object", + "name": "obj_menu", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "maritime_mayhem", "version": "", @@ -53098,6 +65036,7 @@ "projection:top-down" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/maritime_mayhem/README.md", "thumbnail_path": "prefabs/maritime_mayhem/thumbnail.png", "resources": [ @@ -54548,7 +66487,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 78, + "height": 61 + }, + "colour": "#836241" } }, { @@ -54570,7 +66514,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 92, + "height": 108 + }, + "colour": "#bfaf9d" } }, { @@ -54592,7 +66541,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 144 + }, + "colour": "#9c8a5f" } }, { @@ -54614,7 +66568,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 70 + }, + "colour": "#7b7978" } }, { @@ -54636,7 +66595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 95, + "height": 90 + }, + "colour": "#4f4d4b" } }, { @@ -54658,7 +66622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 45, + "height": 47 + }, + "colour": "#787675" } }, { @@ -54680,7 +66649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 58 + }, + "colour": "#b28250" } }, { @@ -54702,7 +66676,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 142, + "height": 142 + }, + "colour": "#ab891b" } }, { @@ -54724,7 +66703,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 119, + "height": 35 + }, + "colour": "#b78752" } }, { @@ -54746,7 +66730,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 236, + "height": 186 + }, + "colour": "#905722" } }, { @@ -54768,7 +66757,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 73, + "height": 38 + }, + "colour": "#aa8662" } }, { @@ -54790,7 +66784,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 31, + "height": 23 + }, + "colour": "#9facb2" } }, { @@ -54812,7 +66811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 94, + "height": 63 + }, + "colour": "#ba963e" } }, { @@ -54834,7 +66838,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 36, + "height": 35 + }, + "colour": "#5cbee8" } }, { @@ -54856,7 +66865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 349 + }, + "colour": "#25739e" } }, { @@ -54879,7 +66893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1280, + "height": 1024 + }, + "colour": "#92a2a3" } }, { @@ -54902,7 +66921,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1280, + "height": 1024 + }, + "colour": "#65bf03" } }, { @@ -54925,7 +66949,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1280, + "height": 1024 + }, + "colour": "#00baec" } }, { @@ -54947,7 +66976,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 25, + "height": 107 + }, + "colour": "#e7554e" } }, { @@ -54969,7 +67003,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 25, + "height": 107 + }, + "colour": "#535b5b" } }, { @@ -54991,7 +67030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 25, + "height": 109 + }, + "colour": "#ffffff" } }, { @@ -55072,7 +67116,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 77 + }, + "colour": "#7b4a21" } }, { @@ -55095,7 +67144,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1280, + "height": 1024 + }, + "colour": "#a3e2d0" } }, { @@ -55176,7 +67230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 163, + "height": 152 + }, + "colour": "#d5b377" } }, { @@ -55198,7 +67257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 132, + "height": 121 + }, + "colour": "#75d609" } }, { @@ -55220,7 +67284,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 105, + "height": 104 + }, + "colour": "#7bd606" } }, { @@ -55242,7 +67311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 133, + "height": 64 + }, + "colour": "#474348" } }, { @@ -55264,7 +67338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 123, + "height": 104 + }, + "colour": "#383944" } }, { @@ -55286,7 +67365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 31, + "height": 31 + }, + "colour": "#2b3448" } }, { @@ -55316,7 +67400,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 55, + "height": 51 + }, + "colour": "#7c7978" } }, { @@ -55363,7 +67452,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 46 + }, + "colour": "#807e7c" } }, { @@ -55385,7 +67479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 93, + "height": 37 + }, + "colour": "#273044" } }, { @@ -55466,7 +67565,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 77, + "height": 73 + }, + "colour": "#b86b26" } }, { @@ -55547,7 +67651,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 77, + "height": 73 + }, + "colour": "#d69a21" } }, { @@ -55582,7 +67691,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#ffe145" } }, { @@ -55605,7 +67719,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 187, + "height": 182 + }, + "colour": "#5d5f69" } }, { @@ -55628,7 +67747,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 187, + "height": 182 + }, + "colour": "#565b6b" } }, { @@ -55650,7 +67774,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 96, + "height": 60 + }, + "colour": "#574436" } }, { @@ -55731,7 +67860,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 107, + "height": 121 + }, + "colour": "#bf422a" } }, { @@ -55812,7 +67946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 107, + "height": 121 + }, + "colour": "#423623" } }, { @@ -55893,7 +68032,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 107, + "height": 121 + }, + "colour": "#cfbdaa" } }, { @@ -55974,7 +68118,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 47 + }, + "colour": "#be7025" } }, { @@ -55997,7 +68146,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 187, + "height": 182 + }, + "colour": "#897c70" } }, { @@ -56019,7 +68173,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 167, + "height": 179 + }, + "colour": "#8f8c6e" } }, { @@ -56041,7 +68200,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 125 + }, + "colour": "#8c8b6c" } }, { @@ -56092,7 +68256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 190 + }, + "colour": "#925925" } }, { @@ -56153,7 +68322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 189 + }, + "colour": "#935823" } }, { @@ -56204,7 +68378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 190 + }, + "colour": "#995a1f" } }, { @@ -56265,7 +68444,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 189 + }, + "colour": "#995a1e" } }, { @@ -56316,7 +68500,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 190 + }, + "colour": "#9d551c" } }, { @@ -56377,7 +68566,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 189 + }, + "colour": "#9c551b" } }, { @@ -56428,7 +68622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 237, + "height": 190 + }, + "colour": "#90531e" } }, { @@ -56489,7 +68688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 231, + "height": 189 + }, + "colour": "#90541e" } }, { @@ -56511,7 +68715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#e3514a" } }, { @@ -56533,7 +68742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#e2514b" } }, { @@ -56555,7 +68769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#3b4645" } }, { @@ -56577,7 +68796,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#3c4646" } }, { @@ -56599,7 +68823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#f1f2ee" } }, { @@ -56621,7 +68850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 301 + }, + "colour": "#f0f1ed" } }, { @@ -56643,7 +68877,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 371 + }, + "colour": "#d83b34" } }, { @@ -56665,7 +68904,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 371 + }, + "colour": "#d83932" } }, { @@ -56687,7 +68931,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 370 + }, + "colour": "#282e33" } }, { @@ -56709,7 +68958,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 370 + }, + "colour": "#252b30" } }, { @@ -56731,7 +68985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 369 + }, + "colour": "#e6eced" } }, { @@ -56753,7 +69012,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 432, + "height": 369 + }, + "colour": "#e4ebec" } }, { @@ -56775,7 +69039,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 50 + }, + "colour": "#e14d46" } }, { @@ -56797,7 +69066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 50 + }, + "colour": "#e15049" } }, { @@ -56819,7 +69093,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 51 + }, + "colour": "#282f31" } }, { @@ -56841,7 +69120,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 51 + }, + "colour": "#283031" } }, { @@ -56863,7 +69147,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 50 + }, + "colour": "#fbfdfb" } }, { @@ -56885,7 +69174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 301, + "height": 50 + }, + "colour": "#fbfcfb" } }, { @@ -56907,7 +69201,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 435 + }, + "colour": "#da3831" } }, { @@ -56929,7 +69228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 435 + }, + "colour": "#da3932" } }, { @@ -56951,7 +69255,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 433 + }, + "colour": "#22292e" } }, { @@ -56973,7 +69282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 433 + }, + "colour": "#242a2f" } }, { @@ -56995,7 +69309,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 433 + }, + "colour": "#e8eeee" } }, { @@ -57017,7 +69336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 433 + }, + "colour": "#e7edee" } }, { @@ -57039,7 +69363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 312, + "height": 260 + }, + "colour": "#e34a43" } }, { @@ -57061,7 +69390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 312, + "height": 260 + }, + "colour": "#e34c45" } }, { @@ -57083,7 +69417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 315, + "height": 260 + }, + "colour": "#333e3e" } }, { @@ -57105,7 +69444,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 315, + "height": 260 + }, + "colour": "#333e3e" } }, { @@ -57127,7 +69471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 312, + "height": 260 + }, + "colour": "#f3f4f0" } }, { @@ -57149,7 +69498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 312, + "height": 260 + }, + "colour": "#f3f4f0" } }, { @@ -57200,7 +69554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 45 + }, + "colour": "#937c6d" } }, { @@ -57251,7 +69610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 45 + }, + "colour": "#af8f6d" } }, { @@ -57302,7 +69666,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 47 + }, + "colour": "#c36752" } }, { @@ -57353,7 +69722,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 42 + }, + "colour": "#696061" } }, { @@ -57375,7 +69749,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 564, + "height": 319 + }, + "colour": "#875012" } }, { @@ -57397,7 +69776,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 564, + "height": 319 + }, + "colour": "#7d4911" } }, { @@ -57419,7 +69803,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 798, + "height": 388 + }, + "colour": "#7e4a14" } }, { @@ -57441,7 +69830,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 798, + "height": 388 + }, + "colour": "#784713" } }, { @@ -57463,7 +69857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 380, + "height": 118 + }, + "colour": "#884c13" } }, { @@ -57485,7 +69884,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 380, + "height": 118 + }, + "colour": "#814913" } }, { @@ -57507,7 +69911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 898, + "height": 459 + }, + "colour": "#7b350e" } }, { @@ -57529,7 +69938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 897, + "height": 459 + }, + "colour": "#74320e" } }, { @@ -57551,7 +69965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 492, + "height": 276 + }, + "colour": "#814611" } }, { @@ -57573,7 +69992,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 492, + "height": 276 + }, + "colour": "#7b4310" } }, { @@ -57614,7 +70038,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 106, + "height": 54 + }, + "colour": "#e3f7ff" } }, { @@ -57636,7 +70065,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 299, + "height": 299 + }, + "colour": "#899468" } }, { @@ -57717,7 +70151,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 119, + "height": 76 + }, + "colour": "#c09c41" } }, { @@ -57739,7 +70178,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 219, + "height": 225 + }, + "colour": "#76d505" } }, { @@ -57761,7 +70205,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 223, + "height": 220 + }, + "colour": "#6bd008" } }, { @@ -57783,7 +70232,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 259 + }, + "colour": "#899567" } }, { @@ -57805,7 +70259,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 258 + }, + "colour": "#849062" } }, { @@ -57827,7 +70286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 239, + "height": 258 + }, + "colour": "#899566" } }, { @@ -57862,7 +70326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 107, + "height": 95 + }, + "colour": "#4acaf3" } }, { @@ -57953,7 +70422,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 365 + }, + "colour": "#2777a2" } }, { @@ -58044,6 +70518,10 @@ "speed": { "unit": "frames_per_second", "value": 30.0 + }, + "size": { + "width": 267, + "height": 361 } } }, @@ -58135,7 +70613,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 267, + "height": 361 + }, + "colour": "#2777a2" } }, { @@ -58175,7 +70658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 351, + "height": 142 + }, + "colour": "#807d7c" } }, { @@ -58232,6 +70720,267 @@ } ] }, + { + "name": "maze_chase", + "version": "", + "display_name": "Maze chase", + "description": "Eat every dot before the ghosts corner you — they route round the walls.", + "category": "game", + "tags": [ + "genre:arcade", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/maze_chase/README.md", + "thumbnail_path": "prefabs/maze_chase/thumbnail.png", + "resources": [ + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_dot", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_ghost", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_hero", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_maze", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "maze_chase", + "kind": "sprite", + "name": "spr_dot", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/maze_chase/sprites/spr_dot/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 10, + "height": 10 + }, + "colour": "#f0f0dc" + } + }, + { + "prefab": "maze_chase", + "kind": "sprite", + "name": "spr_ghost", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/maze_chase/sprites/spr_ghost/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 26, + "height": 26 + }, + "colour": "#eb6e82" + } + }, + { + "prefab": "maze_chase", + "kind": "sprite", + "name": "spr_hero", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/maze_chase/sprites/spr_hero/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 26, + "height": 26 + }, + "colour": "#fadc5a" + } + }, + { + "prefab": "maze_chase", + "kind": "sprite", + "name": "spr_maze_sheet", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/maze_chase/sprites/spr_maze_sheet/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 256, + "height": 224 + }, + "colour": "#3d4979" + } + }, + { + "prefab": "maze_chase", + "kind": "tileset", + "name": "ts_maze", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "motion", + "version": "", + "display_name": "Movement and collision", + "description": "Top-down and platformer movement against solid instances, resolved axis by axis so a wall stops you without sticking. Gravity, a jump with coyote time and a buffered press, and clamping to the room. The part of a game that is identical everywhere and easy to get subtly wrong.", + "category": "library", + "tags": [ + "capability:movement", + "capability:collision", + "genre:platformer", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/motion/README.md", + "thumbnail_path": "prefabs/motion/thumbnail.png", + "resources": [ + { + "prefab": "motion", + "kind": "script", + "name": "scr_motion", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "motion", + "kind": "script", + "name": "scr_motion_carry", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "motion", + "kind": "script", + "name": "scr_motion_platformer", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "music_and_sfx", + "version": "", + "display_name": "Music and sound effects", + "description": "Looping music that survives room changes, plus one-shot effects.", + "category": "component", + "tags": [ + "capability:audio", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/music_and_sfx/README.md", + "thumbnail_path": "prefabs/music_and_sfx/thumbnail.png", + "resources": [ + { + "prefab": "music_and_sfx", + "kind": "object", + "name": "obj_audio", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "music_and_sfx", + "kind": "script", + "name": "scr_audio_cues", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "music_tracks_natural_world", "version": "", @@ -58244,6 +70993,7 @@ "asset_type:sound" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/music_tracks_natural_world/README.md", "thumbnail_path": "prefabs/music_tracks_natural_world/thumbnail.png", "resources": [ @@ -58718,6 +71468,7 @@ "projection:side-view" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ninja_woods/README.md", "thumbnail_path": "prefabs/ninja_woods/thumbnail.png", "resources": [ @@ -58801,7 +71552,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1579 + }, + "colour": "#0e1c28" } }, { @@ -58824,7 +71580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1507 + }, + "colour": "#121e1b" } }, { @@ -58849,7 +71610,12 @@ "speed": { "unit": "frames_per_second", "value": 0.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#486684" } }, { @@ -58875,7 +71641,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 352, + "height": 252 + }, + "colour": "#475f7b" } }, { @@ -58897,7 +71668,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#df9a2f" } }, { @@ -58926,7 +71702,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 71, + "height": 180 + }, + "colour": "#d19c33" } }, { @@ -58948,7 +71729,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#854114" } }, { @@ -58972,7 +71758,12 @@ "speed": { "unit": "frames_per_second", "value": 0.0 - } + }, + "size": { + "width": 128, + "height": 52 + }, + "colour": "#79851e" } }, { @@ -58996,7 +71787,12 @@ "speed": { "unit": "frames_per_second", "value": 0.0 - } + }, + "size": { + "width": 376, + "height": 124 + }, + "colour": "#8d785e" } }, { @@ -59020,7 +71816,12 @@ "speed": { "unit": "frames_per_second", "value": 0.0 - } + }, + "size": { + "width": 376, + "height": 124 + }, + "colour": "#a87056" } }, { @@ -59042,7 +71843,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 384 + }, + "colour": "#1e354c" } }, { @@ -59064,7 +71870,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 320 + }, + "colour": "#3e52a3" } }, { @@ -59086,7 +71897,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 448, + "height": 192 + }, + "colour": "#3f58a6" } }, { @@ -59113,7 +71929,12 @@ "speed": { "unit": "frames_per_second", "value": 10.0 - } + }, + "size": { + "width": 71, + "height": 71 + }, + "colour": "#d19c33" } }, { @@ -59140,7 +71961,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 217, + "height": 98 + }, + "colour": "#ffffff" } }, { @@ -59169,7 +71995,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 108, + "height": 24 + }, + "colour": "#ffffff" } }, { @@ -59191,7 +72022,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 206 + }, + "colour": "#731d3b" } }, { @@ -59213,7 +72049,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 172 + }, + "colour": "#5f5ed0" } }, { @@ -59254,7 +72095,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 312, + "height": 280 + }, + "colour": "#bfc623" } }, { @@ -59283,7 +72129,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 104, + "height": 104 + }, + "colour": "#a73e4d" } }, { @@ -59311,7 +72162,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 104, + "height": 104 + }, + "colour": "#a43752" } }, { @@ -59334,7 +72190,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1364, + "height": 1172 + }, + "colour": "#b468b3" } }, { @@ -59357,7 +72218,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1304, + "height": 1172 + }, + "colour": "#b66bb7" } }, { @@ -59380,7 +72246,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1380, + "height": 1140 + }, + "colour": "#b96eb9" } }, { @@ -59402,7 +72273,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1178, + "height": 656 + }, + "colour": "#8a604b" } }, { @@ -59424,7 +72300,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 908, + "height": 640 + }, + "colour": "#a85566" } }, { @@ -59445,7 +72326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#4a7a6b" } }, { @@ -59467,7 +72353,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 15 + }, + "colour": "#088ca4" } }, { @@ -59489,7 +72380,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 21, + "height": 13 + }, + "colour": "#088ba4" } }, { @@ -59511,7 +72407,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 25, + "height": 16 + }, + "colour": "#088ea5" } }, { @@ -59533,7 +72434,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 72 + }, + "colour": "#0c899f" } }, { @@ -59555,7 +72461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 72 + }, + "colour": "#0c899f" } }, { @@ -59578,7 +72489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 68 + }, + "colour": "#09819b" } }, { @@ -59601,7 +72517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 68 + }, + "colour": "#09819b" } }, { @@ -59623,7 +72544,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 128 + }, + "colour": "#9e5869" } }, { @@ -59645,7 +72571,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 128 + }, + "colour": "#a85466" } }, { @@ -59667,7 +72598,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 25, + "height": 170 + }, + "colour": "#93464f" } }, { @@ -59689,7 +72625,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 63, + "height": 168 + }, + "colour": "#9f486a" } }, { @@ -59711,7 +72652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 94, + "height": 145 + }, + "colour": "#81a1bf" } }, { @@ -59733,7 +72679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 119, + "height": 128 + }, + "colour": "#82aed5" } }, { @@ -59756,7 +72707,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 560, + "height": 144 + }, + "colour": "#a5a47b" } }, { @@ -59780,7 +72736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 84, + "height": 82 + }, + "colour": "#d29b29" } }, { @@ -59804,7 +72765,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 81 + }, + "colour": "#630e34" } }, { @@ -59828,7 +72794,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 81 + }, + "colour": "#b46165" } }, { @@ -59849,7 +72820,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#732e4a" } }, { @@ -59871,7 +72847,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 51, + "height": 30 + }, + "colour": "#b549ca" } }, { @@ -59893,7 +72874,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#524269" } }, { @@ -59916,7 +72902,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 626, + "height": 477 + }, + "colour": "#bd515a" } }, { @@ -59938,7 +72929,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 43, + "height": 17 + }, + "colour": "#1099a8" } }, { @@ -59960,7 +72956,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 146, + "height": 43 + }, + "colour": "#0d849b" } }, { @@ -59982,7 +72983,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 46, + "height": 59 + }, + "colour": "#6092d2" } }, { @@ -60004,7 +73010,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 25 + }, + "colour": "#699dd6" } }, { @@ -60026,7 +73037,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 126 + }, + "colour": "#7c99a7" } }, { @@ -60048,7 +73064,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 58, + "height": 48 + }, + "colour": "#7092c8" } }, { @@ -60070,7 +73091,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 56, + "height": 59 + }, + "colour": "#6085c2" } }, { @@ -60092,7 +73118,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 67 + }, + "colour": "#6d8bc2" } }, { @@ -60114,7 +73145,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 52, + "height": 56 + }, + "colour": "#7688b6" } }, { @@ -60136,7 +73172,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 44, + "height": 35 + }, + "colour": "#7582b6" } }, { @@ -60158,7 +73199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 126 + }, + "colour": "#7c99a7" } }, { @@ -60180,7 +73226,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#cdcdcd" } }, { @@ -60207,7 +73258,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 172, + "height": 72 + }, + "colour": "#a1d4db" } }, { @@ -60236,7 +73292,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 108, + "height": 24 + }, + "colour": "#ffffff" } }, { @@ -60262,7 +73323,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#353d7b" } }, { @@ -60285,7 +73351,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#393e77" } }, { @@ -60315,7 +73386,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#333a73" } }, { @@ -60338,7 +73414,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#333972" } }, { @@ -60368,7 +73449,12 @@ "speed": { "unit": "frames_per_second", "value": 15.0 - } + }, + "size": { + "width": 152, + "height": 152 + }, + "colour": "#2e3e80" } }, { @@ -60390,7 +73476,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 32 + }, + "colour": "#5b8792" } }, { @@ -60412,7 +73503,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 12 + }, + "colour": "#64949e" } }, { @@ -60434,7 +73530,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 45, + "height": 13 + }, + "colour": "#57818a" } }, { @@ -60456,7 +73557,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1, + "height": 1300 + }, + "colour": "#1b0a5e" } }, { @@ -60478,7 +73584,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#556d6d" } }, { @@ -60501,7 +73612,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 384 + }, + "colour": "#284636" } }, { @@ -60523,7 +73639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 384 + }, + "colour": "#191a55" } }, { @@ -60546,7 +73667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1536, + "height": 1152 + }, + "colour": "#384197" } }, { @@ -60570,7 +73696,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 351, + "height": 271 + }, + "colour": "#c99968" } }, { @@ -60594,7 +73725,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 351, + "height": 271 + }, + "colour": "#c89868" } }, { @@ -60618,7 +73754,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 65 + }, + "colour": "#4f8c8e" } }, { @@ -60642,7 +73783,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 351, + "height": 271 + }, + "colour": "#c89868" } }, { @@ -60664,7 +73810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 8, + "height": 62 + }, + "colour": "#0b91a5" } }, { @@ -60686,7 +73837,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 39, + "height": 48 + }, + "colour": "#09849d" } }, { @@ -60708,7 +73864,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 64 + }, + "colour": "#0a88a0" } }, { @@ -60764,6 +73925,7 @@ "capability:leaderboard" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/online_leaderboard/README.md", "thumbnail_path": "prefabs/online_leaderboard/thumbnail.png", "resources": [ @@ -60799,6 +73961,376 @@ } ] }, + { + "name": "parallax", + "version": "", + "display_name": "Parallax backgrounds", + "description": "A back-to-front stack of layers, each moving at a fraction of the camera, tiled to cover the view so no layer ever ends. Factors above 1 sit in front of the action for a foreground. Optional self-drift for clouds that move while the player stands still, with the tiling anchored to the sprite rather than to distance travelled, so a long level does not slowly desynchronise. Layers carry a scale, so art drawn at a different size than the view still fits and still tiles.", + "category": "library", + "tags": [ + "capability:background", + "capability:parallax", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/parallax/README.md", + "thumbnail_path": "prefabs/parallax/thumbnail.png", + "resources": [ + { + "prefab": "parallax", + "kind": "script", + "name": "scr_parallax", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "parallax", + "kind": "script", + "name": "scr_parallax_draw", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "pathgrid", + "version": "", + "display_name": "Pathfinding on a grid", + "description": "A* over a grid of blocked cells, four-directional, returning the cells to walk. An empty array means no route exists, which callers must handle — a walled-off target is a normal state, not an error. Blocked cells can be rebuilt from the instances of an object, so a door that opens is a cell that clears.", + "category": "library", + "tags": [ + "capability:pathfinding", + "capability:ai", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/pathgrid/README.md", + "thumbnail_path": "prefabs/pathgrid/thumbnail.png", + "resources": [ + { + "prefab": "pathgrid", + "kind": "script", + "name": "scr_pathgrid", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "pathgrid", + "kind": "script", + "name": "scr_pathgrid_cells", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "pathgrid", + "kind": "script", + "name": "scr_pathgrid_find", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "pathgrid", + "kind": "script", + "name": "scr_pathgrid_tiles", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "pause_menu", + "version": "", + "display_name": "Pause menu (freeze)", + "description": "Freeze the room, snapshot the screen, dim it, and resume on a key.", + "category": "component", + "tags": [ + "capability:ui", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/pause_menu/README.md", + "thumbnail_path": "prefabs/pause_menu/thumbnail.png", + "resources": [ + { + "prefab": "pause_menu", + "kind": "object", + "name": "obj_pause", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "physics_arena", + "version": "", + "display_name": "Physics arena", + "description": "Roll a ball with real physics, knock out the targets, beat the clock.", + "category": "game", + "tags": [ + "genre:physics", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/physics_arena/README.md", + "thumbnail_path": "prefabs/physics_arena/thumbnail.png", + "resources": [ + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_arena", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_ball", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_bumper", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_target", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "object", + "name": "obj_wall", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_arena", + "kind": "sprite", + "name": "spr_ball", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/physics_arena/sprites/spr_ball/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 34, + "height": 34 + }, + "colour": "#fae182" + } + }, + { + "prefab": "physics_arena", + "kind": "sprite", + "name": "spr_bumper", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/physics_arena/sprites/spr_bumper/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 52, + "height": 52 + }, + "colour": "#78d2be" + } + }, + { + "prefab": "physics_arena", + "kind": "sprite", + "name": "spr_target", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/physics_arena/sprites/spr_target/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 36, + "height": 36 + }, + "colour": "#eb5f73" + } + }, + { + "prefab": "physics_arena", + "kind": "sprite", + "name": "spr_wall", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/physics_arena/sprites/spr_wall/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 40, + "height": 40 + }, + "colour": "#424e7a" + } + } + ] + }, + { + "name": "physics_body", + "version": "", + "display_name": "Physics body hygiene", + "description": "The things a Box2D body needs that an instance does not. A speed ceiling behind phy_bullet, because a body that gains energy off bouncy surfaces eventually outruns continuous collision and passes through a wall. Impact strength from a velocity reversal, which fires once per bounce with a magnitude rather than once per frame of contact with none. Stall detection, because a ball that comes to rest out of reach ends no run. Pinning, because writing x does nothing to a physics body. And a freeze that stops the body, since gating your own Step leaves the world integrating underneath it.", + "category": "library", + "tags": [ + "capability:physics", + "capability:box2d", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/physics_body/README.md", + "thumbnail_path": "prefabs/physics_body/thumbnail.png", + "resources": [ + { + "prefab": "physics_body", + "kind": "script", + "name": "scr_physics_body", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_body", + "kind": "script", + "name": "scr_physics_body_freeze", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "physics_body", + "kind": "script", + "name": "scr_physics_body_impact", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "pickup", + "version": "", + "display_name": "Collectables", + "description": "Collection by distance rather than collision mask, so a pickup can be drawn rather than sprited. A magnet radius pulls one in faster the closer it gets, a bob offset keeps it looking loose on the floor without moving the instance out of range, and scatter drops a handful around a point — what a destroyed enemy leaves behind.", + "category": "library", + "tags": [ + "capability:pickups", + "capability:collectables", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/pickup/README.md", + "thumbnail_path": "prefabs/pickup/thumbnail.png", + "resources": [ + { + "prefab": "pickup", + "kind": "script", + "name": "scr_pickup", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "pickup", + "kind": "script", + "name": "scr_pickup_scatter", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "pixel_protagonists", "version": "", @@ -60813,6 +74345,7 @@ "projection:side-view" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/pixel_protagonists/README.md", "thumbnail_path": "prefabs/pixel_protagonists/thumbnail.png", "resources": [ @@ -60841,7 +74374,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 36 + }, + "colour": "#b30700" } }, { @@ -60868,7 +74406,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 53, + "height": 41 + }, + "colour": "#b00500" } }, { @@ -60896,7 +74439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 86, + "height": 17 + }, + "colour": "#ffce16" } }, { @@ -60922,7 +74470,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 26, + "height": 37 + }, + "colour": "#ffc422" } }, { @@ -60949,7 +74502,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 42, + "height": 17 + }, + "colour": "#ffd026" } }, { @@ -60978,7 +74536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#68565d" } }, { @@ -61004,7 +74567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#67545b" } }, { @@ -61036,7 +74604,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#68565e" } }, { @@ -61062,7 +74635,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#66565f" } }, { @@ -61086,7 +74664,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#68565e" } }, { @@ -61112,7 +74695,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#685860" } }, { @@ -61143,7 +74731,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#67565e" } }, { @@ -61173,7 +74766,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 192, + "height": 128 + }, + "colour": "#64545d" } }, { @@ -61203,7 +74801,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#6c585e" } }, { @@ -61229,7 +74832,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#65555d" } }, { @@ -61252,7 +74860,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 7 + }, + "colour": "#47434b" } }, { @@ -61282,7 +74895,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 30, + "height": 24 + }, + "colour": "#a2d44f" } }, { @@ -61310,7 +74928,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 28, + "height": 32 + }, + "colour": "#bde185" } }, { @@ -61339,7 +74962,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bc3108" } }, { @@ -61365,7 +74993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ba3308" } }, { @@ -61393,7 +75026,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bc3108" } }, { @@ -61419,7 +75057,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ae300b" } }, { @@ -61447,7 +75090,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c03307" } }, { @@ -61473,7 +75121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c03407" } }, { @@ -61502,7 +75155,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bb3009" } }, { @@ -61533,7 +75191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bc3208" } }, { @@ -61561,7 +75224,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bc3208" } }, { @@ -61591,7 +75259,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ba3309" } }, { @@ -61617,7 +75290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bd2e09" } }, { @@ -61644,7 +75322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4b3c47" } }, { @@ -61671,7 +75354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#42353c" } }, { @@ -61705,7 +75393,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#483b42" } }, { @@ -61731,7 +75424,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#453841" } }, { @@ -61755,7 +75453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#493a43" } }, { @@ -61785,7 +75488,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#493a43" } }, { @@ -61811,7 +75519,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#473941" } }, { @@ -61840,7 +75553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 128 + }, + "colour": "#46373e" } }, { @@ -61866,7 +75584,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#443740" } }, { @@ -61896,7 +75619,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4b3f48" } }, { @@ -61925,7 +75653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#443941" } }, { @@ -61966,7 +75699,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4b3b2d" } }, { @@ -61992,7 +75730,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4a3829" } }, { @@ -62022,7 +75765,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4b382c" } }, { @@ -62048,7 +75796,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4c352f" } }, { @@ -62073,7 +75826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#49392f" } }, { @@ -62099,7 +75857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 35, + "height": 73 + }, + "colour": "#4b392f" } }, { @@ -62127,7 +75890,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 123, + "height": 84 + }, + "colour": "#50392c" } }, { @@ -62170,7 +75938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 100, + "height": 70 + }, + "colour": "#51372c" } }, { @@ -62203,7 +75976,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 100, + "height": 70 + }, + "colour": "#51372c" } }, { @@ -62233,7 +76011,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 70, + "height": 70 + }, + "colour": "#4d3237" } }, { @@ -62259,11 +76042,199 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 54, + "height": 61 + }, + "colour": "#4b3930" } } ] }, + { + "name": "platformer", + "version": "", + "display_name": "Platformer", + "description": "Run, jump, collect, reach the flag — a whole side-view game to start from.", + "category": "game", + "tags": [ + "genre:platformer", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/platformer/README.md", + "thumbnail_path": "prefabs/platformer/thumbnail.png", + "resources": [ + { + "prefab": "platformer", + "kind": "object", + "name": "obj_coin", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "object", + "name": "obj_enemy", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "object", + "name": "obj_goal", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "object", + "name": "obj_level", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "object", + "name": "obj_player", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "room", + "name": "rm_level", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "platformer", + "kind": "sprite", + "name": "spr_coin", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/platformer/sprites/spr_coin/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#f0c850" + } + }, + { + "prefab": "platformer", + "kind": "sprite", + "name": "spr_enemy", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/platformer/sprites/spr_enemy/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 24, + "height": 34 + }, + "colour": "#d64a4a" + } + }, + { + "prefab": "platformer", + "kind": "sprite", + "name": "spr_goal", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/platformer/sprites/spr_goal/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 24, + "height": 40 + }, + "colour": "#78dc8c" + } + }, + { + "prefab": "platformer", + "kind": "sprite", + "name": "spr_player", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/platformer/sprites/spr_player/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 24, + "height": 40 + }, + "colour": "#4a7ad6" + } + }, + { + "prefab": "platformer", + "kind": "sprite", + "name": "spr_terrain_sheet", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/platformer/sprites/spr_terrain_sheet/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 256, + "height": 224 + }, + "colour": "#4b5d75" + } + }, + { + "prefab": "platformer", + "kind": "tileset", + "name": "ts_terrain", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "portrait_pack", "version": "", @@ -62277,6 +76248,7 @@ "projection:portrait" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/portrait_pack/README.md", "thumbnail_path": "prefabs/portrait_pack/thumbnail.png", "resources": [ @@ -62297,7 +76269,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3076a7" } }, { @@ -62318,7 +76295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b39c97" } }, { @@ -62339,7 +76321,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ad9584" } }, { @@ -62360,7 +76347,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a18571" } }, { @@ -62381,7 +76373,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7f604a" } }, { @@ -62402,7 +76399,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6b4b3b" } }, { @@ -62423,7 +76425,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#553931" } }, { @@ -62444,7 +76451,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b99f99" } }, { @@ -62465,7 +76477,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b29886" } }, { @@ -62486,7 +76503,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a68872" } }, { @@ -62507,7 +76529,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#82624a" } }, { @@ -62528,7 +76555,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6e4c3b" } }, { @@ -62549,7 +76581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#573a31" } }, { @@ -62570,7 +76607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#929899" } }, { @@ -62591,7 +76633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a06246" } }, { @@ -62612,7 +76659,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b79f9a" } }, { @@ -62633,7 +76685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b19988" } }, { @@ -62654,7 +76711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a58974" } }, { @@ -62675,7 +76737,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#84644d" } }, { @@ -62696,7 +76763,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#714e3f" } }, { @@ -62717,7 +76789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5b3d34" } }, { @@ -62738,7 +76815,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#796269" } }, { @@ -62759,7 +76841,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#584f4c" } }, { @@ -62780,7 +76867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b7a19c" } }, { @@ -62801,7 +76893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b19a88" } }, { @@ -62822,7 +76919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a58974" } }, { @@ -62843,7 +76945,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#82624b" } }, { @@ -62864,7 +76971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6d4c3c" } }, { @@ -62885,7 +76997,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#563a31" } }, { @@ -62906,7 +77023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dab7b3" } }, { @@ -62927,7 +77049,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d2af9b" } }, { @@ -62948,7 +77075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c29b83" } }, { @@ -62969,7 +77101,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#946c51" } }, { @@ -62990,7 +77127,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#79513e" } }, { @@ -63011,7 +77153,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5b3b31" } }, { @@ -63032,7 +77179,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#e9d6d1" } }, { @@ -63053,7 +77205,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#e1ccb5" } }, { @@ -63074,7 +77231,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d0b598" } }, { @@ -63095,7 +77257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9e7e5e" } }, { @@ -63116,7 +77283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#825f49" } }, { @@ -63137,7 +77309,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#624539" } }, { @@ -63158,7 +77335,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ceaea7" } }, { @@ -63179,7 +77361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c6a591" } }, { @@ -63200,7 +77387,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b7937a" } }, { @@ -63221,7 +77413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8c664b" } }, { @@ -63242,7 +77439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#724d3a" } }, { @@ -63263,7 +77465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#56382e" } }, { @@ -63284,7 +77491,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9b8b91" } }, { @@ -63305,7 +77517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d9b9b3" } }, { @@ -63326,7 +77543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d1b09b" } }, { @@ -63347,7 +77569,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c19d82" } }, { @@ -63368,7 +77595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#936d50" } }, { @@ -63389,7 +77621,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#78523e" } }, { @@ -63410,7 +77647,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5b3c31" } }, { @@ -63431,7 +77673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d0aba5" } }, { @@ -63452,7 +77699,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c8a38f" } }, { @@ -63473,7 +77725,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b99078" } }, { @@ -63494,7 +77751,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8d644a" } }, { @@ -63515,7 +77777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#734c39" } }, { @@ -63536,7 +77803,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#57372d" } }, { @@ -63557,7 +77829,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#251c1a" } }, { @@ -63578,7 +77855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#281818" } }, { @@ -63599,7 +77881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#566776" } }, { @@ -63620,7 +77907,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#15110f" } }, { @@ -63641,7 +77933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2e1d1c" } }, { @@ -63662,7 +77959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#28201e" } }, { @@ -63684,7 +77986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dcc3bc" } }, { @@ -63706,7 +78013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d4baa4" } }, { @@ -63728,7 +78040,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c4a58a" } }, { @@ -63750,7 +78067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#977457" } }, { @@ -63772,7 +78094,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7d5844" } }, { @@ -63794,7 +78121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5e4035" } }, { @@ -63816,7 +78148,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dcc4bd" } }, { @@ -63838,7 +78175,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d5bca6" } }, { @@ -63860,7 +78202,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c4a68b" } }, { @@ -63882,7 +78229,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#977557" } }, { @@ -63904,7 +78256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7d5944" } }, { @@ -63926,7 +78283,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5e4035" } }, { @@ -63948,7 +78310,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dec6bf" } }, { @@ -63970,7 +78337,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d6bca6" } }, { @@ -63992,7 +78364,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c6a88c" } }, { @@ -64014,7 +78391,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#977658" } }, { @@ -64036,7 +78418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7d5944" } }, { @@ -64058,7 +78445,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5e4135" } }, { @@ -64080,7 +78472,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d8bab4" } }, { @@ -64102,7 +78499,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d0b29c" } }, { @@ -64124,7 +78526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c19e84" } }, { @@ -64146,7 +78553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#946f53" } }, { @@ -64168,7 +78580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7a5441" } }, { @@ -64190,7 +78607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5c3d32" } }, { @@ -64211,7 +78633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#201c20" } }, { @@ -64232,7 +78659,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#29201f" } }, { @@ -64253,7 +78685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#251f1c" } }, { @@ -64274,7 +78711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2b2221" } }, { @@ -64294,7 +78736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7b1f1e" } }, { @@ -64314,7 +78761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#53312a" } }, { @@ -64334,7 +78786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5d3a32" } }, { @@ -64354,7 +78811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#1d1c1e" } }, { @@ -64374,7 +78836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5d3f25" } }, { @@ -64394,7 +78861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bc4f58" } }, { @@ -64415,7 +78887,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#63412b" } }, { @@ -64436,7 +78913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#382a25" } }, { @@ -64457,7 +78939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#42372a" } }, { @@ -64478,7 +78965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b8a796" } }, { @@ -64499,7 +78991,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c58f3c" } }, { @@ -64520,7 +79017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3e2d24" } }, { @@ -64541,7 +79043,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#f6dfcf" } }, { @@ -64562,7 +79069,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7b5168" } }, { @@ -64583,7 +79095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#512d20" } }, { @@ -64604,7 +79121,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2e231e" } }, { @@ -64625,7 +79147,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#927b6f" } }, { @@ -64646,7 +79173,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a8794b" } }, { @@ -64667,7 +79199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bc6a8c" } }, { @@ -64688,7 +79225,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b33f2c" } }, { @@ -64709,7 +79251,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b7a194" } }, { @@ -64730,7 +79277,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b58147" } }, { @@ -64751,7 +79303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#47302d" } }, { @@ -64772,7 +79329,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b08f60" } }, { @@ -64793,7 +79355,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4f392c" } }, { @@ -64814,7 +79381,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b7a592" } }, { @@ -64835,7 +79407,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#42302a" } }, { @@ -64856,7 +79433,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#988178" } }, { @@ -64877,7 +79459,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#385065" } }, { @@ -64898,7 +79485,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#432d24" } }, { @@ -64919,7 +79511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a45e68" } }, { @@ -64940,7 +79537,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ae671d" } }, { @@ -64961,7 +79563,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#af7e5d" } }, { @@ -64982,7 +79589,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#44362e" } }, { @@ -65003,7 +79615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b09297" } }, { @@ -65024,7 +79641,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#922120" } }, { @@ -65045,7 +79667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3d2e26" } }, { @@ -65066,7 +79693,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4b4237" } }, { @@ -65087,7 +79719,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b38b74" } }, { @@ -65108,7 +79745,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9c714a" } }, { @@ -65129,7 +79771,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#382923" } }, { @@ -65150,7 +79797,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a67e4f" } }, { @@ -65171,7 +79823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4a332b" } }, { @@ -65192,7 +79849,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#af968a" } }, { @@ -65213,7 +79875,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#723327" } }, { @@ -65234,7 +79901,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#aa988b" } }, { @@ -65255,7 +79927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a3734b" } }, { @@ -65276,7 +79953,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#46352a" } }, { @@ -65297,7 +79979,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#98897f" } }, { @@ -65318,7 +80005,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2e241f" } }, { @@ -65339,7 +80031,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bc552e" } }, { @@ -65360,7 +80057,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#af988c" } }, { @@ -65381,7 +80083,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a07f48" } }, { @@ -65402,7 +80109,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#39271c" } }, { @@ -65423,7 +80135,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2d242f" } }, { @@ -65443,7 +80160,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#505d6e" } }, { @@ -65463,7 +80185,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#81486f" } }, { @@ -65483,7 +80210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6e517b" } }, { @@ -65503,7 +80235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5c463f" } }, { @@ -65523,7 +80260,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#463933" } }, { @@ -65543,7 +80285,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b66437" } }, { @@ -65564,7 +80311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#574e49" } }, { @@ -65584,7 +80336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#432a29" } }, { @@ -65604,7 +80361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#643f47" } }, { @@ -65625,7 +80387,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3f697d" } }, { @@ -65646,7 +80413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#664743" } }, { @@ -65667,7 +80439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#588056" } }, { @@ -65688,7 +80465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#67616c" } }, { @@ -65709,7 +80491,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9c6865" } }, { @@ -65730,7 +80517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#966357" } }, { @@ -65751,7 +80543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8a5849" } }, { @@ -65772,7 +80569,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6a3d2d" } }, { @@ -65793,7 +80595,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#562e23" } }, { @@ -65814,7 +80621,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#41221b" } }, { @@ -65835,7 +80647,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8d6969" } }, { @@ -65856,7 +80673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#896660" } }, { @@ -65877,7 +80699,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#815e56" } }, { @@ -65898,7 +80725,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6b4c44" } }, { @@ -65919,7 +80751,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5f423d" } }, { @@ -65940,7 +80777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#513a37" } }, { @@ -65961,7 +80803,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9d6a67" } }, { @@ -65982,7 +80829,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#976559" } }, { @@ -66003,7 +80855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8c5a4b" } }, { @@ -66024,7 +80881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6c3f30" } }, { @@ -66045,7 +80907,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#593025" } }, { @@ -66066,7 +80933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#44241e" } }, { @@ -66087,7 +80959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#916c6a" } }, { @@ -66108,7 +80985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8d6961" } }, { @@ -66129,7 +81011,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#846157" } }, { @@ -66150,7 +81037,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6d4e43" } }, { @@ -66171,7 +81063,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#60433b" } }, { @@ -66192,7 +81089,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#513a36" } }, { @@ -66213,7 +81115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9f6d69" } }, { @@ -66234,7 +81141,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9a675b" } }, { @@ -66255,7 +81167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8e5c4d" } }, { @@ -66276,7 +81193,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6d4130" } }, { @@ -66297,7 +81219,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5a3126" } }, { @@ -66318,7 +81245,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#44241e" } }, { @@ -66339,7 +81271,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9b706d" } }, { @@ -66360,7 +81297,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#976c62" } }, { @@ -66381,7 +81323,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8d6256" } }, { @@ -66402,7 +81349,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#714b3d" } }, { @@ -66423,7 +81375,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#603e35" } }, { @@ -66444,7 +81401,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4e332e" } }, { @@ -66465,7 +81427,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#956460" } }, { @@ -66486,7 +81453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#905f54" } }, { @@ -66507,7 +81479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#855446" } }, { @@ -66528,7 +81505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#653b2b" } }, { @@ -66549,7 +81531,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#532c21" } }, { @@ -66570,7 +81557,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3e201a" } }, { @@ -66591,7 +81583,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#876565" } }, { @@ -66612,7 +81609,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#83625d" } }, { @@ -66633,7 +81635,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7d5b54" } }, { @@ -66654,7 +81661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#684b43" } }, { @@ -66675,7 +81687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5d413d" } }, { @@ -66696,7 +81713,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#503a38" } }, { @@ -66716,7 +81738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#3a4a36" } }, { @@ -66736,7 +81763,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4591b5" } }, { @@ -66756,7 +81788,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9a97a6" } }, { @@ -66777,7 +81814,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cdb8b2" } }, { @@ -66798,7 +81840,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c6af9b" } }, { @@ -66819,7 +81866,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b89d84" } }, { @@ -66840,7 +81892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#907055" } }, { @@ -66861,7 +81918,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#785643" } }, { @@ -66882,7 +81944,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5f4136" } }, { @@ -66903,7 +81970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c7aea9" } }, { @@ -66924,7 +81996,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c1a796" } }, { @@ -66945,7 +82022,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b59884" } }, { @@ -66966,7 +82048,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#93735c" } }, { @@ -66987,7 +82074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#805e4f" } }, { @@ -67008,7 +82100,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#684b42" } }, { @@ -67029,7 +82126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c4a9a4" } }, { @@ -67050,7 +82152,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bda290" } }, { @@ -67071,7 +82178,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b0917a" } }, { @@ -67092,7 +82204,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8a684f" } }, { @@ -67113,7 +82230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#74503f" } }, { @@ -67134,7 +82256,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5c3d34" } }, { @@ -67155,7 +82282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#696e7a" } }, { @@ -67176,7 +82308,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c6b1ab" } }, { @@ -67197,7 +82334,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b79c86" } }, { @@ -67218,7 +82360,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#aa8b72" } }, { @@ -67239,7 +82386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#866449" } }, { @@ -67260,7 +82412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#755341" } }, { @@ -67281,7 +82438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5c3f35" } }, { @@ -67302,7 +82464,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d0bab4" } }, { @@ -67323,7 +82490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9b29d" } }, { @@ -67344,7 +82516,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ba9e85" } }, { @@ -67365,7 +82542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#917055" } }, { @@ -67386,7 +82568,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#795643" } }, { @@ -67407,7 +82594,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5f4136" } }, { @@ -67428,7 +82620,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d2bdb7" } }, { @@ -67449,7 +82646,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cbb49f" } }, { @@ -67470,7 +82672,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bca187" } }, { @@ -67491,7 +82698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#937256" } }, { @@ -67512,7 +82724,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7a5844" } }, { @@ -67533,7 +82750,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5f4237" } }, { @@ -67554,7 +82776,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a47331" } }, { @@ -67575,7 +82802,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#753054" } }, { @@ -67596,7 +82828,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5e6470" } }, { @@ -67617,7 +82854,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#895f47" } }, { @@ -67638,7 +82880,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b57668" } }, { @@ -67659,7 +82906,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#885e46" } }, { @@ -67681,7 +82933,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7a747f" } }, { @@ -67703,7 +82960,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9e8d89" } }, { @@ -67725,7 +82987,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b8462f" } }, { @@ -67747,7 +83014,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bf53b7" } }, { @@ -67769,7 +83041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b2bfe5" } }, { @@ -67791,7 +83068,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#41bca2" } }, { @@ -67813,7 +83095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#932a25" } }, { @@ -67835,7 +83122,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#e1cbc7" } }, { @@ -67857,7 +83149,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d9c1ad" } }, { @@ -67879,7 +83176,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9ac92" } }, { @@ -67901,7 +83203,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9a785b" } }, { @@ -67923,7 +83230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7f5b47" } }, { @@ -67945,7 +83257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#614338" } }, { @@ -67967,7 +83284,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4ba2aa" } }, { @@ -67989,7 +83311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cbc536" } }, { @@ -68011,7 +83338,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#31a633" } }, { @@ -68033,7 +83365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#7c7681" } }, { @@ -68055,7 +83392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b33c24" } }, { @@ -68077,7 +83419,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9cbcf" } }, { @@ -68099,7 +83446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#373852" } }, { @@ -68121,7 +83473,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#98c6cb" } }, { @@ -68143,7 +83500,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#567a53" } }, { @@ -68165,7 +83527,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b4bfe5" } }, { @@ -68187,7 +83554,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d3bdb6" } }, { @@ -68209,7 +83581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ccb49e" } }, { @@ -68231,7 +83608,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bca085" } }, { @@ -68253,7 +83635,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#917053" } }, { @@ -68275,7 +83662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#775541" } }, { @@ -68297,7 +83689,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5b3f34" } }, { @@ -68319,7 +83716,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dddfe3" } }, { @@ -68341,7 +83743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#6d75a9" } }, { @@ -68363,11 +83770,333 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4eb2a0" } } ] }, + { + "name": "powerup", + "version": "", + "display_name": "Timed power-ups", + "description": "Named effects with a duration and a stacking rule — refresh the clock, extend it, or add a level. That rule is the whole design: picking one by accident is how a power-up ends up either useless or permanent. `powerup_step` returns what expired this frame, so an effect is undone exactly once instead of every frame after it ends.", + "category": "library", + "tags": [ + "capability:powerups", + "capability:buffs", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/powerup/README.md", + "thumbnail_path": "prefabs/powerup/thumbnail.png", + "resources": [ + { + "prefab": "powerup", + "kind": "script", + "name": "scr_powerup", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "powerup", + "kind": "script", + "name": "scr_powerup_query", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "projectile", + "version": "", + "display_name": "Shooting", + "description": "Launch a projectile at a direction and speed, age it, destroy it on timeout or once it is off the room, and report what it hit. Plus aiming at a point, leading a moving target, and a fire-rate cooldown that holding the key cannot outrun. The lifetime is the part everyone forgets: a bullet that flies off the room is still simulated, and the frame rate falls away minutes after the mistake.", + "category": "library", + "tags": [ + "capability:shooting", + "capability:combat", + "genre:shooter", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/projectile/README.md", + "thumbnail_path": "prefabs/projectile/thumbnail.png", + "resources": [ + { + "prefab": "projectile", + "kind": "script", + "name": "scr_projectile", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "projectile", + "kind": "script", + "name": "scr_projectile_aim", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "projectile", + "kind": "script", + "name": "scr_projectile_cooldown", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "quiz_trivia", + "version": "", + "display_name": "Quiz / trivia", + "description": "Answer questions against the clock, with a combo for a streak.", + "category": "game", + "tags": [ + "genre:quiz", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/quiz_trivia/README.md", + "thumbnail_path": "prefabs/quiz_trivia/thumbnail.png", + "resources": [ + { + "prefab": "quiz_trivia", + "kind": "object", + "name": "obj_quiz", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "quiz_trivia", + "kind": "room", + "name": "rm_quiz", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "rhythm", + "version": "", + "display_name": "Rhythm", + "description": "Notes fall to the beat and you hit them on the line — a whole rhythm game whose clock is the song itself.", + "category": "game", + "tags": [ + "genre:rhythm", + "capability:audio", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/rhythm/README.md", + "thumbnail_path": "prefabs/rhythm/thumbnail.png", + "resources": [ + { + "prefab": "rhythm", + "kind": "object", + "name": "obj_note", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rhythm", + "kind": "object", + "name": "obj_rhythm", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rhythm", + "kind": "room", + "name": "rm_song", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rhythm", + "kind": "sound", + "name": "snd_hit", + "description": null, + "tags": [], + "preview": { + "kind": "sound", + "path": "prefabs/rhythm/sounds/snd_hit/source.wav" + } + }, + { + "prefab": "rhythm", + "kind": "sound", + "name": "snd_song", + "description": null, + "tags": [], + "preview": { + "kind": "sound", + "path": "prefabs/rhythm/sounds/snd_song/source.wav" + } + }, + { + "prefab": "rhythm", + "kind": "sprite", + "name": "spr_hitline", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/rhythm/sprites/spr_hitline/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 344, + "height": 6 + }, + "colour": "#ffffff" + } + }, + { + "prefab": "rhythm", + "kind": "sprite", + "name": "spr_note", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/rhythm/sprites/spr_note/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 56, + "height": 20 + }, + "colour": "#ecf0f6" + } + } + ] + }, + { + "name": "rooms_as_state_machines", + "version": "", + "display_name": "Rooms as state machines", + "description": "Title → game → results → loop. The skeleton almost every game starts from.", + "category": "game", + "tags": [ + "genre:structure", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/rooms_as_state_machines/README.md", + "thumbnail_path": "prefabs/rooms_as_state_machines/thumbnail.png", + "resources": [ + { + "prefab": "rooms_as_state_machines", + "kind": "object", + "name": "obj_game_controller", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rooms_as_state_machines", + "kind": "object", + "name": "obj_gameover_controller", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rooms_as_state_machines", + "kind": "object", + "name": "obj_title_controller", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rooms_as_state_machines", + "kind": "room", + "name": "rm_game", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rooms_as_state_machines", + "kind": "room", + "name": "rm_gameover", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "rooms_as_state_machines", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "save_slots", + "version": "", + "display_name": "Save slots", + "description": "Numbered slots holding a struct each, written to a temporary file and moved into place so a crash mid-write cannot leave a save that exists, parses as nothing, and has replaced the good one. Reading a corrupt slot returns your fallback rather than throwing. Includes a summary read for a slot-select screen that wants a level and a play time without loading everything.", + "category": "library", + "tags": [ + "capability:save", + "capability:persistence", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/save_slots/README.md", + "thumbnail_path": "prefabs/save_slots/thumbnail.png", + "resources": [ + { + "prefab": "save_slots", + "kind": "script", + "name": "scr_save_slots", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "save_slots", + "kind": "script", + "name": "scr_save_slots_summary", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "scifi_space_shooter", "version": "", @@ -68381,6 +84110,7 @@ "style:cartoon" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/scifi_space_shooter/README.md", "thumbnail_path": "prefabs/scifi_space_shooter/thumbnail.png", "resources": [ @@ -69556,7 +85286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#211f21" } }, { @@ -69579,7 +85314,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#262024" } }, { @@ -69602,7 +85342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#231e22" } }, { @@ -69625,7 +85370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#1f1e1f" } }, { @@ -69648,7 +85398,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#1f1b1a" } }, { @@ -69671,7 +85426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#001815" } }, { @@ -69694,7 +85454,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#00141e" } }, { @@ -69717,7 +85482,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#10112c" } }, { @@ -69740,7 +85510,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1080, + "height": 1920 + }, + "colour": "#100f2b" } }, { @@ -69767,7 +85542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 176, + "height": 308 + }, + "colour": "#ff7175" } }, { @@ -69794,7 +85574,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#ff7270" } }, { @@ -69820,7 +85605,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 176, + "height": 308 + }, + "colour": "#fefb80" } }, { @@ -69848,7 +85638,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#e8e8e8" } }, { @@ -69877,7 +85672,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 176, + "height": 308 + }, + "colour": "#81ff7e" } }, { @@ -69905,7 +85705,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#89ff67" } }, { @@ -69928,7 +85733,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4898c3" } }, { @@ -69951,7 +85761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5fa8cd" } }, { @@ -69974,7 +85789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4f96be" } }, { @@ -69997,7 +85817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5883be" } }, { @@ -70020,7 +85845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5b85bb" } }, { @@ -70043,7 +85873,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#547bb6" } }, { @@ -70066,7 +85901,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#736ed6" } }, { @@ -70089,7 +85929,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#656bc6" } }, { @@ -70112,7 +85957,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4b5eb4" } }, { @@ -70135,7 +85985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6797d0" } }, { @@ -70158,7 +86013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5081b5" } }, { @@ -70181,7 +86041,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#578ac1" } }, { @@ -70204,7 +86069,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#6598d1" } }, { @@ -70227,7 +86097,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5493bf" } }, { @@ -70250,7 +86125,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4288b5" } }, { @@ -70273,7 +86153,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5e9ac7" } }, { @@ -70296,7 +86181,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5696b9" } }, { @@ -70319,7 +86209,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4380a5" } }, { @@ -70347,7 +86242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 188, + "height": 319 + }, + "colour": "#b63fcf" } }, { @@ -70370,7 +86270,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5c469c" } }, { @@ -70393,7 +86298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#332c77" } }, { @@ -70416,7 +86326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#414675" } }, { @@ -70439,7 +86354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#657ca3" } }, { @@ -70462,7 +86382,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7b7da8" } }, { @@ -70485,7 +86410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#46678c" } }, { @@ -70508,7 +86438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#565e8b" } }, { @@ -70531,7 +86466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5c95ab" } }, { @@ -70554,7 +86494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#64999e" } }, { @@ -70577,7 +86522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#5380b5" } }, { @@ -70609,7 +86559,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 723, + "height": 655 + }, + "colour": "#9b7b44" } }, { @@ -70642,7 +86597,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 593, + "height": 584 + }, + "colour": "#feeaa8" } }, { @@ -70665,7 +86625,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 183 + }, + "colour": "#feb142" } }, { @@ -70688,7 +86653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 183 + }, + "colour": "#ffb042" } }, { @@ -70711,7 +86681,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 183 + }, + "colour": "#ffb747" } }, { @@ -70734,7 +86709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 182, + "height": 183 + }, + "colour": "#ffb750" } }, { @@ -70757,7 +86737,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#283cce" } }, { @@ -70784,7 +86769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#d9dbf4" } }, { @@ -70810,7 +86800,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#ffffff" } }, { @@ -70833,7 +86828,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#76445a" } }, { @@ -70861,7 +86861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#a07770" } }, { @@ -70888,7 +86893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#6ca4f6" } }, { @@ -70914,7 +86924,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#8cbaf8" } }, { @@ -70937,7 +86952,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#744460" } }, { @@ -70965,7 +86985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#af887e" } }, { @@ -70993,7 +87018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#dcf8f9" } }, { @@ -71023,7 +87053,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 274, + "height": 343 + }, + "colour": "#bdfca5" } }, { @@ -71051,7 +87086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 308 + }, + "colour": "#c0fda1" } }, { @@ -71074,7 +87114,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#856e96" } }, { @@ -71104,7 +87149,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#866f95" } }, { @@ -71127,7 +87177,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#867195" } }, { @@ -71150,7 +87205,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8782b3" } }, { @@ -71184,7 +87244,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8882b3" } }, { @@ -71207,7 +87272,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#7c76a7" } }, { @@ -71230,7 +87300,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9281a7" } }, { @@ -71260,7 +87335,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9482a6" } }, { @@ -71283,7 +87363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8d7ba3" } }, { @@ -71311,7 +87396,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 188, + "height": 319 + }, + "colour": "#ffffff" } }, { @@ -71334,7 +87424,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 446, + "height": 431 + }, + "colour": "#11bbda" } }, { @@ -71366,7 +87461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 465, + "height": 484 + }, + "colour": "#46b9d7" } }, { @@ -71389,7 +87489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#887eda" } }, { @@ -71412,7 +87517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#827cd6" } }, { @@ -71435,7 +87545,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#ad8cf8" } }, { @@ -71458,7 +87573,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a787f1" } }, { @@ -71481,7 +87601,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a688f1" } }, { @@ -71504,7 +87629,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9e8bec" } }, { @@ -71527,7 +87657,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9090ec" } }, { @@ -71550,7 +87685,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a687ef" } }, { @@ -71573,7 +87713,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#947fdd" } }, { @@ -71596,7 +87741,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#9880de" } }, { @@ -71619,7 +87769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#918de8" } }, { @@ -71642,7 +87797,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#857ad4" } }, { @@ -71665,7 +87825,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#8079d2" } }, { @@ -71688,7 +87853,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#7476b7" } }, { @@ -71711,7 +87881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6b67b7" } }, { @@ -71734,7 +87909,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#a893e1" } }, { @@ -71757,7 +87937,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#7d70ca" } }, { @@ -71780,7 +87965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#7a7dbc" } }, { @@ -71803,7 +87993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6d63b1" } }, { @@ -71826,7 +88021,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6463a9" } }, { @@ -71849,7 +88049,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#635eab" } }, { @@ -71872,7 +88077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#7b7acb" } }, { @@ -71895,7 +88105,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#a1a7ea" } }, { @@ -71918,7 +88133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#7564bf" } }, { @@ -71941,7 +88161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6e77ad" } }, { @@ -71964,7 +88189,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6a68b4" } }, { @@ -71987,7 +88217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#797cae" } }, { @@ -72010,7 +88245,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#706bc2" } }, { @@ -72033,7 +88273,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#6d77a9" } }, { @@ -72056,7 +88301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 569, + "height": 712 + }, + "colour": "#615ab1" } }, { @@ -72079,7 +88329,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 2048 + }, + "colour": "#9685e5" } } ] @@ -72097,6 +88352,7 @@ "theme:modern" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/scrolling_shmup/README.md", "thumbnail_path": "prefabs/scrolling_shmup/thumbnail.png", "resources": [ @@ -72851,7 +89107,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 15, + "height": 28 + }, + "colour": "#7a6a65" } }, { @@ -72874,7 +89135,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 15, + "height": 30 + }, + "colour": "#8a706b" } }, { @@ -72897,7 +89163,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 16, + "height": 23 + }, + "colour": "#846a66" } }, { @@ -72920,7 +89191,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 16, + "height": 22 + }, + "colour": "#8e7a74" } }, { @@ -72943,7 +89219,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 16, + "height": 30 + }, + "colour": "#816a65" } }, { @@ -72966,7 +89247,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 16, + "height": 15 + }, + "colour": "#795d59" } }, { @@ -72988,7 +89274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 176 + }, + "colour": "#796561" } }, { @@ -73010,7 +89301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 72 + }, + "colour": "#d4edfe" } }, { @@ -73032,7 +89328,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 40, + "height": 36 + }, + "colour": "#34244e" } }, { @@ -73054,7 +89355,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 97, + "height": 70 + }, + "colour": "#d0ebfe" } }, { @@ -73076,7 +89382,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 35 + }, + "colour": "#34244e" } }, { @@ -73098,7 +89409,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 68, + "height": 49 + }, + "colour": "#d3ecfe" } }, { @@ -73120,7 +89436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 25 + }, + "colour": "#34244e" } }, { @@ -73142,7 +89463,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 55, + "height": 36 + }, + "colour": "#d0ebfe" } }, { @@ -73164,7 +89490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 27, + "height": 19 + }, + "colour": "#34244e" } }, { @@ -73186,7 +89517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 27 + }, + "colour": "#d2ecfe" } }, { @@ -73208,7 +89544,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 14 + }, + "colour": "#34244e" } }, { @@ -73234,7 +89575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 56, + "height": 44 + }, + "colour": "#34244e" } }, { @@ -73260,7 +89606,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 112, + "height": 88 + }, + "colour": "#d4edfe" } }, { @@ -73283,7 +89634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#635454" } }, { @@ -73308,7 +89664,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#635454" } }, { @@ -73331,7 +89692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#675858" } }, { @@ -73354,7 +89720,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 22, + "height": 22 + }, + "colour": "#5f4e4e" } }, { @@ -73377,7 +89748,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 22, + "height": 22 + }, + "colour": "#614e4e" } }, { @@ -73404,7 +89780,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 22, + "height": 22 + }, + "colour": "#5f4e4e" } }, { @@ -73427,7 +89808,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 22, + "height": 22 + }, + "colour": "#614f4f" } }, { @@ -73459,7 +89845,12 @@ "speed": { "unit": "frames_per_second", "value": 16.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#282828" } }, { @@ -73491,7 +89882,12 @@ "speed": { "unit": "frames_per_second", "value": 16.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#282828" } }, { @@ -73513,7 +89909,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 47 + }, + "colour": "#aa9991" } }, { @@ -73535,7 +89936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 96, + "height": 47 + }, + "colour": "#e7e7e7" } }, { @@ -73556,7 +89962,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 9, + "height": 1 + }, + "colour": "#bbb592" } }, { @@ -73577,7 +89988,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 5, + "height": 8 + }, + "colour": "#34244e" } }, { @@ -73607,7 +90023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 47 + }, + "colour": "#aa968d" } }, { @@ -73640,7 +90061,12 @@ "speed": { "unit": "frames_per_second", "value": 2.0 - } + }, + "size": { + "width": 80, + "height": 80 + }, + "colour": "#5e645c" } }, { @@ -73673,7 +90099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 80 + }, + "colour": "#6c5f4e" } }, { @@ -73706,7 +90137,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 80 + }, + "colour": "#725750" } }, { @@ -73739,7 +90175,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#7d6c54" } }, { @@ -73772,7 +90213,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#865e57" } }, { @@ -73805,7 +90251,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#7b5760" } }, { @@ -73827,7 +90278,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 13, + "height": 11 + }, + "colour": "#34244e" } }, { @@ -73860,7 +90316,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 48 + }, + "colour": "#696e5c" } }, { @@ -73893,7 +90354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 48 + }, + "colour": "#726959" } }, { @@ -73926,7 +90392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 48, + "height": 48 + }, + "colour": "#796459" } }, { @@ -73955,7 +90426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#836e6e" } }, { @@ -73988,7 +90464,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 80 + }, + "colour": "#8f878d" } }, { @@ -74010,7 +90491,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 13, + "height": 12 + }, + "colour": "#34244e" } }, { @@ -74033,7 +90519,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 32 + }, + "colour": "#926367" } }, { @@ -74056,7 +90547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 32 + }, + "colour": "#7e7461" } }, { @@ -74081,7 +90577,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#e68d8b" } }, { @@ -74104,7 +90605,12 @@ "speed": { "unit": "frames_per_second", "value": 4.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#e27d76" } }, { @@ -74127,7 +90633,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 32 + }, + "colour": "#a47b79" } }, { @@ -74150,7 +90661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 32 + }, + "colour": "#a6898e" } }, { @@ -74173,7 +90689,12 @@ "speed": { "unit": "frames_per_second", "value": 1.0 - } + }, + "size": { + "width": 16, + "height": 32 + }, + "colour": "#778ee2" } }, { @@ -74196,7 +90717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 16, + "height": 16 + }, + "colour": "#db7858" } }, { @@ -74221,7 +90747,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 80 + }, + "colour": "#317bbf" } }, { @@ -74243,7 +90774,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 144, + "height": 48 + }, + "colour": "#787154" } }, { @@ -74265,7 +90801,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 144, + "height": 32 + }, + "colour": "#a48576" } }, { @@ -74287,7 +90828,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 144, + "height": 192 + }, + "colour": "#ada196" } }, { @@ -74320,6 +90866,10 @@ "speed": { "unit": "frames_per_second", "value": 30.0 + }, + "size": { + "width": 8, + "height": 8 } } }, @@ -74400,6 +90950,10 @@ "speed": { "unit": "frames_per_second", "value": 30.0 + }, + "size": { + "width": 8, + "height": 8 } } }, @@ -74423,7 +90977,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 12, + "height": 12 + }, + "colour": "#b15c5c" } }, { @@ -74479,6 +91038,7 @@ "audio_type:sfx" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/sfx_animal/README.md", "thumbnail_path": "prefabs/sfx_animal/thumbnail.png", "resources": [ @@ -76159,6 +92719,7 @@ "theme:fantasy" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/sfx_elemental/README.md", "thumbnail_path": "prefabs/sfx_elemental/thumbnail.png", "resources": [ @@ -76980,6 +93541,7 @@ "role:audio" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/sfx_fantasy/README.md", "thumbnail_path": "prefabs/sfx_fantasy/thumbnail.png", "resources": [ @@ -77993,6 +94555,7 @@ "style:retro" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/sfx_retro_character/README.md", "thumbnail_path": "prefabs/sfx_retro_character/thumbnail.png", "resources": [ @@ -78883,6 +95446,7 @@ "role:audio" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/sfx_retro_ninja/README.md", "thumbnail_path": "prefabs/sfx_retro_ninja/thumbnail.png", "resources": [ @@ -79320,6 +95884,7 @@ "capability:camera" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/sidescroller_world/README.md", "thumbnail_path": "prefabs/sidescroller_world/thumbnail.png", "resources": [ @@ -79384,7 +95949,248 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1600, + "height": 900 + }, + "colour": "#686869" + } + } + ] + }, + { + "name": "snake", + "version": "", + "display_name": "Snake", + "description": "Eat, grow, and do not bite yourself — one step at a time on a grid.", + "category": "game", + "tags": [ + "genre:arcade", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/snake/README.md", + "thumbnail_path": "prefabs/snake/thumbnail.png", + "resources": [ + { + "prefab": "snake", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "object", + "name": "obj_snake", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "snake", + "kind": "script", + "name": "scr_snake_play", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "sokoban", + "version": "", + "display_name": "Sokoban", + "description": "Push every crate onto a mark. You can push, never pull, so a mistake is permanent.", + "category": "game", + "tags": [ + "genre:puzzle", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/sokoban/README.md", + "thumbnail_path": "prefabs/sokoban/thumbnail.png", + "resources": [ + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_block", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_crate", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_pusher", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_sokoban", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "room", + "name": "rm_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "script", + "name": "scr_sokoban_play", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "sokoban", + "kind": "sprite", + "name": "spr_block", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/sokoban/sprites/spr_block/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 40, + "height": 40 + }, + "colour": "#424e7a" + } + }, + { + "prefab": "sokoban", + "kind": "sprite", + "name": "spr_crate", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/sokoban/sprites/spr_crate/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 36, + "height": 36 + }, + "colour": "#d7a55a" + } + }, + { + "prefab": "sokoban", + "kind": "sprite", + "name": "spr_pusher", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/sokoban/sprites/spr_pusher/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 36, + "height": 36 + }, + "colour": "#96d2ff" } } ] @@ -79402,6 +96208,7 @@ "projection:top-down" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/space_rocks_hd/README.md", "thumbnail_path": "prefabs/space_rocks_hd/thumbnail.png", "resources": [ @@ -79455,7 +96262,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 324, + "height": 324 + }, + "colour": "#a6764b" } }, { @@ -79508,7 +96320,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 324, + "height": 324 + }, + "colour": "#4684c3" } }, { @@ -79561,7 +96378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 324, + "height": 324 + }, + "colour": "#494164" } }, { @@ -79614,7 +96436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9d6b4d" } }, { @@ -79667,7 +96494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4d87b9" } }, { @@ -79720,7 +96552,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4c466b" } }, { @@ -79773,7 +96610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#835343" } }, { @@ -79826,7 +96668,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#497fb2" } }, { @@ -79879,7 +96726,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#484266" } }, { @@ -79932,7 +96784,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#8b6753" } }, { @@ -79985,7 +96842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#4c78ac" } }, { @@ -80038,7 +96900,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#554b74" } }, { @@ -80091,7 +96958,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#865c4d" } }, { @@ -80144,7 +97016,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#3e73b2" } }, { @@ -80197,7 +97074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#494774" } }, { @@ -80249,7 +97131,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#744e49" } }, { @@ -80302,7 +97189,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#376fb8" } }, { @@ -80354,7 +97246,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#44416c" } }, { @@ -80388,7 +97285,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 362 + }, + "colour": "#3ea4fe" } }, { @@ -80411,7 +97313,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 143 + }, + "colour": "#4fbbfa" } }, { @@ -80434,7 +97341,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 143 + }, + "colour": "#29d02c" } }, { @@ -80457,7 +97369,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 143 + }, + "colour": "#fe6a03" } }, { @@ -80480,7 +97397,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 201 + }, + "colour": "#0689d6" } }, { @@ -80503,7 +97425,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 201 + }, + "colour": "#14fe3f" } }, { @@ -80526,7 +97453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 201 + }, + "colour": "#fe4b01" } }, { @@ -80551,7 +97483,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1214, + "height": 909 + }, + "colour": "#f7337b" } }, { @@ -80576,7 +97513,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 399, + "height": 429 + }, + "colour": "#154bd3" } }, { @@ -80622,7 +97564,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 998, + "height": 882 + }, + "colour": "#fede6f" } }, { @@ -80666,7 +97613,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 585, + "height": 519 + }, + "colour": "#fede70" } }, { @@ -80707,7 +97659,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 348, + "height": 370 + }, + "colour": "#fedc6f" } }, { @@ -80741,7 +97698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 362 + }, + "colour": "#4ffe36" } }, { @@ -80766,7 +97728,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 184 + }, + "colour": "#3381f9" } }, { @@ -80791,7 +97758,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 184 + }, + "colour": "#46eb31" } }, { @@ -80816,7 +97788,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 184 + }, + "colour": "#f99846" } }, { @@ -80842,7 +97819,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 192 + }, + "colour": "#3481fa" } }, { @@ -80868,7 +97850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 192 + }, + "colour": "#47ea32" } }, { @@ -80894,7 +97881,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 152, + "height": 192 + }, + "colour": "#fa9847" } }, { @@ -80919,7 +97911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1654, + "height": 861 + }, + "colour": "#9dd6ff" } }, { @@ -80944,7 +97941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1812, + "height": 880 + }, + "colour": "#b6e0ff" } }, { @@ -80968,7 +97970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 607, + "height": 606 + }, + "colour": "#20b2fe" } }, { @@ -80992,7 +97999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 607, + "height": 606 + }, + "colour": "#6850cf" } }, { @@ -81016,7 +98028,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 607, + "height": 606 + }, + "colour": "#a95286" } }, { @@ -81039,7 +98056,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 438, + "height": 371 + }, + "colour": "#1446b4" } }, { @@ -81062,7 +98084,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 893, + "height": 893 + }, + "colour": "#35526d" } }, { @@ -81085,7 +98112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 865, + "height": 482 + }, + "colour": "#931f62" } }, { @@ -81109,7 +98141,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 295, + "height": 319 + }, + "colour": "#7b4a3b" } }, { @@ -81133,7 +98170,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 295, + "height": 319 + }, + "colour": "#403a77" } }, { @@ -81157,7 +98199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 295, + "height": 319 + }, + "colour": "#852a3b" } }, { @@ -81181,7 +98228,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 335, + "height": 290 + }, + "colour": "#7b4d3f" } }, { @@ -81205,7 +98257,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 335, + "height": 290 + }, + "colour": "#433d77" } }, { @@ -81229,7 +98286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 335, + "height": 290 + }, + "colour": "#842e3e" } }, { @@ -81253,7 +98315,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 299, + "height": 261 + }, + "colour": "#814934" } }, { @@ -81277,7 +98344,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 299, + "height": 261 + }, + "colour": "#3a347d" } }, { @@ -81301,7 +98373,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 299, + "height": 261 + }, + "colour": "#8d2234" } }, { @@ -81326,7 +98403,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#13161d" } }, { @@ -81351,7 +98433,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#17272f" } }, { @@ -81376,7 +98463,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#050a14" } }, { @@ -81400,7 +98492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 358, + "height": 351 + }, + "colour": "#9fff28" } }, { @@ -81424,7 +98521,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 244, + "height": 161 + }, + "colour": "#e43a6a" } }, { @@ -81448,7 +98550,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 286, + "height": 282 + }, + "colour": "#e10624" } }, { @@ -81481,7 +98588,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 326, + "height": 400 + }, + "colour": "#fe8d57" } }, { @@ -81519,7 +98631,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 326, + "height": 388 + }, + "colour": "#fe8e58" } }, { @@ -81562,7 +98679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 326, + "height": 388 + }, + "colour": "#febd72" } }, { @@ -81596,7 +98718,323 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 362 + }, + "colour": "#ff9c20" + } + } + ] + }, + { + "name": "spawner", + "version": "", + "display_name": "Waves and spawning", + "description": "Spawn things on a timer, in waves, without flooding the room.", + "category": "component", + "tags": [ + "capability:spawning", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/spawner/README.md", + "thumbnail_path": "prefabs/spawner/thumbnail.png", + "resources": [ + { + "prefab": "spawner", + "kind": "object", + "name": "obj_spawner", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "tilemap", + "version": "", + "display_name": "Levels as text", + "description": "Build a room from an array of strings with a legend mapping characters to objects, find the cells holding a character without spawning anything, and size the room to the map. A level held as placed instances can only be changed in an editor; a level held as twelve strings can be changed in a diff, and you can see its shape while you do.", + "category": "library", + "tags": [ + "capability:levels", + "capability:tilemap", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/tilemap/README.md", + "thumbnail_path": "prefabs/tilemap/thumbnail.png", + "resources": [ + { + "prefab": "tilemap", + "kind": "script", + "name": "scr_tilemap", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "tilemap", + "kind": "script", + "name": "scr_tilemap_query", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "timer", + "version": "", + "display_name": "Countdowns and stopwatches", + "description": "Countdowns and stopwatches in seconds rather than frames, pausable, extendable, and formatted as m:ss or m:ss.cc. `timer_step` returns true on the single frame a countdown expires, so the round ends once instead of every frame after zero.", + "category": "library", + "tags": [ + "capability:timing", + "capability:hud", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/timer/README.md", + "thumbnail_path": "prefabs/timer/thumbnail.png", + "resources": [ + { + "prefab": "timer", + "kind": "script", + "name": "scr_timer", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "timer", + "kind": "script", + "name": "scr_timer_format", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "timer", + "kind": "script", + "name": "scr_timer_read", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "title_screen", + "version": "", + "display_name": "Title screen", + "description": "A logo that breathes, a prompt, and a key or click to start.", + "category": "component", + "tags": [ + "capability:ui", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/title_screen/README.md", + "thumbnail_path": "prefabs/title_screen/thumbnail.png", + "resources": [ + { + "prefab": "title_screen", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + } + ] + }, + { + "name": "top_down_arena", + "version": "", + "display_name": "Top-down arena", + "description": "Survive waves in one room: move, dodge, collect, and last as long as you can.", + "category": "game", + "tags": [ + "genre:action", + "role:template" + ], + "asset_bundle": false, + "install": "copy", + "readme_path": "prefabs/top_down_arena/README.md", + "thumbnail_path": "prefabs/top_down_arena/thumbnail.png", + "resources": [ + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_arena", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_coin", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_enemy", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_player", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "object", + "name": "obj_wall", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "room", + "name": "rm_arena", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "room", + "name": "rm_results", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "room", + "name": "rm_title", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "top_down_arena", + "kind": "sprite", + "name": "spr_coin", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/top_down_arena/sprites/spr_coin/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 14, + "height": 14 + }, + "colour": "#f5d250" + } + }, + { + "prefab": "top_down_arena", + "kind": "sprite", + "name": "spr_enemy", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/top_down_arena/sprites/spr_enemy/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 26, + "height": 26 + }, + "colour": "#e65a5a" + } + }, + { + "prefab": "top_down_arena", + "kind": "sprite", + "name": "spr_player", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/top_down_arena/sprites/spr_player/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 28, + "height": 28 + }, + "colour": "#78c8ff" + } + }, + { + "prefab": "top_down_arena", + "kind": "sprite", + "name": "spr_wall", + "description": null, + "tags": [], + "preview": { + "kind": "sprite", + "frames": [ + "prefabs/top_down_arena/sprites/spr_wall/frame_000.png" + ], + "speed": { + "unit": "frames_per_second", + "value": 30.0 + }, + "size": { + "width": 40, + "height": 40 + }, + "colour": "#46506e" } } ] @@ -81614,6 +99052,7 @@ "projection:top-down" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/tower_defense_extended/README.md", "thumbnail_path": "prefabs/tower_defense_extended/thumbnail.png", "resources": [ @@ -81664,7 +99103,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 104, + "height": 93 + }, + "colour": "#fffdfd" } }, { @@ -81745,7 +99189,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 87, + "height": 197 + }, + "colour": "#d8a948" } }, { @@ -81826,7 +99275,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 421, + "height": 145 + }, + "colour": "#00b8bf" } }, { @@ -81908,7 +99362,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 110, + "height": 174 + }, + "colour": "#d74c61" } }, { @@ -82027,7 +99486,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 279, + "height": 44 + }, + "colour": "#fefeb4" } }, { @@ -82107,7 +99571,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 457, + "height": 71 + }, + "colour": "#19be21" } }, { @@ -82227,7 +99696,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 345, + "height": 201 + }, + "colour": "#454132" } }, { @@ -82260,7 +99734,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 64, + "height": 64 + }, + "colour": "#4b3821" } }, { @@ -82340,7 +99819,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 333, + "height": 179 + }, + "colour": "#413c0a" } } ] @@ -82358,6 +99842,7 @@ "projection:top-down" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/track_and_street_racer/README.md", "thumbnail_path": "prefabs/track_and_street_racer/thumbnail.png", "resources": [ @@ -83430,7 +100915,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1024, + "height": 1024 + }, + "colour": "#da8888" } }, { @@ -83452,7 +100942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#da7e7e" } }, { @@ -83474,7 +100969,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d98484" } }, { @@ -83496,7 +100996,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dc8f8f" } }, { @@ -83518,7 +101023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dd8a8a" } }, { @@ -83540,7 +101050,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d98484" } }, { @@ -83562,7 +101077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d98989" } }, { @@ -83584,7 +101104,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d88383" } }, { @@ -83606,7 +101131,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d87d7d" } }, { @@ -83628,7 +101158,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83650,7 +101185,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83672,7 +101212,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83694,7 +101239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83716,7 +101266,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83738,7 +101293,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83760,7 +101320,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83782,7 +101347,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83804,7 +101374,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1024, + "height": 1024 + }, + "colour": "#da8686" } }, { @@ -83826,7 +101401,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2176, + "height": 2176 + }, + "colour": "#da8787" } }, { @@ -83848,7 +101428,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83870,7 +101455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83892,7 +101482,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83914,7 +101509,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -83936,7 +101536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83958,7 +101563,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#de3a3a" } }, { @@ -83980,7 +101590,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -84002,7 +101617,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d5d5d5" } }, { @@ -84024,7 +101644,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 768 + }, + "colour": "#556065" } }, { @@ -84046,7 +101671,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 768 + }, + "colour": "#556065" } }, { @@ -84068,7 +101698,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1920 + }, + "colour": "#556065" } }, { @@ -84090,7 +101725,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 379 + }, + "colour": "#606f7a" } }, { @@ -84112,7 +101752,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 379 + }, + "colour": "#747c7a" } }, { @@ -84134,7 +101779,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 623, + "height": 394 + }, + "colour": "#8fa9c4" } }, { @@ -84156,7 +101806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 421, + "height": 553 + }, + "colour": "#97b0ca" } }, { @@ -84178,7 +101833,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 308, + "height": 375 + }, + "colour": "#95afc9" } }, { @@ -84200,7 +101860,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 96, + "height": 96 + }, + "colour": "#408413" } }, { @@ -84222,7 +101887,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 180 + }, + "colour": "#214c94" } }, { @@ -84244,7 +101914,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 181 + }, + "colour": "#5225a1" } }, { @@ -84266,7 +101941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 180 + }, + "colour": "#991f20" } }, { @@ -84288,7 +101968,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 99, + "height": 187 + }, + "colour": "#131416" } }, { @@ -84310,7 +101995,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 180 + }, + "colour": "#8b919e" } }, { @@ -84332,7 +102022,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 91, + "height": 180 + }, + "colour": "#aaa910" } }, { @@ -84354,7 +102049,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 188 + }, + "colour": "#23437e" } }, { @@ -84376,7 +102076,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 188 + }, + "colour": "#49238b" } }, { @@ -84398,7 +102103,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 188 + }, + "colour": "#8a1e1f" } }, { @@ -84420,7 +102130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 93, + "height": 201 + }, + "colour": "#131416" } }, { @@ -84442,7 +102157,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 187 + }, + "colour": "#7c7d86" } }, { @@ -84464,7 +102184,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 80, + "height": 188 + }, + "colour": "#92900e" } }, { @@ -84486,7 +102211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 103, + "height": 172 + }, + "colour": "#434146" } }, { @@ -84508,7 +102238,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 112, + "height": 112 + }, + "colour": "#3b52ff" } }, { @@ -84530,7 +102265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 112, + "height": 112 + }, + "colour": "#e23333" } }, { @@ -84552,7 +102292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 118, + "height": 188 + }, + "colour": "#141416" } }, { @@ -84574,7 +102319,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 102, + "height": 172 + }, + "colour": "#171e4d" } }, { @@ -84596,7 +102346,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 102, + "height": 172 + }, + "colour": "#19543c" } }, { @@ -84618,7 +102373,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 102, + "height": 172 + }, + "colour": "#3b4048" } }, { @@ -84640,7 +102400,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 102, + "height": 172 + }, + "colour": "#511a15" } }, { @@ -84662,7 +102427,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 103, + "height": 172 + }, + "colour": "#5d6672" } }, { @@ -84684,7 +102454,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 172 + }, + "colour": "#2e3e68" } }, { @@ -84706,7 +102481,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 172 + }, + "colour": "#3e2d80" } }, { @@ -84728,7 +102508,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 171 + }, + "colour": "#7f2a2b" } }, { @@ -84750,7 +102535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 118, + "height": 188 + }, + "colour": "#141416" } }, { @@ -84772,7 +102562,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 171 + }, + "colour": "#756b6f" } }, { @@ -84794,7 +102589,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 101, + "height": 172 + }, + "colour": "#7c7b15" } }, { @@ -84816,7 +102616,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 21, + "height": 32 + }, + "colour": "#201a1a" } }, { @@ -84838,7 +102643,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 67, + "height": 66 + }, + "colour": "#c37231" } }, { @@ -84860,7 +102670,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 66, + "height": 54 + }, + "colour": "#bf6922" } }, { @@ -84882,7 +102697,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 40, + "height": 46 + }, + "colour": "#242629" } }, { @@ -84904,7 +102724,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 91 + }, + "colour": "#242629" } }, { @@ -84926,7 +102751,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 347, + "height": 357 + }, + "colour": "#3c4752" } }, { @@ -84948,7 +102778,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 91 + }, + "colour": "#724924" } }, { @@ -84970,7 +102805,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 40, + "height": 46 + }, + "colour": "#7b5128" } }, { @@ -84992,7 +102832,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 148, + "height": 143 + }, + "colour": "#399314" } }, { @@ -85014,7 +102859,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 95, + "height": 95 + }, + "colour": "#2a8202" } }, { @@ -85036,7 +102886,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 106, + "height": 66 + }, + "colour": "#b48a46" } }, { @@ -85058,7 +102913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 46, + "height": 35 + }, + "colour": "#b38a46" } }, { @@ -85080,7 +102940,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 234, + "height": 261 + }, + "colour": "#257d13" } }, { @@ -85102,7 +102967,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 243, + "height": 221 + }, + "colour": "#45971a" } }, { @@ -85124,7 +102994,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 123, + "height": 83 + }, + "colour": "#444a50" } }, { @@ -85146,7 +103021,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 263, + "height": 141 + }, + "colour": "#6e615c" } }, { @@ -85168,7 +103048,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 54, + "height": 48 + }, + "colour": "#80ae5b" } }, { @@ -85190,7 +103075,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 147, + "height": 202 + }, + "colour": "#b33434" } }, { @@ -85212,7 +103102,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 88, + "height": 88 + }, + "colour": "#b65051" } }, { @@ -85234,7 +103129,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 90, + "height": 91 + }, + "colour": "#4b7b9d" } }, { @@ -85256,7 +103156,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 268, + "height": 159 + }, + "colour": "#6ebb2e" } }, { @@ -85278,7 +103183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 141 + }, + "colour": "#60ae23" } }, { @@ -85300,7 +103210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 444 + }, + "colour": "#566675" } }, { @@ -85322,7 +103237,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 444 + }, + "colour": "#914b2e" } }, { @@ -85344,7 +103264,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 444 + }, + "colour": "#91632e" } }, { @@ -85366,7 +103291,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 369 + }, + "colour": "#485a6b" } }, { @@ -85388,7 +103318,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 369 + }, + "colour": "#89442a" } }, { @@ -85410,7 +103345,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 378, + "height": 369 + }, + "colour": "#895f2a" } }, { @@ -85432,7 +103372,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 328, + "height": 444 + }, + "colour": "#57656e" } }, { @@ -85454,7 +103399,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 328, + "height": 444 + }, + "colour": "#934f32" } }, { @@ -85476,7 +103426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 328, + "height": 444 + }, + "colour": "#936632" } }, { @@ -85498,7 +103453,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 58, + "height": 134 + }, + "colour": "#474c4d" } }, { @@ -85520,7 +103480,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 72, + "height": 503 + }, + "colour": "#5a6067" } }, { @@ -85542,7 +103507,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 65, + "height": 101 + }, + "colour": "#8eff74" } }, { @@ -85564,7 +103534,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 65, + "height": 101 + }, + "colour": "#f53232" } }, { @@ -85586,7 +103561,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#161413" } }, { @@ -85608,7 +103588,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 73, + "height": 43 + }, + "colour": "#161413" } }, { @@ -85630,7 +103615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 123, + "height": 93 + }, + "colour": "#171514" } }, { @@ -85652,7 +103642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 63 + }, + "colour": "#171514" } }, { @@ -85674,7 +103669,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 160, + "height": 157 + }, + "colour": "#91c4ee" } }, { @@ -85696,7 +103696,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 161, + "height": 161 + }, + "colour": "#b96a60" } }, { @@ -85718,7 +103723,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 161, + "height": 161 + }, + "colour": "#78756f" } }, { @@ -85740,7 +103750,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 228, + "height": 162 + }, + "colour": "#dfe5f5" } }, { @@ -85762,7 +103777,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 117, + "height": 107 + }, + "colour": "#51a519" } }, { @@ -85784,7 +103804,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 103, + "height": 66 + }, + "colour": "#78a4a6" } }, { @@ -85806,7 +103831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 43, + "height": 36 + }, + "colour": "#78a4a6" } }, { @@ -85828,7 +103858,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 272, + "height": 51 + }, + "colour": "#9e9f86" } }, { @@ -85850,7 +103885,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1342, + "height": 1334 + }, + "colour": "#4f5d76" } }, { @@ -85872,7 +103912,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -85894,7 +103939,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -85916,7 +103966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -85938,7 +103993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5b74" } }, { @@ -85960,7 +104020,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -85982,7 +104047,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86004,7 +104074,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86026,7 +104101,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86048,7 +104128,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5b74" } }, { @@ -86070,7 +104155,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86092,7 +104182,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86114,7 +104209,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86136,7 +104236,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86158,7 +104263,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86180,7 +104290,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86202,7 +104317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86224,7 +104344,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86246,7 +104371,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#505b74" } }, { @@ -86268,7 +104398,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5b74" } }, { @@ -86290,7 +104425,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 657, + "height": 338 + }, + "colour": "#4f5a74" } }, { @@ -86312,7 +104452,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86334,7 +104479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86356,7 +104506,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86378,7 +104533,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86400,7 +104560,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86422,7 +104587,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86444,7 +104614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86466,7 +104641,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86488,7 +104668,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86510,7 +104695,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86532,7 +104722,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86554,7 +104749,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86576,7 +104776,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86598,7 +104803,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86620,7 +104830,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86642,7 +104857,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86664,7 +104884,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86686,7 +104911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#505d75" } }, { @@ -86708,7 +104938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86730,7 +104965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 340, + "height": 338 + }, + "colour": "#4f5d75" } }, { @@ -86752,7 +104992,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 106, + "height": 358 + }, + "colour": "#dfe5f5" } }, { @@ -86774,7 +105019,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 194, + "height": 153 + }, + "colour": "#dfe5f5" } }, { @@ -86796,7 +105046,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#4f7f2b" } }, { @@ -86818,7 +105073,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#a47c3a" } }, { @@ -86840,7 +105100,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 512 + }, + "colour": "#4c5c68" } }, { @@ -86862,7 +105127,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 300, + "height": 284 + }, + "colour": "#69b923" } }, { @@ -86884,7 +105154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 345, + "height": 342 + }, + "colour": "#6ab726" } }, { @@ -86906,7 +105181,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 326, + "height": 339 + }, + "colour": "#67b427" } }, { @@ -86928,7 +105208,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 179, + "height": 179 + }, + "colour": "#46464d" } }, { @@ -86950,7 +105235,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 61, + "height": 61 + }, + "colour": "#131418" } }, { @@ -86972,7 +105262,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 181, + "height": 102 + }, + "colour": "#131318" } }, { @@ -86994,7 +105289,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 190, + "height": 61 + }, + "colour": "#303234" } }, { @@ -87016,7 +105316,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 207, + "height": 122 + }, + "colour": "#26292b" } }, { @@ -87038,7 +105343,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 219, + "height": 129 + }, + "colour": "#26292b" } }, { @@ -87060,7 +105370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 61, + "height": 61 + }, + "colour": "#a52d28" } }, { @@ -87082,7 +105397,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 181, + "height": 102 + }, + "colour": "#aa2e28" } }, { @@ -87104,7 +105424,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 61, + "height": 61 + }, + "colour": "#9ca3b7" } }, { @@ -87126,7 +105451,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 181, + "height": 102 + }, + "colour": "#a1a9bd" } }, { @@ -87148,7 +105478,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 26 + }, + "colour": "#17171d" } }, { @@ -87170,7 +105505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 26 + }, + "colour": "#b54541" } }, { @@ -87192,7 +105532,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 157, + "height": 26 + }, + "colour": "#bbc1d1" } }, { @@ -87214,11 +105559,50 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 144, + "height": 64 + }, + "colour": "#888888" } } ] }, + { + "name": "transition", + "version": "", + "display_name": "Room transitions", + "description": "Fades, wipes and closing bars, with the room change performed on the frame the screen is fully covered. Going first and fading after shows the new room for a frame, which is worse than no transition at all. Includes a busy flag to gate input on, so the player stops playing a room they are leaving.", + "category": "library", + "tags": [ + "capability:transitions", + "capability:rooms", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/transition/README.md", + "thumbnail_path": "prefabs/transition/thumbnail.png", + "resources": [ + { + "prefab": "transition", + "kind": "script", + "name": "scr_transition", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "transition", + "kind": "script", + "name": "scr_transition_draw", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "transition_angular", "version": "", @@ -87231,6 +105615,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_angular/README.md", "thumbnail_path": "prefabs/transition_angular/thumbnail.png", "resources": [ @@ -87263,6 +105648,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_base/README.md", "thumbnail_path": "prefabs/transition_base/thumbnail.png", "resources": [ @@ -87312,6 +105698,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_bounce/README.md", "thumbnail_path": "prefabs/transition_bounce/thumbnail.png", "resources": [ @@ -87345,6 +105732,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_bowtie/README.md", "thumbnail_path": "prefabs/transition_bowtie/thumbnail.png", "resources": [ @@ -87370,6 +105758,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_burn/README.md", "thumbnail_path": "prefabs/transition_burn/thumbnail.png", "resources": [ @@ -87395,6 +105784,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_butterfly_wave/README.md", "thumbnail_path": "prefabs/transition_butterfly_wave/thumbnail.png", "resources": [ @@ -87428,6 +105818,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_caleidoscope/README.md", "thumbnail_path": "prefabs/transition_caleidoscope/thumbnail.png", "resources": [ @@ -87461,6 +105852,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_circle_crop/README.md", "thumbnail_path": "prefabs/transition_circle_crop/thumbnail.png", "resources": [ @@ -87486,6 +105878,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_circle_open/README.md", "thumbnail_path": "prefabs/transition_circle_open/thumbnail.png", "resources": [ @@ -87519,6 +105912,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_color_distance/README.md", "thumbnail_path": "prefabs/transition_color_distance/thumbnail.png", "resources": [ @@ -87552,6 +105946,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_color_phase/README.md", "thumbnail_path": "prefabs/transition_color_phase/thumbnail.png", "resources": [ @@ -87577,6 +105972,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_crazy_parametric/README.md", "thumbnail_path": "prefabs/transition_crazy_parametric/thumbnail.png", "resources": [ @@ -87610,6 +106006,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_cross_hatch/README.md", "thumbnail_path": "prefabs/transition_cross_hatch/thumbnail.png", "resources": [ @@ -87643,6 +106040,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_cross_warp/README.md", "thumbnail_path": "prefabs/transition_cross_warp/thumbnail.png", "resources": [ @@ -87668,6 +106066,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_cross_zoom/README.md", "thumbnail_path": "prefabs/transition_cross_zoom/thumbnail.png", "resources": [ @@ -87701,6 +106100,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_cube/README.md", "thumbnail_path": "prefabs/transition_cube/thumbnail.png", "resources": [ @@ -87734,6 +106134,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_directional/README.md", "thumbnail_path": "prefabs/transition_directional/thumbnail.png", "resources": [ @@ -87759,6 +106160,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_directional_warp/README.md", "thumbnail_path": "prefabs/transition_directional_warp/thumbnail.png", "resources": [ @@ -87784,6 +106186,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_doom/README.md", "thumbnail_path": "prefabs/transition_doom/thumbnail.png", "resources": [ @@ -87817,6 +106220,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_doorway/README.md", "thumbnail_path": "prefabs/transition_doorway/thumbnail.png", "resources": [ @@ -87850,6 +106254,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_dreamy/README.md", "thumbnail_path": "prefabs/transition_dreamy/thumbnail.png", "resources": [ @@ -87875,6 +106280,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_dreamy_zoom/README.md", "thumbnail_path": "prefabs/transition_dreamy_zoom/thumbnail.png", "resources": [ @@ -87908,6 +106314,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_fade_color/README.md", "thumbnail_path": "prefabs/transition_fade_color/thumbnail.png", "resources": [ @@ -87941,6 +106348,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_fade_grayscale/README.md", "thumbnail_path": "prefabs/transition_fade_grayscale/thumbnail.png", "resources": [ @@ -87974,6 +106382,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_flye_eye/README.md", "thumbnail_path": "prefabs/transition_flye_eye/thumbnail.png", "resources": [ @@ -88007,6 +106416,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_glitch_displace/README.md", "thumbnail_path": "prefabs/transition_glitch_displace/thumbnail.png", "resources": [ @@ -88032,6 +106442,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_glitch_memories/README.md", "thumbnail_path": "prefabs/transition_glitch_memories/thumbnail.png", "resources": [ @@ -88057,6 +106468,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_grid_flip/README.md", "thumbnail_path": "prefabs/transition_grid_flip/thumbnail.png", "resources": [ @@ -88090,6 +106502,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_heart/README.md", "thumbnail_path": "prefabs/transition_heart/thumbnail.png", "resources": [ @@ -88115,6 +106528,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_hexagonalize/README.md", "thumbnail_path": "prefabs/transition_hexagonalize/thumbnail.png", "resources": [ @@ -88148,6 +106562,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_leaf/README.md", "thumbnail_path": "prefabs/transition_leaf/thumbnail.png", "resources": [ @@ -88173,6 +106588,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_linear_blur/README.md", "thumbnail_path": "prefabs/transition_linear_blur/thumbnail.png", "resources": [ @@ -88206,6 +106622,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_luminance_melt/README.md", "thumbnail_path": "prefabs/transition_luminance_melt/thumbnail.png", "resources": [ @@ -88239,6 +106656,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_morph/README.md", "thumbnail_path": "prefabs/transition_morph/thumbnail.png", "resources": [ @@ -88272,6 +106690,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_mosaic/README.md", "thumbnail_path": "prefabs/transition_mosaic/thumbnail.png", "resources": [ @@ -88297,6 +106716,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_multiply_blend/README.md", "thumbnail_path": "prefabs/transition_multiply_blend/thumbnail.png", "resources": [ @@ -88322,6 +106742,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_perlin/README.md", "thumbnail_path": "prefabs/transition_perlin/thumbnail.png", "resources": [ @@ -88355,6 +106776,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_pinwheel/README.md", "thumbnail_path": "prefabs/transition_pinwheel/thumbnail.png", "resources": [ @@ -88388,6 +106810,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_pixelize/README.md", "thumbnail_path": "prefabs/transition_pixelize/thumbnail.png", "resources": [ @@ -88413,6 +106836,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_polar_function/README.md", "thumbnail_path": "prefabs/transition_polar_function/thumbnail.png", "resources": [ @@ -88438,6 +106862,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_polka_dots_curtain/README.md", "thumbnail_path": "prefabs/transition_polka_dots_curtain/thumbnail.png", "resources": [ @@ -88471,6 +106896,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_radial/README.md", "thumbnail_path": "prefabs/transition_radial/thumbnail.png", "resources": [ @@ -88504,6 +106930,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_random_squares/README.md", "thumbnail_path": "prefabs/transition_random_squares/thumbnail.png", "resources": [ @@ -88537,6 +106964,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_ripple/README.md", "thumbnail_path": "prefabs/transition_ripple/thumbnail.png", "resources": [ @@ -88570,6 +106998,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_rotate_scale_face/README.md", "thumbnail_path": "prefabs/transition_rotate_scale_face/thumbnail.png", "resources": [ @@ -88603,6 +107032,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_simple_zoom/README.md", "thumbnail_path": "prefabs/transition_simple_zoom/thumbnail.png", "resources": [ @@ -88636,6 +107066,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_squares_wire/README.md", "thumbnail_path": "prefabs/transition_squares_wire/thumbnail.png", "resources": [ @@ -88669,6 +107100,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_squeeze/README.md", "thumbnail_path": "prefabs/transition_squeeze/thumbnail.png", "resources": [ @@ -88702,6 +107134,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_swap/README.md", "thumbnail_path": "prefabs/transition_swap/thumbnail.png", "resources": [ @@ -88735,6 +107168,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_swirl/README.md", "thumbnail_path": "prefabs/transition_swirl/thumbnail.png", "resources": [ @@ -88760,6 +107194,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_undulating_burnout/README.md", "thumbnail_path": "prefabs/transition_undulating_burnout/thumbnail.png", "resources": [ @@ -88793,6 +107228,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_water_drop/README.md", "thumbnail_path": "prefabs/transition_water_drop/thumbnail.png", "resources": [ @@ -88826,6 +107262,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_wind/README.md", "thumbnail_path": "prefabs/transition_wind/thumbnail.png", "resources": [ @@ -88859,6 +107296,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_window_blinds/README.md", "thumbnail_path": "prefabs/transition_window_blinds/thumbnail.png", "resources": [ @@ -88884,6 +107322,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_window_slice/README.md", "thumbnail_path": "prefabs/transition_window_slice/thumbnail.png", "resources": [ @@ -88917,6 +107356,7 @@ "role:library" ], "asset_bundle": false, + "install": "namespace", "readme_path": "prefabs/transition_zoom_in_circles/README.md", "thumbnail_path": "prefabs/transition_zoom_in_circles/thumbnail.png", "resources": [ @@ -88930,6 +107370,48 @@ } ] }, + { + "name": "ui_board", + "version": "", + "display_name": "Interactive UI board", + "description": "Pointer-driven hotspots with hover, press and disabled states, grid and row layouts, selection and answer slots. The catalog holds roughly 1,500 UI and icon sprites with no gameplay layer that can use them; this is that layer.", + "category": "library", + "tags": [ + "capability:ui", + "capability:pointer", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/ui_board/README.md", + "thumbnail_path": "prefabs/ui_board/thumbnail.png", + "resources": [ + { + "prefab": "ui_board", + "kind": "script", + "name": "scr_ui_board", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "ui_board", + "kind": "script", + "name": "scr_ui_board_draw", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "ui_board", + "kind": "script", + "name": "scr_ui_board_layout", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "ui_icons_pixel_flat", "version": "", @@ -88945,6 +107427,7 @@ "style:flat" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_pixel_flat/README.md", "thumbnail_path": "prefabs/ui_icons_pixel_flat/thumbnail.png", "resources": [ @@ -88964,7 +107447,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -88983,7 +107471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89002,7 +107495,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89021,7 +107519,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89040,7 +107543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89059,7 +107567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89078,7 +107591,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89097,7 +107615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89116,7 +107639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89135,7 +107663,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89154,7 +107687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89173,7 +107711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89192,7 +107735,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89211,7 +107759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89230,7 +107783,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89249,7 +107807,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89268,7 +107831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89287,7 +107855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89306,7 +107879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89325,7 +107903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89344,7 +107927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89363,7 +107951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89382,7 +107975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89401,7 +107999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89420,7 +108023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89439,7 +108047,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89458,7 +108071,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89477,7 +108095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89496,7 +108119,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89515,7 +108143,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89534,7 +108167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89553,7 +108191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89572,7 +108215,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89591,7 +108239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89610,7 +108263,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89629,7 +108287,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -89648,7 +108311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } } ] @@ -89668,6 +108336,7 @@ "style:stroke" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_pixel_stroke/README.md", "thumbnail_path": "prefabs/ui_icons_pixel_stroke/thumbnail.png", "resources": [ @@ -89687,7 +108356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -89706,7 +108380,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -89725,7 +108404,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -89744,7 +108428,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -89763,7 +108452,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c6c6c6" } }, { @@ -89782,7 +108476,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b3b3b3" } }, { @@ -89801,7 +108500,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c1c1c1" } }, { @@ -89820,7 +108524,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bababa" } }, { @@ -89839,7 +108548,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c5c5c5" } }, { @@ -89858,7 +108572,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#adadad" } }, { @@ -89877,7 +108596,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c5c5c5" } }, { @@ -89896,7 +108620,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d4d4d4" } }, { @@ -89915,7 +108644,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c9c9c9" } }, { @@ -89934,7 +108668,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a1a1a1" } }, { @@ -89953,7 +108692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a8a8a" } }, { @@ -89972,7 +108716,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a5a5a5" } }, { @@ -89991,7 +108740,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c2c2c2" } }, { @@ -90010,7 +108764,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a6a6a6" } }, { @@ -90029,7 +108788,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c8c8c8" } }, { @@ -90048,7 +108812,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c4c4c4" } }, { @@ -90067,7 +108836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bababa" } }, { @@ -90086,7 +108860,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c8c8c8" } }, { @@ -90105,7 +108884,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfbfbf" } }, { @@ -90124,7 +108908,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -90143,7 +108932,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#acacac" } }, { @@ -90162,7 +108956,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b2b2b2" } }, { @@ -90181,7 +108980,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c5c5c5" } }, { @@ -90200,7 +109004,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bbbbbb" } }, { @@ -90219,7 +109028,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#888888" } }, { @@ -90238,7 +109052,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d1d1d1" } }, { @@ -90257,7 +109076,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8e8e8e" } }, { @@ -90276,7 +109100,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c1c1c1" } }, { @@ -90295,7 +109124,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cfcfcf" } }, { @@ -90314,7 +109148,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cfcfcf" } }, { @@ -90333,7 +109172,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfbfbf" } }, { @@ -90352,7 +109196,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a0a0a0" } }, { @@ -90371,7 +109220,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8b8b8b" } } ] @@ -90391,6 +109245,7 @@ "style:outlined" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_pixel_thick/README.md", "thumbnail_path": "prefabs/ui_icons_pixel_thick/thumbnail.png", "resources": [ @@ -90410,7 +109265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -90429,7 +109289,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#929292" } }, { @@ -90448,7 +109313,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#929292" } }, { @@ -90467,7 +109337,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -90486,7 +109361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#959595" } }, { @@ -90505,7 +109385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7f7f7f" } }, { @@ -90524,7 +109409,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8e8e8e" } }, { @@ -90543,7 +109433,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#818181" } }, { @@ -90562,7 +109457,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#919191" } }, { @@ -90581,7 +109481,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#797979" } }, { @@ -90600,7 +109505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -90619,7 +109529,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a7a7a7" } }, { @@ -90638,7 +109553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#979797" } }, { @@ -90657,7 +109577,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#666666" } }, { @@ -90676,7 +109601,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#545454" } }, { @@ -90695,7 +109625,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#6e6e6e" } }, { @@ -90714,7 +109649,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8e8e8e" } }, { @@ -90733,7 +109673,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#6b6b6b" } }, { @@ -90752,7 +109697,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#959595" } }, { @@ -90771,7 +109721,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8e8e8e" } }, { @@ -90790,7 +109745,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#898989" } }, { @@ -90809,7 +109769,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#949494" } }, { @@ -90828,7 +109793,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#888888" } }, { @@ -90847,7 +109817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#979797" } }, { @@ -90866,7 +109841,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#747474" } }, { @@ -90885,7 +109865,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#797979" } }, { @@ -90904,7 +109889,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -90923,7 +109913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#878787" } }, { @@ -90942,7 +109937,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4b4b4b" } }, { @@ -90961,7 +109961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a3a3a3" } }, { @@ -90980,7 +109985,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#565656" } }, { @@ -90999,7 +110009,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a8a8a" } }, { @@ -91018,7 +110033,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9f9f9f" } }, { @@ -91037,7 +110057,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9f9f9f" } }, { @@ -91056,7 +110081,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#888888" } }, { @@ -91075,7 +110105,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#696969" } }, { @@ -91094,7 +110129,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#555555" } } ] @@ -91113,6 +110153,7 @@ "style:flat" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_round_flat/README.md", "thumbnail_path": "prefabs/ui_icons_round_flat/thumbnail.png", "resources": [ @@ -91132,7 +110173,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91151,7 +110197,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91170,7 +110221,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91189,7 +110245,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91208,7 +110269,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91227,7 +110293,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91246,7 +110317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91265,7 +110341,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91284,7 +110365,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91303,7 +110389,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91322,7 +110413,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91341,7 +110437,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91360,7 +110461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91379,7 +110485,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91398,7 +110509,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91417,7 +110533,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91436,7 +110557,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91455,7 +110581,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91474,7 +110605,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91493,7 +110629,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91512,7 +110653,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91531,7 +110677,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91550,7 +110701,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91569,7 +110725,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91588,7 +110749,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91607,7 +110773,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91626,7 +110797,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91645,7 +110821,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91664,7 +110845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91683,7 +110869,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91702,7 +110893,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91721,7 +110917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91740,7 +110941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91759,7 +110965,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91778,7 +110989,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91797,7 +111013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } }, { @@ -91816,7 +111037,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ffffff" } } ] @@ -91836,6 +111062,7 @@ "style:stroke" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_round_stroke/README.md", "thumbnail_path": "prefabs/ui_icons_round_stroke/thumbnail.png", "resources": [ @@ -91855,7 +111082,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -91874,7 +111106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -91893,7 +111130,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -91912,7 +111154,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -91931,7 +111178,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c9c9c9" } }, { @@ -91950,7 +111202,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b6b6b6" } }, { @@ -91969,7 +111226,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c2c2c2" } }, { @@ -91988,7 +111250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bcbcbc" } }, { @@ -92007,7 +111274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cccccc" } }, { @@ -92026,7 +111298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a3a3a3" } }, { @@ -92045,7 +111322,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cecece" } }, { @@ -92064,7 +111346,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dbdbdb" } }, { @@ -92083,7 +111370,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d0d0d0" } }, { @@ -92102,7 +111394,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9b9b9b" } }, { @@ -92121,7 +111418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#969696" } }, { @@ -92140,7 +111442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#acacac" } }, { @@ -92159,7 +111466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bababa" } }, { @@ -92178,7 +111490,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b3b3b3" } }, { @@ -92197,7 +111514,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c6c6c6" } }, { @@ -92216,7 +111538,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c2c2c2" } }, { @@ -92235,7 +111562,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bebebe" } }, { @@ -92254,7 +111586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2d2d2" } }, { @@ -92273,7 +111610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cdcdcd" } }, { @@ -92292,7 +111634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d0d0d0" } }, { @@ -92311,7 +111658,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a8a8a8" } }, { @@ -92330,7 +111682,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bababa" } }, { @@ -92349,7 +111706,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cecece" } }, { @@ -92368,7 +111730,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b6b6b6" } }, { @@ -92387,7 +111754,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9b9b9b" } }, { @@ -92406,7 +111778,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d9d9d9" } }, { @@ -92425,7 +111802,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -92444,7 +111826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cecece" } }, { @@ -92463,7 +111850,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d4d4d4" } }, { @@ -92482,7 +111874,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d4d4d4" } }, { @@ -92501,7 +111898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -92520,7 +111922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8f8f8f" } }, { @@ -92539,7 +111946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9a9a9a" } } ] @@ -92558,6 +111970,7 @@ "style:outlined" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_round_thick/README.md", "thumbnail_path": "prefabs/ui_icons_round_thick/thumbnail.png", "resources": [ @@ -92577,7 +111990,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#909090" } }, { @@ -92596,7 +112014,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a8a8a" } }, { @@ -92615,7 +112038,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a8a8a" } }, { @@ -92634,7 +112062,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8e8e8e" } }, { @@ -92653,7 +112086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#989898" } }, { @@ -92672,7 +112110,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#838383" } }, { @@ -92691,7 +112134,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#919191" } }, { @@ -92710,7 +112158,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#848484" } }, { @@ -92729,7 +112182,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9a9a9a" } }, { @@ -92748,7 +112206,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#707070" } }, { @@ -92767,7 +112230,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a0a0a0" } }, { @@ -92786,7 +112254,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b4b4b4" } }, { @@ -92805,7 +112278,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a1a1a1" } }, { @@ -92824,7 +112302,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#606060" } }, { @@ -92843,7 +112326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5d5d5d" } }, { @@ -92862,7 +112350,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#747474" } }, { @@ -92881,7 +112374,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#858585" } }, { @@ -92900,7 +112398,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7b7b7b" } }, { @@ -92919,7 +112422,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#929292" } }, { @@ -92938,7 +112446,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8b8b8b" } }, { @@ -92957,7 +112470,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8f8f8f" } }, { @@ -92976,7 +112494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a4a4a4" } }, { @@ -92995,7 +112518,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9a9a9a" } }, { @@ -93014,7 +112542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a3a3a3" } }, { @@ -93033,7 +112566,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#707070" } }, { @@ -93052,7 +112590,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#838383" } }, { @@ -93071,7 +112614,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a0a0a0" } }, { @@ -93090,7 +112638,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7d7d7d" } }, { @@ -93109,7 +112662,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5d5d5d" } }, { @@ -93128,7 +112686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b0b0b0" } }, { @@ -93147,7 +112710,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5e5e5e" } }, { @@ -93166,7 +112734,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#9d9d9d" } }, { @@ -93185,7 +112758,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a7a7a7" } }, { @@ -93204,7 +112782,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a6a6a6" } }, { @@ -93223,7 +112806,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#939393" } }, { @@ -93242,7 +112830,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#565656" } }, { @@ -93261,7 +112854,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#636363" } } ] @@ -93280,6 +112878,7 @@ "style:flat" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_icons_shadow_flat/README.md", "thumbnail_path": "prefabs/ui_icons_shadow_flat/thumbnail.png", "resources": [ @@ -93299,7 +112898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bbbbbb" } }, { @@ -93318,7 +112922,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bdbdbd" } }, { @@ -93337,7 +112946,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b9b9b9" } }, { @@ -93356,7 +112970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c1c1c1" } }, { @@ -93375,7 +112994,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b0b0b0" } }, { @@ -93394,7 +113018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -93413,7 +113042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b9b9b9" } }, { @@ -93432,7 +113066,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ababab" } }, { @@ -93451,7 +113090,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c3c3c3" } }, { @@ -93470,7 +113114,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#909090" } }, { @@ -93489,7 +113138,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d0d0d0" } }, { @@ -93508,7 +113162,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e0e0e0" } }, { @@ -93527,7 +113186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e0e0e0" } }, { @@ -93546,7 +113210,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a1a1a1" } }, { @@ -93565,7 +113234,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#959595" } }, { @@ -93584,7 +113258,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c5c5c5" } }, { @@ -93603,7 +113282,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c0c0c0" } }, { @@ -93622,7 +113306,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b3b3b3" } }, { @@ -93641,7 +113330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cdcdcd" } }, { @@ -93660,7 +113354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c8c8c8" } }, { @@ -93679,7 +113378,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b3b3b3" } }, { @@ -93698,7 +113402,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c9c9c9" } }, { @@ -93717,7 +113426,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7c7c7" } }, { @@ -93736,7 +113450,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cfcfcf" } }, { @@ -93755,7 +113474,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a9a9a9" } }, { @@ -93774,7 +113498,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b0b0b0" } }, { @@ -93793,7 +113522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d6d6d6" } }, { @@ -93812,7 +113546,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c6c6c6" } }, { @@ -93831,7 +113570,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#959595" } }, { @@ -93850,7 +113594,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d9d9d9" } }, { @@ -93869,7 +113618,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a6a6a6" } }, { @@ -93888,7 +113642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cccccc" } }, { @@ -93907,7 +113666,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d1d1d1" } }, { @@ -93926,7 +113690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d7d7d7" } }, { @@ -93945,7 +113714,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b2b2b2" } }, { @@ -93964,7 +113738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7a7a7a" } }, { @@ -93983,7 +113762,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#a9a9a9" } } ] @@ -94003,6 +113787,7 @@ "capability:ui" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_panels_flat/README.md", "thumbnail_path": "prefabs/ui_panels_flat/thumbnail.png", "resources": [ @@ -94022,7 +113807,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#4a6ce8" } }, { @@ -94041,7 +113831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#486ae3" } }, { @@ -94060,7 +113855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4460cd" } }, { @@ -94079,7 +113879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4461cd" } }, { @@ -94098,7 +113903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#334a9f" } }, { @@ -94117,7 +113927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#4e72f4" } }, { @@ -94136,7 +113951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#4868dd" } }, { @@ -94155,7 +113975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#4767db" } }, { @@ -94174,7 +113999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ccd2dc" } }, { @@ -94193,7 +114023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#334a9f" } }, { @@ -94212,7 +114047,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#4e72f4" } }, { @@ -94231,7 +114071,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#334a9f" } }, { @@ -94250,7 +114095,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#4e72f4" } }, { @@ -94269,7 +114119,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4360cc" } }, { @@ -94288,7 +114143,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#4563d2" } }, { @@ -94307,7 +114167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#63baeb" } }, { @@ -94326,7 +114191,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#61b5e6" } }, { @@ -94345,7 +114215,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#57a5cf" } }, { @@ -94364,7 +114239,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#58a6cf" } }, { @@ -94383,7 +114263,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#447fa1" } }, { @@ -94402,7 +114287,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#68c3f7" } }, { @@ -94421,7 +114311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#5eb1df" } }, { @@ -94440,7 +114335,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#5db0dd" } }, { @@ -94459,7 +114359,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d1e1dd" } }, { @@ -94478,7 +114383,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#447fa1" } }, { @@ -94497,7 +114407,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#68c3f7" } }, { @@ -94516,7 +114431,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#447fa1" } }, { @@ -94535,7 +114455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#68c3f7" } }, { @@ -94554,7 +114479,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#57a5ce" } }, { @@ -94573,7 +114503,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5aaad5" } }, { @@ -94592,7 +114527,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#68f055" } }, { @@ -94611,7 +114551,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#65eb54" } }, { @@ -94630,7 +114575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5cd24d" } }, { @@ -94649,7 +114599,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5dd34d" } }, { @@ -94668,7 +114623,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#47a43b" } }, { @@ -94687,7 +114647,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#6dfc5a" } }, { @@ -94706,7 +114671,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#63e352" } }, { @@ -94725,7 +114695,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#62e151" } }, { @@ -94744,7 +114719,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2ebc1" } }, { @@ -94763,7 +114743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#47a43b" } }, { @@ -94782,7 +114767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#6dfc5a" } }, { @@ -94801,7 +114791,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#47a43b" } }, { @@ -94820,7 +114815,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#6dfc5a" } }, { @@ -94839,7 +114839,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5cd14c" } }, { @@ -94858,7 +114863,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#5fd84f" } }, { @@ -94877,7 +114887,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#a7a7a7" } }, { @@ -94896,7 +114911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#a4a4a4" } }, { @@ -94915,7 +114935,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#929292" } }, { @@ -94934,7 +114959,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#929292" } }, { @@ -94953,7 +114983,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#727272" } }, { @@ -94972,7 +115007,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#b0b0b0" } }, { @@ -94991,7 +115031,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#9e9e9e" } }, { @@ -95010,7 +115055,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#9d9d9d" } }, { @@ -95029,7 +115079,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d7d7d7" } }, { @@ -95048,7 +115103,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#727272" } }, { @@ -95067,7 +115127,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#b0b0b0" } }, { @@ -95086,7 +115151,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#727272" } }, { @@ -95105,7 +115175,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#b0b0b0" } }, { @@ -95124,7 +115199,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#919191" } }, { @@ -95143,7 +115223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#969696" } }, { @@ -95162,7 +115247,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#f38c5e" } }, { @@ -95181,7 +115271,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#ed895c" } }, { @@ -95200,7 +115295,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d47c52" } }, { @@ -95219,7 +115319,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d57c53" } }, { @@ -95238,7 +115343,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#a66040" } }, { @@ -95257,7 +115367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ff9363" } }, { @@ -95276,7 +115391,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#e68559" } }, { @@ -95295,7 +115415,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#e38458" } }, { @@ -95314,7 +115439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ecd8c2" } }, { @@ -95333,7 +115463,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a66040" } }, { @@ -95352,7 +115487,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#ff9363" } }, { @@ -95371,7 +115511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a66040" } }, { @@ -95390,7 +115535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#ff9363" } }, { @@ -95409,7 +115559,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d37c52" } }, { @@ -95428,7 +115583,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#da7f55" } }, { @@ -95447,7 +115607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#ea53b0" } }, { @@ -95466,7 +115631,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#e552ac" } }, { @@ -95485,7 +115655,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ce4a9d" } }, { @@ -95504,7 +115679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cf4b9d" } }, { @@ -95523,7 +115703,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#a03978" } }, { @@ -95542,7 +115727,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#f658b9" } }, { @@ -95561,7 +115751,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#de50a8" } }, { @@ -95580,7 +115775,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dc4fa7" } }, { @@ -95599,7 +115799,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#eacdd2" } }, { @@ -95618,7 +115823,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a03978" } }, { @@ -95637,7 +115847,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#f658b9" } }, { @@ -95656,7 +115871,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a03978" } }, { @@ -95675,7 +115895,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#f658b9" } }, { @@ -95694,7 +115919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cd4a9c" } }, { @@ -95713,7 +115943,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d44ca1" } }, { @@ -95732,7 +115967,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#9a5aea" } }, { @@ -95751,7 +115991,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#9758e5" } }, { @@ -95770,7 +116015,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a4fce" } }, { @@ -95789,7 +116039,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a4fce" } }, { @@ -95808,7 +116063,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#693ea0" } }, { @@ -95827,7 +116087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#a25ff6" } }, { @@ -95846,7 +116111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#9355de" } }, { @@ -95865,7 +116135,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#9255dc" } }, { @@ -95884,7 +116159,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dbcfdd" } }, { @@ -95903,7 +116183,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#693ea0" } }, { @@ -95922,7 +116207,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a25ff6" } }, { @@ -95941,7 +116231,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#693ea0" } }, { @@ -95960,7 +116255,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a25ff6" } }, { @@ -95979,7 +116279,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#894fcd" } }, { @@ -95998,7 +116303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8d51d4" } }, { @@ -96017,7 +116327,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#f35c5c" } }, { @@ -96036,7 +116351,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#ed5a5a" } }, { @@ -96055,7 +116375,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d45050" } }, { @@ -96074,7 +116399,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d55151" } }, { @@ -96093,7 +116423,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#a63f3f" } }, { @@ -96112,7 +116447,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ff6161" } }, { @@ -96131,7 +116471,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#e65757" } }, { @@ -96150,7 +116495,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#e35656" } }, { @@ -96169,7 +116519,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#eccfc2" } }, { @@ -96188,7 +116543,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a63f3f" } }, { @@ -96207,7 +116567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#ff6161" } }, { @@ -96226,7 +116591,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#a63f3f" } }, { @@ -96245,7 +116615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#ff6161" } }, { @@ -96264,7 +116639,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d35050" } }, { @@ -96283,7 +116663,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#da5353" } }, { @@ -96302,7 +116687,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 504, + "height": 115 + }, + "colour": "#e9c25a" } }, { @@ -96321,7 +116711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 76 + }, + "colour": "#e4be58" } }, { @@ -96340,7 +116735,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cdaa4f" } }, { @@ -96359,7 +116759,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ceab4f" } }, { @@ -96378,7 +116783,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#9f853e" } }, { @@ -96397,7 +116807,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#f5cc5f" } }, { @@ -96416,7 +116831,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#ddb855" } }, { @@ -96435,7 +116855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dcb755" } }, { @@ -96454,7 +116879,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#eae2c1" } }, { @@ -96473,7 +116903,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9f853e" } }, { @@ -96492,7 +116927,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#f5cc5f" } }, { @@ -96511,7 +116951,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9f853e" } }, { @@ -96530,7 +116975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#f5cc5f" } }, { @@ -96549,7 +116999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ccaa4f" } }, { @@ -96568,7 +117023,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d3af51" } } ] @@ -96586,6 +117046,7 @@ "asset_type:sprite" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_panels_glossy/README.md", "thumbnail_path": "prefabs/ui_panels_glossy/thumbnail.png", "resources": [ @@ -96606,7 +117067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#1050df" } }, { @@ -96626,7 +117092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#004cd4" } }, { @@ -96646,7 +117117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0740d2" } }, { @@ -96666,7 +117142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0741d2" } }, { @@ -96686,7 +117167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#001870" } }, { @@ -96706,7 +117192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#004efe" } }, { @@ -96726,7 +117217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#0640d5" } }, { @@ -96746,7 +117242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#053ed5" } }, { @@ -96766,7 +117267,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfc8d6" } }, { @@ -96786,7 +117292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#001870" } }, { @@ -96806,7 +117317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#014efe" } }, { @@ -96826,7 +117342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#001870" } }, { @@ -96846,7 +117367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#014efe" } }, { @@ -96866,7 +117392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#053dd1" } }, { @@ -96886,7 +117417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0540d2" } }, { @@ -96906,7 +117442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#10c5dc" } }, { @@ -96926,7 +117467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#00b8d4" } }, { @@ -96946,7 +117492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#06b1d2" } }, { @@ -96966,7 +117517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#06b1d2" } }, { @@ -96986,7 +117542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#004770" } }, { @@ -97006,7 +117567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#00ddfe" } }, { @@ -97026,7 +117592,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#04b4d5" } }, { @@ -97046,7 +117617,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#03b3d5" } }, { @@ -97066,7 +117642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfdbd6" } }, { @@ -97086,7 +117667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#004770" } }, { @@ -97106,7 +117692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#01ddfe" } }, { @@ -97126,7 +117717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#004770" } }, { @@ -97146,7 +117742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#01ddfe" } }, { @@ -97166,7 +117767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#03afd1" } }, { @@ -97186,7 +117792,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#04b1d2" } }, { @@ -97206,7 +117817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#55cf20" } }, { @@ -97226,7 +117842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#4bc516" } }, { @@ -97246,7 +117867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#45ba17" } }, { @@ -97266,7 +117892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#45bb18" } }, { @@ -97286,7 +117917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#0d7000" } }, { @@ -97306,7 +117942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#4fe51b" } }, { @@ -97326,7 +117967,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#46bd16" } }, { @@ -97346,7 +117992,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#45bc15" } }, { @@ -97366,7 +118017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c7e1b3" } }, { @@ -97386,7 +118042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#0d7000" } }, { @@ -97406,7 +118067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#4fe51c" } }, { @@ -97426,7 +118092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#0d7000" } }, { @@ -97446,7 +118117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#4fe51c" } }, { @@ -97466,7 +118142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#43b914" } }, { @@ -97486,7 +118167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#44bb16" } }, { @@ -97506,7 +118192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#919191" } }, { @@ -97526,7 +118217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#848484" } }, { @@ -97546,7 +118242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7b7b7b" } }, { @@ -97566,7 +118267,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7b7b7b" } }, { @@ -97586,7 +118292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#363636" } }, { @@ -97606,7 +118317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#9e9e9e" } }, { @@ -97626,7 +118342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#7c7c7c" } }, { @@ -97646,7 +118367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#7b7b7b" } }, { @@ -97666,7 +118392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bebebe" } }, { @@ -97686,7 +118417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#363636" } }, { @@ -97706,7 +118442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9e9e9e" } }, { @@ -97726,7 +118467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#363636" } }, { @@ -97746,7 +118492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9e9e9e" } }, { @@ -97766,7 +118517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#797979" } }, { @@ -97786,7 +118542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7a7a7a" } }, { @@ -97806,7 +118567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#df560b" } }, { @@ -97826,7 +118592,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#d44f00" } }, { @@ -97846,7 +118617,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d24d06" } }, { @@ -97866,7 +118642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d24d06" } }, { @@ -97886,7 +118667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#702200" } }, { @@ -97906,7 +118692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#fe5a00" } }, { @@ -97926,7 +118717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d64d04" } }, { @@ -97946,7 +118742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d54c03" } }, { @@ -97966,7 +118767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e4cab1" } }, { @@ -97986,7 +118792,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#702200" } }, { @@ -98006,7 +118817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fe5a01" } }, { @@ -98026,7 +118842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#702200" } }, { @@ -98046,7 +118867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fe5a01" } }, { @@ -98066,7 +118892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d14b03" } }, { @@ -98086,7 +118917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d24c04" } }, { @@ -98106,7 +118942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#df10c8" } }, { @@ -98126,7 +118967,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#d40cbc" } }, { @@ -98146,7 +118992,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c906ad" } }, { @@ -98166,7 +119017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ca06ae" } }, { @@ -98186,7 +119042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#720448" } }, { @@ -98206,7 +119067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#fe00e3" } }, { @@ -98226,7 +119092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#cc04b0" } }, { @@ -98246,7 +119117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#cb03ae" } }, { @@ -98266,7 +119142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e4bece" } }, { @@ -98286,7 +119167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#720448" } }, { @@ -98306,7 +119192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fe01e3" } }, { @@ -98326,7 +119217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#720448" } }, { @@ -98346,7 +119242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fe01e3" } }, { @@ -98366,7 +119267,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#c703aa" } }, { @@ -98386,7 +119292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ca04ae" } }, { @@ -98406,7 +119317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#8410df" } }, { @@ -98426,7 +119342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#7c00d4" } }, { @@ -98446,7 +119367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7806ca" } }, { @@ -98466,7 +119392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7806cb" } }, { @@ -98486,7 +119417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#320070" } }, { @@ -98506,7 +119442,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#9600fe" } }, { @@ -98526,7 +119467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#7a04cd" } }, { @@ -98546,7 +119492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#7803cb" } }, { @@ -98566,7 +119517,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2bed6" } }, { @@ -98586,7 +119542,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#320070" } }, { @@ -98606,7 +119567,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9601fe" } }, { @@ -98626,7 +119592,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#320070" } }, { @@ -98646,7 +119617,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#9601fe" } }, { @@ -98666,7 +119642,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7603c8" } }, { @@ -98686,7 +119667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#7804cb" } }, { @@ -98706,7 +119692,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#de0b0b" } }, { @@ -98726,7 +119717,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#d2100f" } }, { @@ -98746,7 +119742,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d00606" } }, { @@ -98766,7 +119767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d00606" } }, { @@ -98786,7 +119792,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#610000" } }, { @@ -98806,7 +119817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#e50000" } }, { @@ -98826,7 +119842,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d30404" } }, { @@ -98846,7 +119867,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d30303" } }, { @@ -98866,7 +119892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e0beb1" } }, { @@ -98886,7 +119917,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#610000" } }, { @@ -98906,7 +119942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#e50101" } }, { @@ -98926,7 +119967,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#610000" } }, { @@ -98946,7 +119992,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#e50101" } }, { @@ -98966,7 +120017,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ce0303" } }, { @@ -98986,7 +120042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ce0707" } }, { @@ -99006,7 +120067,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 514, + "height": 125 + }, + "colour": "#dfbf10" } }, { @@ -99026,7 +120092,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 512, + "height": 86 + }, + "colour": "#d4b300" } }, { @@ -99046,7 +120117,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2a806" } }, { @@ -99066,7 +120142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2a806" } }, { @@ -99086,7 +120167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#705200" } }, { @@ -99106,7 +120192,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#fed200" } }, { @@ -99126,7 +120217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d6aa04" } }, { @@ -99146,7 +120242,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#d5a903" } }, { @@ -99166,7 +120267,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e4dbb1" } }, { @@ -99186,7 +120292,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#705200" } }, { @@ -99206,7 +120317,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fed201" } }, { @@ -99226,7 +120342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#705200" } }, { @@ -99246,7 +120367,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#fed201" } }, { @@ -99266,7 +120392,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d1a603" } }, { @@ -99286,7 +120417,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d2a804" } } ] @@ -99304,6 +120440,7 @@ "asset_type:sprite" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_panels_pixel/README.md", "thumbnail_path": "prefabs/ui_panels_pixel/thumbnail.png", "resources": [ @@ -99324,7 +120461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#035dd2" } }, { @@ -99344,7 +120486,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#0256c5" } }, { @@ -99364,7 +120511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0862d8" } }, { @@ -99384,7 +120536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0863da" } }, { @@ -99404,7 +120561,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#000d50" } }, { @@ -99424,7 +120586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#0057ff" } }, { @@ -99444,7 +120611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#0765de" } }, { @@ -99464,7 +120636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#0663db" } }, { @@ -99484,7 +120661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfcdd8" } }, { @@ -99504,7 +120686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#000d50" } }, { @@ -99524,7 +120711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#0057ff" } }, { @@ -99544,7 +120736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#000d50" } }, { @@ -99564,7 +120761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#0057ff" } }, { @@ -99584,7 +120786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0660d6" } }, { @@ -99604,7 +120811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#0861d6" } }, { @@ -99624,7 +120836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#03c4d2" } }, { @@ -99644,7 +120861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#02b8c5" } }, { @@ -99664,7 +120886,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#08cbd8" } }, { @@ -99684,7 +120911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#08ccda" } }, { @@ -99704,7 +120936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#004d50" } }, { @@ -99724,7 +120961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#00e8ff" } }, { @@ -99744,7 +120986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#07d0de" } }, { @@ -99764,7 +121011,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#06cddb" } }, { @@ -99784,7 +121036,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#bfe3d8" } }, { @@ -99804,7 +121061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#004d50" } }, { @@ -99824,7 +121086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#00e8ff" } }, { @@ -99844,7 +121111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#004d50" } }, { @@ -99864,7 +121136,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#00e8ff" } }, { @@ -99884,7 +121161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#06c8d6" } }, { @@ -99904,7 +121186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#08c8d6" } }, { @@ -99924,7 +121211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#61c61d" } }, { @@ -99944,7 +121236,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#59ba1a" } }, { @@ -99964,7 +121261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#66cd22" } }, { @@ -99984,7 +121286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#67ce22" } }, { @@ -100004,7 +121311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#0d5000" } }, { @@ -100024,7 +121336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#57e71c" } }, { @@ -100044,7 +121361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#69d222" } }, { @@ -100064,7 +121386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#67cf21" } }, { @@ -100084,7 +121411,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#cee3b5" } }, { @@ -100104,7 +121436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#0d5000" } }, { @@ -100124,7 +121461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#57e71c" } }, { @@ -100144,7 +121486,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#0d5000" } }, { @@ -100164,7 +121511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#57e71c" } }, { @@ -100184,7 +121536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#64ca20" } }, { @@ -100204,7 +121561,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#65ca22" } }, { @@ -100224,7 +121586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#717171" } }, { @@ -100244,7 +121611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#6a6a6a" } }, { @@ -100264,7 +121636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#747474" } }, { @@ -100284,7 +121661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#757575" } }, { @@ -100304,7 +121686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#373737" } }, { @@ -100324,7 +121711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#cecece" } }, { @@ -100344,7 +121736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#747474" } }, { @@ -100364,7 +121761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#747474" } }, { @@ -100384,7 +121786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#b5b5b5" } }, { @@ -100404,7 +121811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#373737" } }, { @@ -100424,7 +121836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#cecece" } }, { @@ -100444,7 +121861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#373737" } }, { @@ -100464,7 +121886,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#cecece" } }, { @@ -100484,7 +121911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#717171" } }, { @@ -100504,7 +121936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#737373" } }, { @@ -100524,7 +121961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#d25803" } }, { @@ -100544,7 +121986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#c55302" } }, { @@ -100564,7 +122011,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d85e08" } }, { @@ -100584,7 +122036,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#da5e08" } }, { @@ -100604,7 +122061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#502100" } }, { @@ -100624,7 +122086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ff6c00" } }, { @@ -100644,7 +122111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#de5f07" } }, { @@ -100664,7 +122136,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#db5e06" } }, { @@ -100684,7 +122161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e6ceb1" } }, { @@ -100704,7 +122186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#502100" } }, { @@ -100724,7 +122211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff6c00" } }, { @@ -100744,7 +122236,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#502100" } }, { @@ -100764,7 +122261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff6c00" } }, { @@ -100784,7 +122286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d65c06" } }, { @@ -100804,7 +122311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d65d08" } }, { @@ -100824,7 +122336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#d41dc9" } }, { @@ -100844,7 +122361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#c81bbd" } }, { @@ -100864,7 +122386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#db22cf" } }, { @@ -100884,7 +122411,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dc23d0" } }, { @@ -100904,7 +122436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#64005e" } }, { @@ -100924,7 +122461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ff1aee" } }, { @@ -100944,7 +122486,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#df23d4" } }, { @@ -100964,7 +122511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dd22d1" } }, { @@ -100984,7 +122536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e8c2d8" } }, { @@ -101004,7 +122561,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#64005e" } }, { @@ -101024,7 +122586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff1aee" } }, { @@ -101044,7 +122611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#64005e" } }, { @@ -101064,7 +122636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff1aee" } }, { @@ -101084,7 +122661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d821cc" } }, { @@ -101104,7 +122686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d822cd" } }, { @@ -101124,7 +122711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#8403d2" } }, { @@ -101144,7 +122736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#7c02c5" } }, { @@ -101164,7 +122761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8a08d8" } }, { @@ -101184,7 +122786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8b08da" } }, { @@ -101204,7 +122811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#3a0050" } }, { @@ -101224,7 +122836,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#a000ff" } }, { @@ -101244,7 +122861,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#8d07de" } }, { @@ -101264,7 +122886,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#8b06db" } }, { @@ -101284,7 +122911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d8bed8" } }, { @@ -101304,7 +122936,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#3a0050" } }, { @@ -101324,7 +122961,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#a000ff" } }, { @@ -101344,7 +122986,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#3a0050" } }, { @@ -101364,7 +123011,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#a000ff" } }, { @@ -101384,7 +123036,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8806d6" } }, { @@ -101404,7 +123061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#8908d6" } }, { @@ -101424,7 +123086,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#d42523" } }, { @@ -101444,7 +123111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#c82220" } }, { @@ -101464,7 +123136,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#db2a28" } }, { @@ -101484,7 +123161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dc2b29" } }, { @@ -101504,7 +123186,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#640200" } }, { @@ -101524,7 +123211,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ff322f" } }, { @@ -101544,7 +123236,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#df2b29" } }, { @@ -101564,7 +123261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dd2a28" } }, { @@ -101584,7 +123286,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e8c3b6" } }, { @@ -101604,7 +123311,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#640200" } }, { @@ -101624,7 +123336,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff322f" } }, { @@ -101644,7 +123361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#640200" } }, { @@ -101664,7 +123386,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ff322f" } }, { @@ -101684,7 +123411,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d82926" } }, { @@ -101704,7 +123436,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d82a28" } }, { @@ -101724,7 +123461,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 128 + }, + "colour": "#d2bb03" } }, { @@ -101744,7 +123486,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 516, + "height": 88 + }, + "colour": "#c5af02" } }, { @@ -101764,7 +123511,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d8c108" } }, { @@ -101784,7 +123536,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#dac208" } }, { @@ -101804,7 +123561,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 32 + }, + "colour": "#504400" } }, { @@ -101824,7 +123586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 24 + }, + "colour": "#ffd600" } }, { @@ -101844,7 +123611,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dec607" } }, { @@ -101864,7 +123636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 128 + }, + "colour": "#dbc306" } }, { @@ -101884,7 +123661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#e6e0b1" } }, { @@ -101904,7 +123686,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#504400" } }, { @@ -101924,7 +123711,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ffd600" } }, { @@ -101944,7 +123736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 128 + }, + "colour": "#504400" } }, { @@ -101964,7 +123761,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 24, + "height": 120 + }, + "colour": "#ffd600" } }, { @@ -101984,7 +123786,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d6be06" } }, { @@ -102004,7 +123811,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#d6bf08" } } ] @@ -102020,6 +123832,7 @@ "role:ui" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_horror/README.md", "thumbnail_path": "prefabs/ui_sfx_horror/thumbnail.png", "resources": [ @@ -102585,6 +124398,7 @@ "role:ui" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_metallic/README.md", "thumbnail_path": "prefabs/ui_sfx_metallic/thumbnail.png", "resources": [ @@ -103149,6 +124963,7 @@ "theme:modern" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_military/README.md", "thumbnail_path": "prefabs/ui_sfx_military/thumbnail.png", "resources": [ @@ -103609,6 +125424,7 @@ "role:ui" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_pen_and_paper/README.md", "thumbnail_path": "prefabs/ui_sfx_pen_and_paper/thumbnail.png", "resources": [ @@ -104171,6 +125987,7 @@ "style:retro" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_retro_beeps/README.md", "thumbnail_path": "prefabs/ui_sfx_retro_beeps/thumbnail.png", "resources": [ @@ -104734,6 +126551,7 @@ "audio_type:ui-sound" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_scifi_futuristic/README.md", "thumbnail_path": "prefabs/ui_sfx_scifi_futuristic/thumbnail.png", "resources": [ @@ -105262,6 +127080,7 @@ "audio_type:ui-sound" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_stone/README.md", "thumbnail_path": "prefabs/ui_sfx_stone/thumbnail.png", "resources": [ @@ -105862,6 +127681,7 @@ "role:ui" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/ui_sfx_woodwind/README.md", "thumbnail_path": "prefabs/ui_sfx_woodwind/thumbnail.png", "resources": [ @@ -106424,6 +128244,7 @@ "style:shaded" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/vfx_elemental/README.md", "thumbnail_path": "prefabs/vfx_elemental/thumbnail.png", "resources": [ @@ -106455,7 +128276,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f76a1e" } }, { @@ -106482,7 +128308,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#fbb570" } }, { @@ -106508,7 +128339,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#fcc785" } }, { @@ -106539,7 +128375,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#12c1ed" } }, { @@ -106578,7 +128419,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cac551" } }, { @@ -106609,7 +128455,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#e9e46d" } }, { @@ -106654,7 +128505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#474039" } }, { @@ -106691,7 +128547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#f8ed69" } }, { @@ -106749,7 +128610,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#ff6600" } }, { @@ -106786,7 +128652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4e4744" } }, { @@ -106819,7 +128690,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 115, + "height": 200 + }, + "colour": "#f4ef9d" } }, { @@ -106877,7 +128753,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f5f09f" } }, { @@ -106939,7 +128820,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#f8d08f" } }, { @@ -106986,7 +128872,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#f5d49b" } }, { @@ -107022,7 +128913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 132, + "height": 189 + }, + "colour": "#48423f" } }, { @@ -107047,7 +128943,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 135, + "height": 110 + }, + "colour": "#463e3b" } }, { @@ -107073,7 +128974,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 135, + "height": 110 + }, + "colour": "#00b3e8" } }, { @@ -107113,7 +129019,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 139 + }, + "colour": "#e97b34" } }, { @@ -107153,7 +129064,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 139, + "height": 200 + }, + "colour": "#93a4cc" } }, { @@ -107190,7 +129106,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#7dd643" } }, { @@ -107219,7 +129140,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f9731e" } }, { @@ -107254,7 +129180,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#494240" } }, { @@ -107286,7 +129217,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 135, + "height": 110 + }, + "colour": "#49423f" } }, { @@ -107329,7 +129265,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 200 + }, + "colour": "#3e3936" } }, { @@ -107360,7 +129301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#f4ef9d" } }, { @@ -107418,7 +129364,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#4b413d" } }, { @@ -107448,7 +129399,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#f4ef9d" } }, { @@ -107492,7 +129448,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#e5e5e5" } }, { @@ -107528,7 +129489,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#dfdfdf" } }, { @@ -107560,7 +129526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#48aadd" } }, { @@ -107604,7 +129575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#0fc0ed" } }, { @@ -107639,7 +129615,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#3a9fd5" } }, { @@ -107698,7 +129679,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#00b6ea" } }, { @@ -107723,7 +129709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 155, + "height": 155 + }, + "colour": "#21caef" } }, { @@ -107751,11 +129742,50 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#efea89" } } ] }, + { + "name": "wave", + "version": "", + "display_name": "Waves and difficulty", + "description": "A spawn interval that tightens each wave down to a floor, a wave counter and progress through the current one, and a per-wave batch size so the curve keeps climbing after the interval bottoms out. Deliberately knows nothing about what spawns: the same curve drives enemies, obstacles or cards.", + "category": "library", + "tags": [ + "capability:difficulty", + "capability:waves", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/wave/README.md", + "thumbnail_path": "prefabs/wave/thumbnail.png", + "resources": [ + { + "prefab": "wave", + "kind": "script", + "name": "scr_wave", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "wave", + "kind": "script", + "name": "scr_wave_read", + "description": null, + "tags": [], + "preview": null + } + ] + }, { "name": "windier_woods", "version": "", @@ -107769,6 +129799,7 @@ "style:painted" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/windier_woods/README.md", "thumbnail_path": "prefabs/windier_woods/thumbnail.png", "resources": [ @@ -107806,7 +129837,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 51, + "height": 30 + }, + "colour": "#87a726" } }, { @@ -107828,7 +129864,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 51, + "height": 30 + }, + "colour": "#b8c6db" } }, { @@ -107851,7 +129892,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1579 + }, + "colour": "#869ddd" } }, { @@ -107874,7 +129920,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1507 + }, + "colour": "#455b48" } }, { @@ -107897,7 +129948,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 768, + "height": 384 + }, + "colour": "#404067" } }, { @@ -107920,7 +129976,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#232338" } }, { @@ -107943,7 +130004,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#28293d" } }, { @@ -107966,7 +130032,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1579 + }, + "colour": "#7ca3ea" } }, { @@ -107989,7 +130060,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1507 + }, + "colour": "#556082" } }, { @@ -108011,7 +130087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#27295a" } }, { @@ -108033,7 +130114,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#373c68" } }, { @@ -108055,7 +130141,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#ca9421" } }, { @@ -108077,7 +130168,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#232556" } }, { @@ -108099,7 +130195,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#353a66" } }, { @@ -108121,7 +130222,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#232556" } }, { @@ -108143,7 +130249,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#343965" } }, { @@ -108165,7 +130276,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#232556" } }, { @@ -108187,7 +130303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#343965" } }, { @@ -108209,7 +130330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#232556" } }, { @@ -108231,7 +130357,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#343965" } }, { @@ -108273,7 +130404,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 352, + "height": 252 + }, + "colour": "#2a2c60" } }, { @@ -108315,7 +130451,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 352, + "height": 252 + }, + "colour": "#404776" } }, { @@ -108337,7 +130478,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#7dbced" } }, { @@ -108359,7 +130505,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 55 + }, + "colour": "#62a0dd" } }, { @@ -108381,7 +130532,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 55 + }, + "colour": "#5d9cd9" } }, { @@ -108403,7 +130559,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#7bb9e8" } }, { @@ -108425,7 +130586,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#82c1ee" } }, { @@ -108447,7 +130613,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#80bcea" } }, { @@ -108469,7 +130640,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#7dbded" } }, { @@ -108491,7 +130667,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 55 + }, + "colour": "#64a1dd" } }, { @@ -108513,7 +130694,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 32, + "height": 55 + }, + "colour": "#609dd8" } }, { @@ -108535,7 +130721,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 55 + }, + "colour": "#7cb9e9" } }, { @@ -108557,7 +130748,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 502, + "height": 300 + }, + "colour": "#9d9d9d" } }, { @@ -108579,7 +130775,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 153, + "height": 172 + }, + "colour": "#fefada" } }, { @@ -108601,7 +130802,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 337, + "height": 206 + }, + "colour": "#a49266" } }, { @@ -108623,7 +130829,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 37, + "height": 38 + }, + "colour": "#94a673" } }, { @@ -108645,7 +130856,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 38, + "height": 34 + }, + "colour": "#babf59" } }, { @@ -108667,7 +130883,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 41, + "height": 40 + }, + "colour": "#a8943d" } }, { @@ -108689,7 +130910,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#1a97ee" } }, { @@ -108711,7 +130937,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 128 + }, + "colour": "#53bdf5" } }, { @@ -108733,7 +130964,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 22, + "height": 15 + }, + "colour": "#91b907" } }, { @@ -108755,7 +130991,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 21, + "height": 13 + }, + "colour": "#9bbf0d" } }, { @@ -108777,7 +131018,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 15 + }, + "colour": "#88b105" } }, { @@ -108799,7 +131045,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1536, + "height": 1152 + }, + "colour": "#252837" } }, { @@ -108821,7 +131072,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1536, + "height": 1152 + }, + "colour": "#3c445d" } }, { @@ -108843,7 +131099,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 70 + }, + "colour": "#b4daf1" } }, { @@ -108865,7 +131126,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 57, + "height": 70 + }, + "colour": "#b6ddf3" } }, { @@ -108887,7 +131153,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 50 + }, + "colour": "#9d9d9d" } }, { @@ -108909,7 +131180,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 145, + "height": 43 + }, + "colour": "#3f7f15" } }, { @@ -108931,7 +131207,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 110, + "height": 28 + }, + "colour": "#cadef2" } }, { @@ -108953,7 +131234,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 48 + }, + "colour": "#c8dff3" } }, { @@ -108975,7 +131261,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 36 + }, + "colour": "#cce1f4" } }, { @@ -108997,7 +131288,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 256 + }, + "colour": "#9fa0b0" } }, { @@ -109019,7 +131315,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 384 + }, + "colour": "#38333f" } }, { @@ -109041,7 +131342,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 869, + "height": 906 + }, + "colour": "#4e7433" } }, { @@ -109063,7 +131369,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 844, + "height": 1021 + }, + "colour": "#646e0a" } }, { @@ -109085,7 +131396,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 844, + "height": 1021 + }, + "colour": "#b27718" } }, { @@ -109107,7 +131423,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 725, + "height": 900 + }, + "colour": "#3e7219" } }, { @@ -109129,7 +131450,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 869, + "height": 906 + }, + "colour": "#698283" } }, { @@ -109151,7 +131477,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 853, + "height": 1056 + }, + "colour": "#5a8d8e" } }, { @@ -109173,7 +131504,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 725, + "height": 900 + }, + "colour": "#62888e" } }, { @@ -109216,6 +131552,7 @@ "state:static" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/world_backgrounds/README.md", "thumbnail_path": "prefabs/world_backgrounds/thumbnail.png", "resources": [ @@ -109238,7 +131575,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#793637" } }, { @@ -109260,7 +131602,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#c4a7ac" } }, { @@ -109282,7 +131629,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#f7b9b5" } }, { @@ -109304,7 +131656,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#892d2a" } }, { @@ -109326,7 +131683,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#e6d0b6" } }, { @@ -109348,7 +131710,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#d48720" } }, { @@ -109370,7 +131737,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#b68870" } }, { @@ -109392,7 +131764,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#f48215" } }, { @@ -109414,7 +131791,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#976c5e" } }, { @@ -109436,7 +131818,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#ce4025" } }, { @@ -109458,7 +131845,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#ad723c" } }, { @@ -109480,7 +131872,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#3e1a24" } }, { @@ -109502,7 +131899,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#381b19" } }, { @@ -109524,7 +131926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#8b3c46" } }, { @@ -109546,7 +131953,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#462227" } }, { @@ -109568,7 +131980,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#473437" } }, { @@ -109590,7 +132007,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#987154" } }, { @@ -109612,7 +132034,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#2a2746" } }, { @@ -109634,7 +132061,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#313d8f" } }, { @@ -109656,7 +132088,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#5472a1" } }, { @@ -109678,7 +132115,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#20262f" } }, { @@ -109700,7 +132142,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#b7a1c8" } }, { @@ -109722,7 +132169,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#303662" } }, { @@ -109744,7 +132196,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#496d57" } }, { @@ -109766,7 +132223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#1c282f" } }, { @@ -109788,7 +132250,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#101622" } }, { @@ -109810,7 +132277,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#152c20" } }, { @@ -109832,7 +132304,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#2e3448" } }, { @@ -109854,7 +132331,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#313b55" } }, { @@ -109876,7 +132358,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#b0929f" } }, { @@ -109898,7 +132385,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#3e3168" } }, { @@ -109920,7 +132412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#2e394b" } }, { @@ -109942,7 +132439,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#1c0f2a" } }, { @@ -109964,7 +132466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#301d2c" } }, { @@ -109986,7 +132493,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#132e29" } }, { @@ -110008,7 +132520,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#a1c4d3" } }, { @@ -110030,7 +132547,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#b0c2fc" } }, { @@ -110052,7 +132574,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#f1f7f8" } }, { @@ -110074,7 +132601,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#759bbe" } }, { @@ -110096,7 +132628,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#acc3d9" } }, { @@ -110118,7 +132655,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#b6bbc3" } }, { @@ -110140,7 +132682,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#5f85a4" } }, { @@ -110162,7 +132709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#84a9c4" } }, { @@ -110184,7 +132736,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#d8f3ff" } }, { @@ -110206,7 +132763,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#7e9cc1" } }, { @@ -110228,7 +132790,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#d7d9de" } }, { @@ -110250,7 +132817,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#ccebbf" } }, { @@ -110272,7 +132844,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#6485bb" } }, { @@ -110294,7 +132871,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#6a94ca" } }, { @@ -110316,7 +132898,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#97d0e2" } }, { @@ -110338,7 +132925,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#97cdd5" } }, { @@ -110360,7 +132952,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#36294f" } }, { @@ -110382,7 +132979,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#5d0351" } }, { @@ -110404,7 +133006,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#1e4771" } }, { @@ -110426,7 +133033,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#57065b" } }, { @@ -110448,7 +133060,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#121e35" } }, { @@ -110470,7 +133087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#09083e" } }, { @@ -110492,7 +133114,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#716182" } }, { @@ -110514,7 +133141,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#188ea4" } }, { @@ -110536,7 +133168,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#4b7493" } }, { @@ -110558,7 +133195,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#121643" } }, { @@ -110580,7 +133222,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#30202d" } }, { @@ -110602,7 +133249,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#32344e" } }, { @@ -110624,7 +133276,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#9ccde4" } }, { @@ -110646,7 +133303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#53183a" } }, { @@ -110668,7 +133330,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#3c435f" } }, { @@ -110690,7 +133357,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#1998b3" } }, { @@ -110712,7 +133384,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#05dbf1" } }, { @@ -110734,7 +133411,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#1e75a6" } }, { @@ -110756,7 +133438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#0090ba" } }, { @@ -110778,7 +133465,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#368da2" } }, { @@ -110800,7 +133492,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#006bb3" } }, { @@ -110822,7 +133519,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#297f91" } }, { @@ -110844,7 +133546,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#0063b3" } }, { @@ -110866,7 +133573,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 244, + "height": 155 + }, + "colour": "#fa7551" } }, { @@ -110888,7 +133600,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#0b547c" } }, { @@ -110910,7 +133627,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 309, + "height": 176 + }, + "colour": "#aaa56d" } }, { @@ -110932,7 +133654,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 480, + "height": 270 + }, + "colour": "#425969" } }, { @@ -110954,7 +133681,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1129, + "height": 455 + }, + "colour": "#6d8689" } }, { @@ -110976,7 +133708,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 669, + "height": 280 + }, + "colour": "#50666b" } }, { @@ -110998,7 +133735,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#1b505b" } }, { @@ -111020,7 +133762,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#43323c" } }, { @@ -111042,7 +133789,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#3f6470" } }, { @@ -111064,7 +133816,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 1920, + "height": 1080 + }, + "colour": "#2a5f73" } } ] @@ -111082,6 +133839,7 @@ "style:painted" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/world_hexmaps/README.md", "thumbnail_path": "prefabs/world_hexmaps/thumbnail.png", "resources": [ @@ -111105,7 +133863,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5e983b" } }, { @@ -111128,7 +133891,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#639841" } }, { @@ -111151,7 +133919,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5d943c" } }, { @@ -111174,7 +133947,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#568f3e" } }, { @@ -111197,7 +133975,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#608a3a" } }, { @@ -111219,7 +134002,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 212, + "height": 148 + }, + "colour": "#6d634c" } }, { @@ -111242,7 +134030,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 34, + "height": 35 + }, + "colour": "#b2af9f" } }, { @@ -111265,7 +134058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a37d27" } }, { @@ -111287,7 +134085,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 45 + }, + "colour": "#6a7033" } }, { @@ -111309,7 +134112,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 90, + "height": 118 + }, + "colour": "#54902c" } }, { @@ -111331,7 +134139,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 90, + "height": 118 + }, + "colour": "#3a9365" } }, { @@ -111353,7 +134166,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#716352" } }, { @@ -111375,7 +134193,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#bc5b0d" } }, { @@ -111397,7 +134220,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d39409" } }, { @@ -111419,7 +134247,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b0bbbd" } }, { @@ -111441,7 +134274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b05a49" } }, { @@ -111463,7 +134301,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#707372" } }, { @@ -111485,7 +134328,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#7b5525" } }, { @@ -111508,7 +134356,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b2601a" } }, { @@ -111531,7 +134384,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#d79408" } }, { @@ -111554,7 +134412,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#8b897a" } }, { @@ -111577,7 +134440,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#94a0a0" } }, { @@ -111599,7 +134467,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b2a47d" } }, { @@ -111622,7 +134495,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#b79b5f" } }, { @@ -111645,7 +134523,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 200, + "height": 200 + }, + "colour": "#cba046" } }, { @@ -111668,7 +134551,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2897d5" } }, { @@ -111691,7 +134579,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2a97d4" } }, { @@ -111714,7 +134607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#248bce" } }, { @@ -111738,7 +134636,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#56984e" } }, { @@ -111762,7 +134665,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#569650" } }, { @@ -111786,7 +134694,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#56984d" } }, { @@ -111810,7 +134723,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#559651" } }, { @@ -111834,7 +134752,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#569650" } }, { @@ -111858,7 +134781,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#4e9564" } }, { @@ -111882,7 +134810,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#56954f" } }, { @@ -111906,7 +134839,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5a9747" } }, { @@ -111930,7 +134868,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#5a9746" } }, { @@ -111954,7 +134897,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#599849" } }, { @@ -111978,7 +134926,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#569550" } }, { @@ -112002,7 +134955,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#599749" } }, { @@ -112026,7 +134984,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#869290" } }, { @@ -112050,7 +135013,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#829191" } }, { @@ -112074,7 +135042,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#869290" } }, { @@ -112098,7 +135071,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#829192" } }, { @@ -112122,7 +135100,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#829192" } }, { @@ -112146,7 +135129,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#74919b" } }, { @@ -112170,7 +135158,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#839191" } }, { @@ -112194,7 +135187,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8a928d" } }, { @@ -112218,7 +135216,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8a928d" } }, { @@ -112242,7 +135245,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#88928e" } }, { @@ -112266,7 +135274,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#829191" } }, { @@ -112290,7 +135303,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#89928e" } }, { @@ -112314,7 +135332,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9bd58" } }, { @@ -112338,7 +135361,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c2ba5b" } }, { @@ -112362,7 +135390,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9bd57" } }, { @@ -112386,7 +135419,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c2b95b" } }, { @@ -112410,7 +135448,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c3ba5a" } }, { @@ -112434,7 +135477,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#abb36c" } }, { @@ -112458,7 +135506,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c3b95a" } }, { @@ -112482,7 +135535,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cfbd51" } }, { @@ -112506,7 +135564,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cfbd51" } }, { @@ -112530,7 +135593,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cdbe54" } }, { @@ -112554,7 +135622,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c2b95a" } }, { @@ -112578,7 +135651,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cdbd53" } }, { @@ -112602,7 +135680,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c8dce6" } }, { @@ -112626,7 +135709,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c0d8e3" } }, { @@ -112650,7 +135738,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c9dce6" } }, { @@ -112674,7 +135767,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c1d8e3" } }, { @@ -112698,7 +135796,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c2d9e4" } }, { @@ -112722,7 +135825,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#aacde1" } }, { @@ -112746,7 +135854,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c2d8e4" } }, { @@ -112770,7 +135883,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cedee6" } }, { @@ -112794,7 +135912,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cedee6" } }, { @@ -112818,7 +135941,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#ccdee6" } }, { @@ -112842,7 +135970,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c1d7e3" } }, { @@ -112866,7 +135999,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#cddee6" } }, { @@ -112889,7 +136027,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2890d0" } }, { @@ -112912,7 +136055,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2b9bda" } }, { @@ -112935,7 +136083,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2892d3" } }, { @@ -112958,7 +136111,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#278fd1" } }, { @@ -112981,7 +136139,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2998d9" } }, { @@ -113004,7 +136167,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2997d9" } }, { @@ -113027,7 +136195,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2a9edc" } }, { @@ -113050,7 +136223,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#268dce" } }, { @@ -113073,7 +136251,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#258ed2" } }, { @@ -113096,7 +136279,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#939287" } }, { @@ -113119,7 +136307,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#908e83" } }, { @@ -113142,7 +136335,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8c8a7e" } }, { @@ -113165,7 +136363,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8c8a7c" } }, { @@ -113188,7 +136391,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#8d8c80" } }, { @@ -113210,7 +136418,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 280, + "height": 197 + }, + "colour": "#858475" } }, { @@ -113232,7 +136445,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 118, + "height": 76 + }, + "colour": "#7b7965" } }, { @@ -113254,7 +136472,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 71, + "height": 39 + }, + "colour": "#828172" } }, { @@ -113276,7 +136499,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 121, + "height": 97 + }, + "colour": "#8f8e7f" } }, { @@ -113298,7 +136526,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 118, + "height": 57 + }, + "colour": "#929080" } }, { @@ -113320,7 +136553,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 217, + "height": 181 + }, + "colour": "#858473" } }, { @@ -113342,7 +136580,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 281, + "height": 294 + }, + "colour": "#8e8b79" } }, { @@ -113364,7 +136607,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 79, + "height": 48 + }, + "colour": "#929080" } }, { @@ -113386,7 +136634,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 60, + "height": 44 + }, + "colour": "#919080" } }, { @@ -113408,7 +136661,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 92, + "height": 75 + }, + "colour": "#8d8c7e" } }, { @@ -113430,7 +136688,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 37, + "height": 22 + }, + "colour": "#7e7d6e" } }, { @@ -113452,7 +136715,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 43, + "height": 31 + }, + "colour": "#858475" } }, { @@ -113475,7 +136743,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#939289" } }, { @@ -113498,7 +136771,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dfc247" } }, { @@ -113521,7 +136799,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c7b63f" } }, { @@ -113544,7 +136827,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#b4b157" } }, { @@ -113567,7 +136855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#a9a634" } }, { @@ -113590,7 +136883,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bfac43" } }, { @@ -113613,7 +136911,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 82, + "height": 66 + }, + "colour": "#3f8719" } }, { @@ -113635,7 +136938,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 258, + "height": 165 + }, + "colour": "#a39754" } }, { @@ -113658,7 +136966,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 257, + "height": 210 + }, + "colour": "#89b7a8" } }, { @@ -113680,7 +136993,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 128, + "height": 125 + }, + "colour": "#3e7912" } }, { @@ -113703,7 +137021,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c7ae38" } }, { @@ -113726,7 +137049,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#dee6e8" } }, { @@ -113749,7 +137077,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d3ddde" } }, { @@ -113772,7 +137105,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#bbccc9" } }, { @@ -113795,7 +137133,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#9bb5ad" } }, { @@ -113818,7 +137161,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#d1dcdf" } }, { @@ -113840,7 +137188,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 199, + "height": 138 + }, + "colour": "#b6c6c9" } }, { @@ -113862,7 +137215,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 39, + "height": 24 + }, + "colour": "#cddcde" } }, { @@ -113885,7 +137243,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 62, + "height": 41 + }, + "colour": "#bbbfc1" } }, { @@ -113908,7 +137271,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#c7dde1" } }, { @@ -113930,7 +137298,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 95, + "height": 117 + }, + "colour": "#6e9380" } }, { @@ -113953,7 +137326,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#1481c3" } }, { @@ -113976,7 +137354,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#1d8dcd" } }, { @@ -113999,7 +137382,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#2690ce" } }, { @@ -114022,7 +137410,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#1477ba" } }, { @@ -114045,7 +137438,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 500, + "height": 500 + }, + "colour": "#0c78be" } }, { @@ -114068,7 +137466,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 91 + }, + "colour": "#2dc4f5" } }, { @@ -114091,7 +137494,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 91 + }, + "colour": "#2dc4f5" } }, { @@ -114114,7 +137522,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 36, + "height": 60 + }, + "colour": "#217fbd" } }, { @@ -114137,7 +137550,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 36, + "height": 59 + }, + "colour": "#217fbd" } }, { @@ -114160,7 +137578,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 91 + }, + "colour": "#0b60b4" } }, { @@ -114183,7 +137606,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 19, + "height": 91 + }, + "colour": "#0b60b4" } } ] @@ -114199,6 +137627,7 @@ "role:terrain" ], "asset_bundle": true, + "install": "namespace", "readme_path": "prefabs/world_tilesets/README.md", "thumbnail_path": "prefabs/world_tilesets/thumbnail.png", "resources": [ @@ -114223,7 +137652,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 1536 + }, + "colour": "#543e40" } }, { @@ -114247,7 +137681,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 1536 + }, + "colour": "#373b53" } }, { @@ -114271,7 +137710,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 1536 + }, + "colour": "#40535f" } }, { @@ -114295,7 +137739,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 1536 + }, + "colour": "#252536" } }, { @@ -114319,7 +137768,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2048, + "height": 1536 + }, + "colour": "#38415b" } }, { @@ -114343,7 +137797,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1536 + }, + "colour": "#7c5031" } }, { @@ -114367,7 +137826,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1536 + }, + "colour": "#3e454e" } }, { @@ -114391,7 +137855,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1536 + }, + "colour": "#707c89" } }, { @@ -114415,7 +137884,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1536 + }, + "colour": "#2d2c36" } }, { @@ -114439,7 +137913,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 2304, + "height": 1536 + }, + "colour": "#3d738b" } }, { @@ -114463,7 +137942,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 192 + }, + "colour": "#3a1a31" } }, { @@ -114487,7 +137971,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 192 + }, + "colour": "#533831" } }, { @@ -114511,7 +138000,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 192 + }, + "colour": "#52add1" } }, { @@ -114535,7 +138029,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 192 + }, + "colour": "#2e3f39" } }, { @@ -114559,7 +138058,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 256, + "height": 192 + }, + "colour": "#249ca2" } }, { @@ -114583,7 +138087,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 288, + "height": 192 + }, + "colour": "#58332d" } }, { @@ -114607,7 +138116,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 288, + "height": 192 + }, + "colour": "#3d6836" } }, { @@ -114631,7 +138145,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 288, + "height": 192 + }, + "colour": "#99c0db" } }, { @@ -114655,7 +138174,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 288, + "height": 192 + }, + "colour": "#401348" } }, { @@ -114679,7 +138203,12 @@ "speed": { "unit": "frames_per_second", "value": 30.0 - } + }, + "size": { + "width": 288, + "height": 192 + }, + "colour": "#28a2a0" } }, { @@ -114963,6 +138492,48 @@ "preview": null } ] + }, + { + "name": "zone", + "version": "", + "display_name": "Trigger regions", + "description": "Rectangular and circular regions with enter/exit/inside/outside tracking, so a trigger fires on the single frame the answer changed rather than every frame the player stands in it. Plus distance to the edge, for a prompt that fades in on approach, and a reset so a player who died inside a trigger can set it off again.", + "category": "library", + "tags": [ + "capability:triggers", + "capability:regions", + "role:library" + ], + "asset_bundle": false, + "install": "namespace", + "readme_path": "prefabs/zone/README.md", + "thumbnail_path": "prefabs/zone/thumbnail.png", + "resources": [ + { + "prefab": "zone", + "kind": "script", + "name": "scr_zone", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "zone", + "kind": "script", + "name": "scr_zone_draw", + "description": null, + "tags": [], + "preview": null + }, + { + "prefab": "zone", + "kind": "script", + "name": "scr_zone_track", + "description": null, + "tags": [], + "preview": null + } + ] } ] } diff --git a/prefabs/boss/LICENSE b/prefabs/boss/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/boss/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/boss/README.md b/prefabs/boss/README.md new file mode 100644 index 00000000..2dcb5b49 --- /dev/null +++ b/prefabs/boss/README.md @@ -0,0 +1,3 @@ +# boss + +Stages, and attacks the player can see coming. diff --git a/prefabs/boss/objects/obj_demo/Alarm_0.gml b/prefabs/boss/objects/obj_demo/Alarm_0.gml new file mode 100644 index 00000000..da7a4994 --- /dev/null +++ b/prefabs/boss/objects/obj_demo/Alarm_0.gml @@ -0,0 +1 @@ +fight.invulnerable = false; diff --git a/prefabs/boss/objects/obj_demo/Create.gml b/prefabs/boss/objects/obj_demo/Create.gml new file mode 100644 index 00000000..0613d196 --- /dev/null +++ b/prefabs/boss/objects/obj_demo/Create.gml @@ -0,0 +1,10 @@ +// Three stages down one health bar, with the harder moves gated behind +// the later ones. +fight = boss_make(300, 3); +moves = [ + { move: "sweep", phase: 1 }, + { move: "volley", phase: 2 }, + { move: "slam", phase: 3 }, +]; +log_lines = []; +shots = []; diff --git a/prefabs/boss/objects/obj_demo/Draw.gml b/prefabs/boss/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..b1343736 --- /dev/null +++ b/prefabs/boss/objects/obj_demo/Draw.gml @@ -0,0 +1,17 @@ +// The telegraph made visible: a marker that grows through the wind-up, so +// the dodge can be timed rather than guessed. +if (boss_telegraphing(fight)) { + var _t = boss_telegraph_fraction(fight); + draw_set_alpha(0.3 + _t * 0.7); + draw_set_colour(c_red); + draw_circle(480, 200, 40 + _t * 150, true); + draw_set_alpha(1); +} +draw_set_colour(boss_attacking(fight) ? c_orange : c_white); +draw_circle(480, 200, 40, false); + +draw_set_colour(c_yellow); +for (var i = 0; i < array_length(shots); i++) { + draw_circle(shots[i].x, shots[i].y, 6, false); +} +draw_set_colour(c_white); diff --git a/prefabs/boss/objects/obj_demo/Draw_GUI.gml b/prefabs/boss/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..c21b74f8 --- /dev/null +++ b/prefabs/boss/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,16 @@ +draw_text(40, 30, "boss demo — SPACE damages, ENTER restarts"); +draw_text(40, 55, "phase " + string(fight.phase) + " of " + string(fight.phases) + + (fight.invulnerable ? " (transition — invulnerable)" : "")); + +draw_rectangle(40, 85, 40 + 600 * boss_fraction(fight), 105, false); +draw_rectangle(40, 85, 640, 105, true); + +draw_text(40, 125, boss_telegraphing(fight) + ? "winding up " + boss_move(fight) + + " " + string_format(boss_telegraph_fraction(fight) * 100, 3, 0) + "%" + : (boss_attacking(fight) ? boss_move(fight) + " active" + : (boss_dead(fight) ? "defeated" : "recovering — your turn"))); + +for (var i = 0; i < array_length(log_lines); i++) { + draw_text(700, 160 + i * 20, log_lines[i]); +} diff --git a/prefabs/boss/objects/obj_demo/Step.gml b/prefabs/boss/objects/obj_demo/Step.gml new file mode 100644 index 00000000..7d89d647 --- /dev/null +++ b/prefabs/boss/objects/obj_demo/Step.gml @@ -0,0 +1,37 @@ +// One event per frame, so each of these drives exactly one thing. +var _ev = boss_step(fight); +switch (_ev) { + case "phase": + // Invulnerable through the transition, or a big hit skips a stage. + fight.invulnerable = true; + alarm[0] = 45; + array_push(log_lines, "phase " + string(fight.phase)); + break; + case "telegraph_end": + // The wind-up is over: this is where the attack actually fires. + array_push(log_lines, boss_move(fight) + " fires"); + repeat (fight.phase * 2) { + array_push(shots, { x: 480, y: 200, dir: irandom(359), life: 1.4 }); + } + break; + case "attack_end": + array_push(log_lines, "recovering"); + break; +} +while (array_length(log_lines) > 7) array_delete(log_lines, 0, 1); + +if (boss_ready(fight)) { + var _pick = boss_pick(fight, moves); + if (_pick != "") boss_begin(fight, _pick, 0.9, 0.5, 1.1); +} + +for (var i = array_length(shots) - 1; i >= 0; i--) { + var _s = shots[i]; + _s.x += lengthdir_x(5, _s.dir); + _s.y += lengthdir_y(5, _s.dir); + _s.life -= delta_time / 1000000; + if (_s.life <= 0) array_delete(shots, i, 1); +} + +if (keyboard_check_pressed(vk_space)) boss_hurt(fight, 25); +if (keyboard_check_pressed(vk_enter)) { fight = boss_make(300, 3); shots = []; } diff --git a/prefabs/boss/objects/obj_demo/object.toml b/prefabs/boss/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/boss/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/boss/prefab.toml b/prefabs/boss/prefab.toml new file mode 100644 index 00000000..3a2137ae --- /dev/null +++ b/prefabs/boss/prefab.toml @@ -0,0 +1,17 @@ +schema_version = 1 +name = "Boss fights" + +# A library: installed under `prefabs//` in the target project and +# called as `::boss::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Health split into stages, attacks gated by stage so a later one escalates rather than repeating, and a wind-up/active/recover cycle for every move. The telegraph is the whole design: an attack that fires the instant it is chosen is not difficult but unfair, and the fight reads as random. `boss_step` returns one event per frame rather than setting several flags, because a flag left set is how an attack fires twice." +category = "library" +tags = ["capability:boss", "capability:combat", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "Make the boss invulnerable while a phase transition plays, or a big hit at the wrong moment skips a stage.", + "The recovery window is the player's turn — a boss with none is a boss you cannot approach.", +] diff --git a/prefabs/boss/project.toml b/prefabs/boss/project.toml new file mode 100644 index 00000000..0b4199e9 --- /dev/null +++ b/prefabs/boss/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "boss" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/boss/rooms/rm_demo/layers/Background.toml b/prefabs/boss/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/boss/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/boss/rooms/rm_demo/layers/Instances.toml b/prefabs/boss/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/boss/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/boss/rooms/rm_demo/room.toml b/prefabs/boss/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/boss/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/boss/scripts/scr_boss/scr_boss.gml b/prefabs/boss/scripts/scr_boss/scr_boss.gml new file mode 100644 index 00000000..8e46c138 --- /dev/null +++ b/prefabs/boss/scripts/scr_boss/scr_boss.gml @@ -0,0 +1,86 @@ +/// A fight with stages, and attacks you can see coming. +/// +/// The telegraph is the whole thing. An attack that fires the instant it is +/// chosen is not difficult, it is unfair — the player loses to something +/// they had no frame in which to react to, and the fight reads as random. +/// Every attack here has a wind-up the player can see and a window in which +/// to move. + +/// @function boss_make(hp, phase_count) +/// @description Boss state with `hp` and `phase_count` stages, split evenly +/// down the health bar. Starts idle in phase 1. +function boss_make(hp, phase_count) { + return { + hp: hp, + max: max(1, hp), + phases: max(1, phase_count), + phase: 1, + changed: false, + move: "", + telegraph: 0, + telegraph_span: 0, + active: 0, + cooldown: 0, + invulnerable: false, + }; +} + +/// @function boss_hurt(state, amount) +/// @description Take damage unless the boss is invulnerable — which it +/// should be while a phase transition plays, or the player skips a stage by +/// landing a big hit at the wrong moment. +/// +/// Returns whether the hit landed. +function boss_hurt(state, amount) { + if (state.invulnerable || boss_dead(state)) return false; + state.hp = max(0, state.hp - amount); + return true; +} + +/// @function boss_dead(state) +function boss_dead(state) { + return state.hp <= 0; +} + +/// @function boss_fraction(state) +/// @description Health as 0..1, for the bar across the top. +function boss_fraction(state) { + return state.hp / state.max; +} + +/// @function boss_step(state) +/// @description Advance timers and phase. Call once a frame. +/// +/// Returns what happened this frame: "telegraph_end" when the wind-up +/// finishes and the attack should actually fire, "attack_end" when it +/// finishes, "phase" when the boss enters a new stage, or "" for nothing. +/// One return rather than several flags, because each of these should drive +/// exactly one thing and a flag left set is how an attack fires twice. +function boss_step(state) { + var _dt = delta_time / 1000000; + + var _want = boss_phase_for(state, boss_fraction(state)); + if (_want != state.phase) { + state.phase = _want; + state.changed = true; + return "phase"; + } + state.changed = false; + + if (state.telegraph > 0) { + state.telegraph = max(0, state.telegraph - _dt); + if (state.telegraph <= 0) return "telegraph_end"; + return ""; + } + if (state.active > 0) { + state.active = max(0, state.active - _dt); + if (state.active <= 0) { + state.move = ""; + return "attack_end"; + } + return ""; + } + if (state.cooldown > 0) state.cooldown = max(0, state.cooldown - _dt); + return ""; +} + diff --git a/prefabs/boss/scripts/scr_boss_move/scr_boss_move.gml b/prefabs/boss/scripts/scr_boss_move/scr_boss_move.gml new file mode 100644 index 00000000..190f1a30 --- /dev/null +++ b/prefabs/boss/scripts/scr_boss_move/scr_boss_move.gml @@ -0,0 +1,21 @@ +/// @function boss_move(state) +/// @description Which attack is running, or "" for none. +function boss_move(state) { + return state.move; +} + +/// @function boss_pick(state, moves) +/// @description Choose an attack for the current phase from an array of +/// `{ move, phase }` structs, where `phase` is the earliest stage it can +/// appear in. Returns "" when nothing is available. +/// +/// Gating by phase rather than picking freely is what makes a later stage +/// feel like an escalation rather than the same fight with more health. +function boss_pick(state, moves) { + var _ok = []; + for (var i = 0; i < array_length(moves); i++) { + if (moves[i].phase <= state.phase) array_push(_ok, moves[i].move); + } + if (array_length(_ok) == 0) return ""; + return _ok[irandom(array_length(_ok) - 1)]; +} diff --git a/prefabs/boss/scripts/scr_boss_phase/scr_boss_phase.gml b/prefabs/boss/scripts/scr_boss_phase/scr_boss_phase.gml new file mode 100644 index 00000000..0a25030e --- /dev/null +++ b/prefabs/boss/scripts/scr_boss_phase/scr_boss_phase.gml @@ -0,0 +1,51 @@ +/// @function boss_phase_for(state, fraction) +/// @description Which stage a health fraction falls in, counting from 1. +function boss_phase_for(state, fraction) { + var _p = state.phases - floor(clamp(fraction, 0, 0.999) * state.phases); + return clamp(_p, 1, state.phases); +} + +/// @function boss_ready(state) +/// @description Whether the boss is free to start something — not already +/// winding up, attacking, or recovering. +function boss_ready(state) { + return state.telegraph <= 0 && state.active <= 0 && state.cooldown <= 0 + && !boss_dead(state); +} + +/// @function boss_begin(state, move, telegraph_seconds, active_seconds, recover_seconds) +/// @description Start an attack: wind up for `telegraph_seconds`, then +/// `boss_step` returns "telegraph_end" and you fire it, then it runs for +/// `active_seconds`, then the boss recovers for `recover_seconds`. +/// +/// The recovery is the player's turn. A boss with none is a boss you cannot +/// approach. +function boss_begin(state, move, telegraph_seconds, active_seconds, recover_seconds) { + if (!boss_ready(state)) return false; + state.move = move; + state.telegraph = telegraph_seconds; + state.telegraph_span = max(0.0001, telegraph_seconds); + state.active = active_seconds; + state.cooldown = recover_seconds; + return true; +} + +/// @function boss_telegraphing(state) +function boss_telegraphing(state) { + return state.telegraph > 0; +} + +/// @function boss_telegraph_fraction(state) +/// @description How far through the wind-up, 0..1. Drive a flash, a growing +/// marker or a sound pitch off this so the player can time the dodge rather +/// than guess it. +function boss_telegraph_fraction(state) { + if (state.telegraph <= 0) return 0; + return 1 - state.telegraph / state.telegraph_span; +} + +/// @function boss_attacking(state) +function boss_attacking(state) { + return state.active > 0; +} + diff --git a/prefabs/boss/thumbnail.png b/prefabs/boss/thumbnail.png new file mode 100644 index 00000000..30919abc Binary files /dev/null and b/prefabs/boss/thumbnail.png differ diff --git a/prefabs/breakout/LICENSE b/prefabs/breakout/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/breakout/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/breakout/README.md b/prefabs/breakout/README.md new file mode 100644 index 00000000..8c3f0d9d --- /dev/null +++ b/prefabs/breakout/README.md @@ -0,0 +1,50 @@ +# breakout + +Clear the bricks, do not drop the ball — and steer it with the paddle. + +## The paddle is not a wall + +`steer` is the number that decides whether the game has any skill in it. The +ball's bounce off the paddle is set by *where on the paddle it lands*, not by +the angle it arrived at: + +```gml +var _offset = (x - _pad.x) / (_pad.sprite_width / 2); +direction = 90 + clamp(_offset, -1, 1) * steer; +``` + +At `steer = 0` the paddle reflects like a wall, every rally repeats, and the +player has no way to aim at the last brick in the corner. That one number is +the difference between a toy and a game. + +## The bricks are placed entries + +The wall lives in `rooms/rm_play/layers/Instances.toml` as real entries, so a +brick can be dragged in a visual editor, and that layer is the only copy of the +wall. Edit it there — there is no map file beside it to keep in step. + +A brick is an instance rather than a tile because the game destroys it and +counts what is left: `instance_number(obj_brick) == 0` is how a cleared board +is detected, and a tile layer cannot answer that. Terrain that only has to be +collided against belongs in a tile layer; a thing the game counts, scores or +removes is an object. + +The room stays inspectable either way — a level built by a spawn loop at run +time is neither readable nor editable. + +## Walls are bounds, not objects + +The room edge is not an instance, so the ball tests its own position against +`room_width`/`room_height` rather than colliding. The direction check matters: + +```gml +if (x < 8 && lengthdir_x(1, direction) < 0) direction = 180 - direction; +``` + +Without the second half, a ball that is *already* inside the margin flips +every frame and vibrates along the edge instead of leaving it. + +## What it does not do + +No power-ups, no multi-ball, no second level — clearing the bricks ends the +run. `powerup` is the prefab that fits it best if you want falling capsules. diff --git a/prefabs/breakout/demo.gametest.json b/prefabs/breakout/demo.gametest.json new file mode 100644 index 00000000..160d3a42 --- /dev/null +++ b/prefabs/breakout/demo.gametest.json @@ -0,0 +1,121 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 160, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_p0=undefined; global.gt_paddle=false; global.gt_bx=undefined; global.gt_ballmoved=false; global.gt_bricks0=0; global.gt_bricks_min=99999;" + }, + "watch": { + "gml": "if (instance_exists(obj_ball)) { global.gt_seen=true; with (obj_ball) { if (global.gt_bx == undefined) global.gt_bx = { x: x, y: y }; if (point_distance(x, y, global.gt_bx.x, global.gt_bx.y) > 8) global.gt_ballmoved = true; } } if (instance_exists(obj_paddle)) { with (obj_paddle) { if (global.gt_p0 == undefined) global.gt_p0 = x; if (x > global.gt_p0 + 8) global.gt_paddle = true; } } var _n = instance_number(obj_brick); if (_n > 0) { if (global.gt_bricks0 == 0) global.gt_bricks0 = _n; global.gt_bricks_min = min(global.gt_bricks_min, _n); }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "ball_moves": { + "gml": "return global.gt_ballmoved;" + }, + "paddle_moves": { + "gml": "return global.gt_paddle;" + }, + "bricks_present": { + "gml": "return global.gt_bricks0 > 0;" + }, + "brick_broken": { + "gml": "return global.gt_bricks_min < global.gt_bricks0;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 80, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 150, + "assert": { + "id": "seen", + "name": "the ball is in play in rm_play", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "ball_moves", + "name": "the ball travels", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "paddle_moves", + "name": "holding right moves the paddle", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "bricks_present", + "name": "the room is laid out with bricks", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "brick_broken", + "name": "the ball clears at least one brick", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 148, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/breakout/objects/obj_ball/Create.gml b/prefabs/breakout/objects/obj_ball/Create.gml new file mode 100644 index 00000000..04174bdd --- /dev/null +++ b/prefabs/breakout/objects/obj_ball/Create.gml @@ -0,0 +1,8 @@ +speed = ::kernel::kernel_tuning("ball_speed", 6); +direction = choose(230, 310); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +ball_speed_gain = ::kernel::kernel_tuning("ball_speed_gain", 0.12); +brick_score = ::kernel::kernel_tuning("brick_score", 50); +steer = ::kernel::kernel_tuning("steer", 65); diff --git a/prefabs/breakout/objects/obj_ball/Step.gml b/prefabs/breakout/objects/obj_ball/Step.gml new file mode 100644 index 00000000..3f34a030 --- /dev/null +++ b/prefabs/breakout/objects/obj_ball/Step.gml @@ -0,0 +1,33 @@ +if (!::kernel::kernel_playing()) exit; + +// Walls. The room edge is not an object, so this is a bounds test rather +// than a collision. +if (x < 8 && lengthdir_x(1, direction) < 0) direction = 180 - direction; +if (x > room_width - 8 && lengthdir_x(1, direction) > 0) direction = 180 - direction; +if (y < 8 && lengthdir_y(1, direction) < 0) direction = -direction; + +var _brick = instance_place(x, y, obj_brick); +if (_brick != noone) { + direction = -direction; + speed += ball_speed_gain; + ::kernel::kernel_score_add(brick_score); + ::feel::feel_pop(_brick.x, _brick.y, + "+" + string(brick_score), c_yellow); + ::feel::feel_shake(0.12, 4); + instance_destroy(_brick); +} + +// The paddle bends the bounce by where the ball lands on it. Without this +// the paddle is a wall and every rally repeats. +// +// Minus, not plus: `direction` is 0 right, 90 up, 180 left, so adding a +// negative offset steers a ball that hit the left of the paddle to the +// right. The game still plays, which is exactly why the sign is worth +// stating — a measured aim is the only way to notice. +var _pad = instance_place(x, y, obj_paddle); +if (_pad != noone && lengthdir_y(1, direction) > 0) { + var _offset = (x - _pad.x) / (_pad.sprite_width / 2); + var _steer = steer; + direction = 90 - clamp(_offset, -1, 1) * _steer; + y = _pad.bbox_top - 8; +} diff --git a/prefabs/breakout/objects/obj_ball/object.toml b/prefabs/breakout/objects/obj_ball/object.toml new file mode 100644 index 00000000..dd8f7dc2 --- /dev/null +++ b/prefabs/breakout/objects/obj_ball/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_ball" diff --git a/prefabs/breakout/objects/obj_breakout/Create.gml b/prefabs/breakout/objects/obj_breakout/Create.gml new file mode 100644 index 00000000..e0d7cc69 --- /dev/null +++ b/prefabs/breakout/objects/obj_breakout/Create.gml @@ -0,0 +1,5 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +lives_left = ::kernel::kernel_tuning("lives", 3); +total_bricks = 0; diff --git a/prefabs/breakout/objects/obj_breakout/Draw_GUI.gml b/prefabs/breakout/objects/obj_breakout/Draw_GUI.gml new file mode 100644 index 00000000..9f0bad0e --- /dev/null +++ b/prefabs/breakout/objects/obj_breakout/Draw_GUI.gml @@ -0,0 +1,4 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, "lives " + string(lives_left)); +::kernel::kernel_draw_text(16, 64, + "bricks " + string(instance_number(obj_brick)) + " / " + string(total_bricks)); diff --git a/prefabs/breakout/objects/obj_breakout/Other_RoomStart.gml b/prefabs/breakout/objects/obj_breakout/Other_RoomStart.gml new file mode 100644 index 00000000..d30c1d1a --- /dev/null +++ b/prefabs/breakout/objects/obj_breakout/Other_RoomStart.gml @@ -0,0 +1,4 @@ +// Counted here, not in Create: Create runs as each instance is made, so a +// controller placed first in the layer would count a room still half-built. +total_bricks = instance_number(obj_brick); +breakout_serve(); diff --git a/prefabs/breakout/objects/obj_breakout/Step.gml b/prefabs/breakout/objects/obj_breakout/Step.gml new file mode 100644 index 00000000..0ee7c68f --- /dev/null +++ b/prefabs/breakout/objects/obj_breakout/Step.gml @@ -0,0 +1,24 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); + +if (instance_number(obj_brick) == 0) { + ::kernel::kernel_game_over("cleared"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); + exit; +} + +// Dropped: the ball is past the bottom, which is the only losing condition. +var _ball = instance_find(obj_ball, 0); +if (_ball != noone && _ball.y > room_height + 20) { + instance_destroy(_ball); + lives_left -= 1; + ::feel::feel_shake(0.3, 9); + if (lives_left <= 0) { + ::kernel::kernel_game_over("dropped"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); + exit; + } + breakout_serve(); +} diff --git a/prefabs/breakout/objects/obj_breakout/object.toml b/prefabs/breakout/objects/obj_breakout/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/breakout/objects/obj_breakout/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/breakout/objects/obj_brick/object.toml b/prefabs/breakout/objects/obj_brick/object.toml new file mode 100644 index 00000000..6ed9f6f3 --- /dev/null +++ b/prefabs/breakout/objects/obj_brick/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_brick" diff --git a/prefabs/breakout/objects/obj_paddle/Create.gml b/prefabs/breakout/objects/obj_paddle/Create.gml new file mode 100644 index 00000000..bca766b3 --- /dev/null +++ b/prefabs/breakout/objects/obj_paddle/Create.gml @@ -0,0 +1 @@ +move_speed = ::kernel::kernel_tuning("paddle_speed", 9); diff --git a/prefabs/breakout/objects/obj_paddle/Step.gml b/prefabs/breakout/objects/obj_paddle/Step.gml new file mode 100644 index 00000000..28bffe3d --- /dev/null +++ b/prefabs/breakout/objects/obj_paddle/Step.gml @@ -0,0 +1,3 @@ +if (!::kernel::kernel_playing()) exit; +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +x = clamp(x + _dx * move_speed, sprite_width / 2, room_width - sprite_width / 2); diff --git a/prefabs/breakout/objects/obj_paddle/object.toml b/prefabs/breakout/objects/obj_paddle/object.toml new file mode 100644 index 00000000..5064f3fe --- /dev/null +++ b/prefabs/breakout/objects/obj_paddle/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_paddle" diff --git a/prefabs/breakout/objects/obj_results/Draw_GUI.gml b/prefabs/breakout/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/breakout/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/breakout/objects/obj_results/Step.gml b/prefabs/breakout/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/breakout/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/breakout/objects/obj_results/object.toml b/prefabs/breakout/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/breakout/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/breakout/objects/obj_title/Create.gml b/prefabs/breakout/objects/obj_title/Create.gml new file mode 100644 index 00000000..04899fa9 --- /dev/null +++ b/prefabs/breakout/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(breakout_data, breakout_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/breakout/objects/obj_title/Draw_GUI.gml b/prefabs/breakout/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..a75ca922 --- /dev/null +++ b/prefabs/breakout/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "BREAKOUT", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "clear the bricks, do not drop the ball", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/breakout/objects/obj_title/Step.gml b/prefabs/breakout/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/breakout/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/breakout/objects/obj_title/object.toml b/prefabs/breakout/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/breakout/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/breakout/prefab.toml b/prefabs/breakout/prefab.toml new file mode 100644 index 00000000..50129ca0 --- /dev/null +++ b/prefabs/breakout/prefab.toml @@ -0,0 +1,36 @@ +schema_version = 1 +name = "Breakout" +description = "Clear the bricks, do not drop the ball — and steer it with the paddle." +category = "game" +tags = ["genre:arcade", "role:template"] + +install = "copy" +requires = ["kernel", "feel"] + +seam = "rooms/rm_play/layers/Instances.toml" + +steps = [ + "`breakout.gametest.json` came with the prefab and already passes: the ball is in play, it travels, the paddle answers to left/right, the room is laid out with bricks, and the ball clears at least one of them. Run it with `gmx test breakout.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The wall is real entries in rm_play's instances layer, and that layer is the only copy of it — drag a brick in the room editor, or edit `rooms/rm_play/layers/Instances.toml` directly. A brick is an instance rather than a tile because the game destroys it and counts what is left: `instance_number(obj_brick)` is how a cleared board is detected, which a tile layer cannot answer.", + "`steer` is the number that decides whether the game has skill in it. At 0 the ball bounces off the paddle like a wall and every rally is identical.", +] + +[roles] +obj_breakout = "serves the ball, counts bricks, ends the round" +obj_paddle = "moves on the arrow keys, clamped to the room" +obj_ball = "bounces off walls, bricks and the paddle; steered by where it lands" +obj_brick = "one cell of the map; scores and vanishes" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "cleared or dropped, and the best score" + +[tuning] +paddle_speed = 9 +ball_speed = 6 +ball_speed_gain = 0.12 +# How much where the ball lands on the paddle bends its bounce. 0 makes the +# paddle a wall and every rally identical; this is the number that puts skill +# in the game. +steer = 65 +lives = 3 +brick_score = 50 diff --git a/prefabs/breakout/project.toml b/prefabs/breakout/project.toml new file mode 100644 index 00000000..12b989d7 --- /dev/null +++ b/prefabs/breakout/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "breakout" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/breakout/rooms/rm_play/layers/Background.toml b/prefabs/breakout/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/breakout/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/breakout/rooms/rm_play/layers/Instances.toml b/prefabs/breakout/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..a3e7a11e --- /dev/null +++ b/prefabs/breakout/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,303 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_breakout" +object = "obj_breakout" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_rm_play_obj_brick_0" +object = "obj_brick" +x = 62.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_1" +object = "obj_brick" +x = 138.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_2" +object = "obj_brick" +x = 214.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_3" +object = "obj_brick" +x = 290.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_4" +object = "obj_brick" +x = 366.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_5" +object = "obj_brick" +x = 442.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_6" +object = "obj_brick" +x = 518.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_7" +object = "obj_brick" +x = 594.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_8" +object = "obj_brick" +x = 670.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_9" +object = "obj_brick" +x = 746.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_10" +object = "obj_brick" +x = 822.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_11" +object = "obj_brick" +x = 898.0 +y = 70.0 + +[[entries]] +id = "inst_rm_play_obj_brick_12" +object = "obj_brick" +x = 62.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_13" +object = "obj_brick" +x = 138.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_14" +object = "obj_brick" +x = 214.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_15" +object = "obj_brick" +x = 290.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_16" +object = "obj_brick" +x = 366.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_17" +object = "obj_brick" +x = 442.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_18" +object = "obj_brick" +x = 518.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_19" +object = "obj_brick" +x = 594.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_20" +object = "obj_brick" +x = 670.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_21" +object = "obj_brick" +x = 746.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_22" +object = "obj_brick" +x = 822.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_23" +object = "obj_brick" +x = 898.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_brick_24" +object = "obj_brick" +x = 62.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_25" +object = "obj_brick" +x = 138.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_26" +object = "obj_brick" +x = 366.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_27" +object = "obj_brick" +x = 442.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_28" +object = "obj_brick" +x = 518.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_29" +object = "obj_brick" +x = 594.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_30" +object = "obj_brick" +x = 822.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_31" +object = "obj_brick" +x = 898.0 +y = 130.0 + +[[entries]] +id = "inst_rm_play_obj_brick_32" +object = "obj_brick" +x = 62.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_33" +object = "obj_brick" +x = 138.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_34" +object = "obj_brick" +x = 214.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_35" +object = "obj_brick" +x = 290.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_36" +object = "obj_brick" +x = 366.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_37" +object = "obj_brick" +x = 442.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_38" +object = "obj_brick" +x = 518.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_39" +object = "obj_brick" +x = 594.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_40" +object = "obj_brick" +x = 670.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_41" +object = "obj_brick" +x = 746.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_42" +object = "obj_brick" +x = 822.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_43" +object = "obj_brick" +x = 898.0 +y = 160.0 + +[[entries]] +id = "inst_rm_play_obj_brick_44" +object = "obj_brick" +x = 366.0 +y = 190.0 + +[[entries]] +id = "inst_rm_play_obj_brick_45" +object = "obj_brick" +x = 442.0 +y = 190.0 + +[[entries]] +id = "inst_rm_play_obj_brick_46" +object = "obj_brick" +x = 518.0 +y = 190.0 + +[[entries]] +id = "inst_rm_play_obj_brick_47" +object = "obj_brick" +x = 594.0 +y = 190.0 + +[[entries]] +id = "inst_rm_play_obj_paddle_0" +object = "obj_paddle" +x = 480.0 +y = 490.0 diff --git a/prefabs/breakout/rooms/rm_play/room.toml b/prefabs/breakout/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/breakout/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/breakout/rooms/rm_results/layers/Background.toml b/prefabs/breakout/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/breakout/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/breakout/rooms/rm_results/layers/Instances.toml b/prefabs/breakout/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/breakout/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/breakout/rooms/rm_results/room.toml b/prefabs/breakout/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/breakout/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/breakout/rooms/rm_title/layers/Background.toml b/prefabs/breakout/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/breakout/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/breakout/rooms/rm_title/layers/Instances.toml b/prefabs/breakout/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/breakout/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/breakout/rooms/rm_title/room.toml b/prefabs/breakout/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/breakout/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/breakout/scripts/scr_breakout_serve/scr_breakout_serve.gml b/prefabs/breakout/scripts/scr_breakout_serve/scr_breakout_serve.gml new file mode 100644 index 00000000..f473c661 --- /dev/null +++ b/prefabs/breakout/scripts/scr_breakout_serve/scr_breakout_serve.gml @@ -0,0 +1,9 @@ +/// @function breakout_serve() +/// @description Put a ball back in play above the paddle. Called at the +/// start of the round and after every drop, so the two paths cannot drift +/// apart. +function breakout_serve() { + var _pad = instance_find(obj_paddle, 0); + var _x = _pad == noone ? room_width / 2 : _pad.x; + instance_create_depth(_x, room_height - 90, 0, obj_ball); +} diff --git a/prefabs/breakout/sprites/spr_ball/frame_000.png b/prefabs/breakout/sprites/spr_ball/frame_000.png new file mode 100644 index 00000000..508f4334 Binary files /dev/null and b/prefabs/breakout/sprites/spr_ball/frame_000.png differ diff --git a/prefabs/breakout/sprites/spr_ball/sprite.toml b/prefabs/breakout/sprites/spr_ball/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/breakout/sprites/spr_ball/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/breakout/sprites/spr_brick/frame_000.png b/prefabs/breakout/sprites/spr_brick/frame_000.png new file mode 100644 index 00000000..f1b0bd72 Binary files /dev/null and b/prefabs/breakout/sprites/spr_brick/frame_000.png differ diff --git a/prefabs/breakout/sprites/spr_brick/sprite.toml b/prefabs/breakout/sprites/spr_brick/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/breakout/sprites/spr_brick/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/breakout/sprites/spr_paddle/frame_000.png b/prefabs/breakout/sprites/spr_paddle/frame_000.png new file mode 100644 index 00000000..8a7418fb Binary files /dev/null and b/prefabs/breakout/sprites/spr_paddle/frame_000.png differ diff --git a/prefabs/breakout/sprites/spr_paddle/sprite.toml b/prefabs/breakout/sprites/spr_paddle/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/breakout/sprites/spr_paddle/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/breakout/thumbnail.png b/prefabs/breakout/thumbnail.png new file mode 100644 index 00000000..0623d21f Binary files /dev/null and b/prefabs/breakout/thumbnail.png differ diff --git a/prefabs/bullet_hell/LICENSE b/prefabs/bullet_hell/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/bullet_hell/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/bullet_hell/README.md b/prefabs/bullet_hell/README.md new file mode 100644 index 00000000..9a998a94 --- /dev/null +++ b/prefabs/bullet_hell/README.md @@ -0,0 +1,41 @@ +# bullet_hell + +Thread the pattern. One hit ends it, so the hitbox is smaller than you are. + +## The hitbox is much smaller than the ship + +`hitbox_radius` is 4 against a 20px sprite, and collision is a distance check +rather than a sprite overlap. Every game in the genre does this, and it is not +a cheat: a hitbox matching the art makes a dense pattern *impossible* rather +than difficult, because there is no gap wide enough to pass through. + +The prefab draws the hitbox as a dot. That is also standard — the player has +to be able to see the thing the game is actually testing. + +Holding shift halves the speed for precise threading, which is the other half +of the bargain. + +## Every pattern is telegraphed + +Patterns are emitted on the boss's `telegraph_end`, never when the attack is +chosen. The wind-up is drawn as a ring that closes, so the player can time the +dodge instead of guessing it. A wall of bullets that arrives without warning +is not difficulty; the fight just reads as random. + +## Bullets expire + +`::projectile::projectile_step` ages each bullet and destroys it off-screen. +Without a lifetime, a two-minute fight leaves thousands of live instances and +the slowdown appears long after its cause. + +## The spiral offsets per shot, not per ring + +A ring of evenly spaced bullets is a wall. The spiral pattern adds a small +per-shot offset so the gaps rotate — the difference between a pattern you +thread and a pattern you die to. + +## What it does not do + +The player does not shoot. Scoring is survival time, and the boss is defeated +only by `boss_hurt` calls you add — wiring a fire button to it is the first +thing to do. diff --git a/prefabs/bullet_hell/demo.gametest.json b/prefabs/bullet_hell/demo.gametest.json new file mode 100644 index 00000000..cdc5c4c9 --- /dev/null +++ b/prefabs/bullet_hell/demo.gametest.json @@ -0,0 +1,139 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 160, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_x0=undefined; global.gt_moved=false; global.gt_shots=0; global.gt_bullets=0; global.gt_boss=false;" + }, + "watch": { + "gml": "if (instance_exists(obj_ship)) { with (obj_ship) { global.gt_seen = true; if (global.gt_x0 == undefined) global.gt_x0 = x; if (abs(x - global.gt_x0) > 8) global.gt_moved = true; } } global.gt_shots = max(global.gt_shots, instance_number(obj_shot)); global.gt_bullets = max(global.gt_bullets, instance_number(obj_bullet)); if (instance_exists(obj_boss)) global.gt_boss = true;" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "moved": { + "gml": "return global.gt_moved;" + }, + "fires": { + "gml": "return global.gt_shots > 0;" + }, + "pattern": { + "gml": "return global.gt_bullets > 0;" + }, + "boss": { + "gml": "return global.gt_boss;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 60, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 70, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 110, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 150, + "assert": { + "id": "seen", + "name": "the ship is in rm_play", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "boss", + "name": "the boss is on screen", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "moved", + "name": "arrow keys move the ship", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "fires", + "name": "space fires a shot", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "pattern", + "name": "the boss puts bullets on screen", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 148, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/bullet_hell/objects/obj_boss/Alarm_0.gml b/prefabs/bullet_hell/objects/obj_boss/Alarm_0.gml new file mode 100644 index 00000000..da7a4994 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_boss/Alarm_0.gml @@ -0,0 +1 @@ +fight.invulnerable = false; diff --git a/prefabs/bullet_hell/objects/obj_boss/Create.gml b/prefabs/bullet_hell/objects/obj_boss/Create.gml new file mode 100644 index 00000000..f0847215 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_boss/Create.gml @@ -0,0 +1,14 @@ +fight = ::boss::boss_make( + ::kernel::kernel_tuning("boss_hp", 400), + ::kernel::kernel_tuning("boss_phases", 3)); +moves = [ + { move: "ring", phase: 1 }, + { move: "spiral", phase: 2 }, + { move: "aimed", phase: 3 }, +]; +drift = 0; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +telegraph_seconds = ::kernel::kernel_tuning("telegraph_seconds", 0.8); +recover_seconds = ::kernel::kernel_tuning("recover_seconds", 1.0); diff --git a/prefabs/bullet_hell/objects/obj_boss/Draw.gml b/prefabs/bullet_hell/objects/obj_boss/Draw.gml new file mode 100644 index 00000000..e19c91b0 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_boss/Draw.gml @@ -0,0 +1,10 @@ +// The telegraph, made visible — a ring that closes as the wind-up runs. +if (::boss::boss_telegraphing(fight)) { + var _t = ::boss::boss_telegraph_fraction(fight); + draw_set_alpha(0.25 + _t * 0.6); + draw_set_colour(c_red); + draw_circle(x, y, 200 - _t * 150, true); + draw_set_alpha(1); + draw_set_colour(c_white); +} +draw_self(); diff --git a/prefabs/bullet_hell/objects/obj_boss/Step.gml b/prefabs/bullet_hell/objects/obj_boss/Step.gml new file mode 100644 index 00000000..f42c9a6e --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_boss/Step.gml @@ -0,0 +1,22 @@ +if (!::kernel::kernel_playing()) exit; + +drift += delta_time / 1000000; +x = room_width / 2 + sin(drift * 0.7) * 220; + +var _ev = ::boss::boss_step(fight); +if (_ev == "phase") { + fight.invulnerable = true; + alarm[0] = 40; +} +// The pattern is emitted when the wind-up ends, so a wall of bullets never +// arrives without warning. +if (_ev == "telegraph_end") hell_emit(::boss::boss_move(fight)); + +if (::boss::boss_ready(fight)) { + var _pick = ::boss::boss_pick(fight, moves); + if (_pick != "") { + ::boss::boss_begin(fight, _pick, + telegraph_seconds, 0.3, + recover_seconds); + } +} diff --git a/prefabs/bullet_hell/objects/obj_boss/object.toml b/prefabs/bullet_hell/objects/obj_boss/object.toml new file mode 100644 index 00000000..22aea371 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_boss/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_boss" diff --git a/prefabs/bullet_hell/objects/obj_bullet/Create.gml b/prefabs/bullet_hell/objects/obj_bullet/Create.gml new file mode 100644 index 00000000..7eef88e6 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_bullet/Create.gml @@ -0,0 +1 @@ +projectile_life = ::kernel::kernel_tuning("bullet_life", 8); diff --git a/prefabs/bullet_hell/objects/obj_bullet/Step.gml b/prefabs/bullet_hell/objects/obj_bullet/Step.gml new file mode 100644 index 00000000..3902a1f4 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_bullet/Step.gml @@ -0,0 +1,4 @@ +if (!::kernel::kernel_playing()) exit; +// Ages and destroys itself off-screen; without that a long fight +// accumulates thousands of them. +::projectile::projectile_step(noone); diff --git a/prefabs/bullet_hell/objects/obj_bullet/object.toml b/prefabs/bullet_hell/objects/obj_bullet/object.toml new file mode 100644 index 00000000..cc4c0be8 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_bullet/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_bullet" diff --git a/prefabs/bullet_hell/objects/obj_hell/Create.gml b/prefabs/bullet_hell/objects/obj_hell/Create.gml new file mode 100644 index 00000000..0bd65445 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_hell/Create.gml @@ -0,0 +1,12 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +// The ship and the boss are placed in rm_play — move either in an editor and +// the fight starts somewhere else with no code change. +survived = ::timer::timer_stopwatch(); +tally = 0; +overlord = noone; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +survive_score = ::kernel::kernel_tuning("survive_score", 5); diff --git a/prefabs/bullet_hell/objects/obj_hell/Draw_GUI.gml b/prefabs/bullet_hell/objects/obj_hell/Draw_GUI.gml new file mode 100644 index 00000000..98e21caf --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_hell/Draw_GUI.gml @@ -0,0 +1,11 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, + "survived " + ::timer::timer_format_ms(::timer::timer_seconds(survived))); +::kernel::kernel_draw_text(16, 64, "bullets " + string(instance_number(obj_bullet))); + +if (!instance_exists(overlord)) exit; +var _f = overlord.fight; +::kernel::kernel_draw_text(16, 96, + "phase " + string(_f.phase) + " of " + string(_f.phases)); +draw_rectangle(16, 120, 16 + 500 * ::boss::boss_fraction(_f), 134, false); +draw_rectangle(16, 120, 516, 134, true); diff --git a/prefabs/bullet_hell/objects/obj_hell/Other_RoomStart.gml b/prefabs/bullet_hell/objects/obj_hell/Other_RoomStart.gml new file mode 100644 index 00000000..2298e4c8 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_hell/Other_RoomStart.gml @@ -0,0 +1,3 @@ +// Resolved here, not in Create: Create runs as each instance is made, so a +// controller placed first in the layer would look for a room still half-built. +overlord = instance_find(obj_boss, 0); diff --git a/prefabs/bullet_hell/objects/obj_hell/Step.gml b/prefabs/bullet_hell/objects/obj_hell/Step.gml new file mode 100644 index 00000000..e17bf057 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_hell/Step.gml @@ -0,0 +1,17 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); +::timer::timer_step(survived); + +// Surviving is the score, so it accrues per second rather than per event. +var _want = floor(::timer::timer_seconds(survived)) + * survive_score; +if (_want > tally) { + ::kernel::kernel_score_add(_want - tally); + tally = _want; +} + +if (instance_exists(overlord) && ::boss::boss_dead(overlord.fight)) { + ::kernel::kernel_game_over("cleared"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); +} diff --git a/prefabs/bullet_hell/objects/obj_hell/object.toml b/prefabs/bullet_hell/objects/obj_hell/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_hell/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/bullet_hell/objects/obj_results/Draw_GUI.gml b/prefabs/bullet_hell/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/bullet_hell/objects/obj_results/Step.gml b/prefabs/bullet_hell/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/bullet_hell/objects/obj_results/object.toml b/prefabs/bullet_hell/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/bullet_hell/objects/obj_ship/Create.gml b/prefabs/bullet_hell/objects/obj_ship/Create.gml new file mode 100644 index 00000000..ac7b1934 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_ship/Create.gml @@ -0,0 +1,4 @@ +speed_full = ::kernel::kernel_tuning("ship_speed", 5); +speed_focus = ::kernel::kernel_tuning("focus_speed", 2); +hitbox = ::kernel::kernel_tuning("hitbox_radius", 4); +shot_cool = 0; diff --git a/prefabs/bullet_hell/objects/obj_ship/Draw.gml b/prefabs/bullet_hell/objects/obj_ship/Draw.gml new file mode 100644 index 00000000..c49cae1d --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_ship/Draw.gml @@ -0,0 +1,4 @@ +draw_self(); +// Showing the hitbox is standard in the genre, not a debug aid. +draw_set_colour(c_white); +draw_circle(x, y, hitbox, false); diff --git a/prefabs/bullet_hell/objects/obj_ship/Step.gml b/prefabs/bullet_hell/objects/obj_ship/Step.gml new file mode 100644 index 00000000..96070e57 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_ship/Step.gml @@ -0,0 +1,36 @@ +if (!::kernel::kernel_playing()) exit; + +// Holding shift slows you down for precise threading — the other half of +// the small-hitbox bargain. +var _s = keyboard_check(vk_shift) ? speed_focus : speed_full; +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +var _dy = keyboard_check(vk_down) - keyboard_check(vk_up); +x = clamp(x + _dx * _s, 12, room_width - 12); +y = clamp(y + _dy * _s, 12, room_height - 12); + +// Fire on a cooldown rather than every frame, so the rate is a knob rather +// than the frame rate. +shot_cool -= delta_time / 1000000; +if (keyboard_check(vk_space) && shot_cool <= 0) { + shot_cool = ::kernel::kernel_tuning("shot_interval", 0.12); + ::projectile::projectile_launch(obj_shot, x, y - 14, 90, + ::kernel::kernel_tuning("shot_speed", 9), + ::kernel::kernel_tuning("shot_life", 2)); +} + +// Collision by distance against the tiny hitbox, not by sprite overlap. +// This is the whole genre: a hitbox matching the art makes a dense +// pattern impossible rather than difficult. +var _struck = false; +with (obj_bullet) { + if (point_distance(x, y, other.x, other.y) <= other.hitbox + 5) { + _struck = true; + break; + } +} +if (!_struck) exit; + +::feel::feel_shake(0.4, 12); +::kernel::kernel_game_over("hit"); +::kernel::kernel_save_high_score(::kernel::kernel_score()); +room_goto(rm_results); diff --git a/prefabs/bullet_hell/objects/obj_ship/object.toml b/prefabs/bullet_hell/objects/obj_ship/object.toml new file mode 100644 index 00000000..e296d88d --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_ship/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_ship" diff --git a/prefabs/bullet_hell/objects/obj_shot/Create.gml b/prefabs/bullet_hell/objects/obj_shot/Create.gml new file mode 100644 index 00000000..0eba6f9c --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_shot/Create.gml @@ -0,0 +1,2 @@ +// `projectile_launch` sets `projectile_life`; damage is the shot's own. +damage = ::kernel::kernel_tuning("shot_damage", 6); diff --git a/prefabs/bullet_hell/objects/obj_shot/Step.gml b/prefabs/bullet_hell/objects/obj_shot/Step.gml new file mode 100644 index 00000000..d6aa27d0 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_shot/Step.gml @@ -0,0 +1,13 @@ +if (!::kernel::kernel_playing()) exit; + +// The only thing that damages the boss. Without a weapon its hp never moves, +// so `boss_dead` is unreachable, the cleared ending never fires, and because +// the phase is derived from the hp fraction the fight stays in phase one — +// every later pattern, and the phase-invulnerability path, is dead code. +var _boss = ::projectile::projectile_step(obj_boss); +if (_boss == noone) exit; + +if (::boss::boss_hurt(_boss.fight, damage)) { + ::feel::feel_pop(x, y, string(damage), c_aqua); +} +instance_destroy(); diff --git a/prefabs/bullet_hell/objects/obj_shot/object.toml b/prefabs/bullet_hell/objects/obj_shot/object.toml new file mode 100644 index 00000000..166e6582 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_shot/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_shot" diff --git a/prefabs/bullet_hell/objects/obj_title/Create.gml b/prefabs/bullet_hell/objects/obj_title/Create.gml new file mode 100644 index 00000000..4b692b90 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(bullet_hell_data, bullet_hell_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/bullet_hell/objects/obj_title/Draw_GUI.gml b/prefabs/bullet_hell/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..80abbbc4 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "BULLET HELL", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "thread the pattern, one hit ends it", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/bullet_hell/objects/obj_title/Step.gml b/prefabs/bullet_hell/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/bullet_hell/objects/obj_title/object.toml b/prefabs/bullet_hell/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/bullet_hell/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/bullet_hell/prefab.toml b/prefabs/bullet_hell/prefab.toml new file mode 100644 index 00000000..5e3a6a4f --- /dev/null +++ b/prefabs/bullet_hell/prefab.toml @@ -0,0 +1,47 @@ +schema_version = 1 +name = "Bullet hell" +description = "Thread the pattern. One hit ends it, so the hitbox is smaller than you are." +category = "game" +tags = ["genre:shmup", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "boss", "projectile", "timer"] + +seam = "scripts/scr_bullet_hell_data/scr_bullet_hell_data.gml" + +steps = [ + "`bullet_hell.gametest.json` came with the prefab and already passes: the ship is in rm_play with the boss on screen, arrows move it, space fires a shot, and the boss puts bullets on screen. Run it with `gmx test bullet_hell.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "`hitbox_radius` is far smaller than the ship sprite on purpose. A hitbox that matches the art makes a dense pattern impossible rather than difficult, and every game in this genre does this.", + "Patterns are emitted on the boss's `telegraph_end`, so the player always sees a wind-up before a wall of bullets arrives.", + "Bullets are destroyed off-screen by `::projectile::`. Without that a two-minute fight accumulates thousands and the frame rate falls away long after the mistake.", +] + +[roles] +obj_hell = "runs the fight, picks patterns, ends the round" +obj_ship = "the player — moves, and has a hitbox much smaller than its sprite" +obj_shot = "the ship's shot, and the only thing that damages the boss" +obj_bullet = "one shot; travels, expires, and ends the run on contact" +obj_boss = "takes damage, changes phase, telegraphs every pattern" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "how long you survived, and the best" + +[tuning] +ship_speed = 5 +focus_speed = 2 +# Much smaller than the sprite. A hitbox matching the art makes a dense +# pattern impossible rather than hard — the whole genre depends on this. +hitbox_radius = 4 +boss_hp = 400 +boss_phases = 3 +bullet_speed = 3.5 +bullet_life = 8 +ring_count = 18 +telegraph_seconds = 0.8 +recover_seconds = 1.0 +# The ship's own gun. `boss_hp` is only reachable through this. +shot_damage = 6 +shot_interval = 0.12 +shot_speed = 9 +shot_life = 2 +survive_score = 5 diff --git a/prefabs/bullet_hell/project.toml b/prefabs/bullet_hell/project.toml new file mode 100644 index 00000000..7974422d --- /dev/null +++ b/prefabs/bullet_hell/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "bullet_hell" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/bullet_hell/rooms/rm_play/layers/Background.toml b/prefabs/bullet_hell/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/bullet_hell/rooms/rm_play/layers/Instances.toml b/prefabs/bullet_hell/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..e8e25dc1 --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,21 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_hell_0" +object = "obj_hell" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_rm_play_obj_ship_0" +object = "obj_ship" +x = 480.0 +y = 450.0 + +[[entries]] +id = "inst_rm_play_obj_boss_0" +object = "obj_boss" +x = 480.0 +y = 130.0 diff --git a/prefabs/bullet_hell/rooms/rm_play/room.toml b/prefabs/bullet_hell/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/bullet_hell/rooms/rm_results/layers/Background.toml b/prefabs/bullet_hell/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/bullet_hell/rooms/rm_results/layers/Instances.toml b/prefabs/bullet_hell/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/bullet_hell/rooms/rm_results/room.toml b/prefabs/bullet_hell/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/bullet_hell/rooms/rm_title/layers/Background.toml b/prefabs/bullet_hell/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/bullet_hell/rooms/rm_title/layers/Instances.toml b/prefabs/bullet_hell/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/bullet_hell/rooms/rm_title/room.toml b/prefabs/bullet_hell/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/bullet_hell/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/bullet_hell/scripts/scr_hell_patterns/scr_hell_patterns.gml b/prefabs/bullet_hell/scripts/scr_hell_patterns/scr_hell_patterns.gml new file mode 100644 index 00000000..d9d185da --- /dev/null +++ b/prefabs/bullet_hell/scripts/scr_hell_patterns/scr_hell_patterns.gml @@ -0,0 +1,41 @@ +/// @function hell_emit(move) +/// @description Fire one pattern from the boss. Runs in `obj_boss`, so `x` +/// and `y` are its position. +/// +/// Each pattern is a loop over `::projectile::projectile_launch`, which +/// gives every bullet its lifetime — the part that keeps a long fight from +/// accumulating thousands of instances. +function hell_emit(move) { + var _n = ::kernel::kernel_tuning("ring_count", 18); + var _spd = ::kernel::kernel_tuning("bullet_speed", 3.5); + var _life = ::kernel::kernel_tuning("bullet_life", 8); + var _ship = instance_find(obj_ship, 0); + + switch (move) { + case "ring": + for (var i = 0; i < _n; i++) { + ::projectile::projectile_launch( + obj_bullet, x, y, i * 360 / _n, _spd, _life); + } + break; + + case "spiral": + // Offset per shot rather than per ring, so the wall has gaps + // that move — a solid ring is a wall you cannot thread. + for (var i = 0; i < _n * 2; i++) { + ::projectile::projectile_launch( + obj_bullet, x, y, i * 360 / _n + i * 7, _spd, _life); + } + break; + + case "aimed": + if (_ship == noone) break; + var _at = ::projectile::projectile_aim(x, y, _ship.x, _ship.y); + for (var i = -3; i <= 3; i++) { + ::projectile::projectile_launch( + obj_bullet, x, y, _at + i * 9, _spd * 1.3, _life); + } + break; + } + ::feel::feel_shake(0.12, 4); +} diff --git a/prefabs/bullet_hell/sprites/spr_boss/frame_000.png b/prefabs/bullet_hell/sprites/spr_boss/frame_000.png new file mode 100644 index 00000000..aab6b894 Binary files /dev/null and b/prefabs/bullet_hell/sprites/spr_boss/frame_000.png differ diff --git a/prefabs/bullet_hell/sprites/spr_boss/sprite.toml b/prefabs/bullet_hell/sprites/spr_boss/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/bullet_hell/sprites/spr_boss/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/bullet_hell/sprites/spr_bullet/frame_000.png b/prefabs/bullet_hell/sprites/spr_bullet/frame_000.png new file mode 100644 index 00000000..ea875ddd Binary files /dev/null and b/prefabs/bullet_hell/sprites/spr_bullet/frame_000.png differ diff --git a/prefabs/bullet_hell/sprites/spr_bullet/sprite.toml b/prefabs/bullet_hell/sprites/spr_bullet/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/bullet_hell/sprites/spr_bullet/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/bullet_hell/sprites/spr_ship/frame_000.png b/prefabs/bullet_hell/sprites/spr_ship/frame_000.png new file mode 100644 index 00000000..823beaaf Binary files /dev/null and b/prefabs/bullet_hell/sprites/spr_ship/frame_000.png differ diff --git a/prefabs/bullet_hell/sprites/spr_ship/sprite.toml b/prefabs/bullet_hell/sprites/spr_ship/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/bullet_hell/sprites/spr_ship/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/bullet_hell/sprites/spr_shot/frame_000.png b/prefabs/bullet_hell/sprites/spr_shot/frame_000.png new file mode 100644 index 00000000..73075426 Binary files /dev/null and b/prefabs/bullet_hell/sprites/spr_shot/frame_000.png differ diff --git a/prefabs/bullet_hell/sprites/spr_shot/sprite.toml b/prefabs/bullet_hell/sprites/spr_shot/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/bullet_hell/sprites/spr_shot/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/bullet_hell/thumbnail.png b/prefabs/bullet_hell/thumbnail.png new file mode 100644 index 00000000..51b741a1 Binary files /dev/null and b/prefabs/bullet_hell/thumbnail.png differ diff --git a/prefabs/camera/LICENSE b/prefabs/camera/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/camera/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/camera/README.md b/prefabs/camera/README.md new file mode 100644 index 00000000..9676cf03 --- /dev/null +++ b/prefabs/camera/README.md @@ -0,0 +1,3 @@ +# camera + +A view that follows something, with a deadzone, clamped to the room. diff --git a/prefabs/camera/objects/obj_demo/Create.gml b/prefabs/camera/objects/obj_demo/Create.gml new file mode 100644 index 00000000..d2bb7036 --- /dev/null +++ b/prefabs/camera/objects/obj_demo/Create.gml @@ -0,0 +1,4 @@ +// A room four times the view, so the clamp and the deadzone both have +// somewhere to show themselves. +cam = camera_follow_make(960, 540, { ease: 0.12 }); +camera_follow_snap(cam, x, y); diff --git a/prefabs/camera/objects/obj_demo/Draw.gml b/prefabs/camera/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..eec466e5 --- /dev/null +++ b/prefabs/camera/objects/obj_demo/Draw.gml @@ -0,0 +1,9 @@ +draw_self(); +// The deadzone, so what the camera is doing is visible rather than felt. +draw_set_colour(#3A4152); +var _cx = cam.x + cam.view_w * 0.5, _cy = cam.y + cam.view_h * 0.5; +draw_rectangle( + _cx - cam.deadzone_x, _cy - cam.deadzone_y, + _cx + cam.deadzone_x, _cy + cam.deadzone_y, true +); +draw_set_colour(c_white); diff --git a/prefabs/camera/objects/obj_demo/Step.gml b/prefabs/camera/objects/obj_demo/Step.gml new file mode 100644 index 00000000..1ba2c510 --- /dev/null +++ b/prefabs/camera/objects/obj_demo/Step.gml @@ -0,0 +1,7 @@ +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +var _dy = keyboard_check(vk_down) - keyboard_check(vk_up); +x = clamp(x + _dx * 6, 0, room_width); +y = clamp(y + _dy * 6, 0, room_height); + +var _p = camera_follow_step(cam, x, y); +camera_set_view_pos(view_camera[0], _p.x, _p.y); diff --git a/prefabs/camera/objects/obj_demo/object.toml b/prefabs/camera/objects/obj_demo/object.toml new file mode 100644 index 00000000..72e2c3bd --- /dev/null +++ b/prefabs/camera/objects/obj_demo/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_dot" diff --git a/prefabs/camera/prefab.toml b/prefabs/camera/prefab.toml new file mode 100644 index 00000000..49fc7852 --- /dev/null +++ b/prefabs/camera/prefab.toml @@ -0,0 +1,12 @@ +schema_version = 1 +name = "Follow camera" + +# A library: installed under `prefabs//` in the target project and +# called as `::camera::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "A camera that follows a target with a deadzone it can move inside freely, eases toward it rather than snapping, and never shows outside the room. Three rules that are individually obvious and collectively fiddly — especially the clamp, which has to win over the ease or the view judders at the edges." +category = "library" +tags = ["capability:camera", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo", "sprites/spr_demo_dot"] diff --git a/prefabs/camera/project.toml b/prefabs/camera/project.toml new file mode 100644 index 00000000..8ad5be6b --- /dev/null +++ b/prefabs/camera/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "camera" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/camera/rooms/rm_demo/layers/Background.toml b/prefabs/camera/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/camera/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/camera/rooms/rm_demo/layers/Instances.toml b/prefabs/camera/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..9001801b --- /dev/null +++ b/prefabs/camera/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 480.0 +y = 270.0 diff --git a/prefabs/camera/rooms/rm_demo/room.toml b/prefabs/camera/rooms/rm_demo/room.toml new file mode 100644 index 00000000..a5929009 --- /dev/null +++ b/prefabs/camera/rooms/rm_demo/room.toml @@ -0,0 +1,13 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 3840, height = 2160 } +enable_views = true + +# An `enable_views` room with no visible viewport renders nothing — see the +# platformer recipe's room for the same note. +[[views]] +visible = true +w_view = 960 +h_view = 540 +w_port = 960 +h_port = 540 diff --git a/prefabs/camera/scripts/scr_camera/scr_camera.gml b/prefabs/camera/scripts/scr_camera/scr_camera.gml new file mode 100644 index 00000000..1bf9ca96 --- /dev/null +++ b/prefabs/camera/scripts/scr_camera/scr_camera.gml @@ -0,0 +1,69 @@ +/// A view that follows something. +/// +/// Three rules, applied in this order and no other: the deadzone decides +/// whether the camera wants to move at all, the ease decides how fast it +/// gets there, and the clamp decides where it is allowed to end up. Ease +/// after clamp lets the view drift outside the room and snap back; clamp +/// after ease is what makes the edges feel solid. + +/// @function camera_follow_make(view_w, view_h, tuning) +/// @description Camera state for a `view_w` x `view_h` view. Override any +/// default by passing a struct. +function camera_follow_make(view_w, view_h, tuning = {}) { + var _d = { + view_w: view_w, + view_h: view_h, + // Half-size of the box the target moves in before the camera + // reacts. Zero follows every pixel, which reads as motion + // sickness on a walking character. + deadzone_x: view_w * 0.15, + deadzone_y: view_h * 0.15, + // 1 snaps, 0 never arrives. 0.1–0.2 is a camera you stop noticing. + ease: 0.15, + x: 0, + y: 0 + }; + var _keys = variable_struct_get_names(tuning); + for (var i = 0; i < array_length(_keys); i++) { + _d[$ _keys[i]] = tuning[$ _keys[i]]; + } + return _d; +} + +/// @function camera_follow_step(state, tx, ty) +/// @description Move `state` one frame toward (`tx`, `ty`) and return the +/// top-left corner as a struct `{x, y}`. Apply it with +/// `camera_set_view_pos(view_camera[0], p.x, p.y)`. +function camera_follow_step(state, tx, ty) { + var _cx = state.x + state.view_w * 0.5; + var _cy = state.y + state.view_h * 0.5; + + // Only the distance *beyond* the deadzone is a reason to move. + var _dx = tx - _cx; + var _dy = ty - _cy; + if (abs(_dx) <= state.deadzone_x) _dx = 0; + else _dx -= sign(_dx) * state.deadzone_x; + if (abs(_dy) <= state.deadzone_y) _dy = 0; + else _dy -= sign(_dy) * state.deadzone_y; + + state.x += _dx * state.ease; + state.y += _dy * state.ease; + + // Clamp last, so a room edge is a hard stop rather than something the + // ease keeps pulling away from. A room smaller than the view pins to 0 + // instead of producing a negative range. + state.x = clamp(state.x, 0, max(0, room_width - state.view_w)); + state.y = clamp(state.y, 0, max(0, room_height - state.view_h)); + + return { x: state.x, y: state.y }; +} + +/// @function camera_follow_snap(state, tx, ty) +/// @description Put the camera where it would eventually settle, with no +/// ease. Use it on room start so the first frame is not a pan in from the +/// corner. +function camera_follow_snap(state, tx, ty) { + state.x = clamp(tx - state.view_w * 0.5, 0, max(0, room_width - state.view_w)); + state.y = clamp(ty - state.view_h * 0.5, 0, max(0, room_height - state.view_h)); + return { x: state.x, y: state.y }; +} diff --git a/prefabs/camera/sprites/spr_demo_dot/frame_000.png b/prefabs/camera/sprites/spr_demo_dot/frame_000.png new file mode 100644 index 00000000..efd7e24d Binary files /dev/null and b/prefabs/camera/sprites/spr_demo_dot/frame_000.png differ diff --git a/prefabs/camera/sprites/spr_demo_dot/sprite.toml b/prefabs/camera/sprites/spr_demo_dot/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/camera/sprites/spr_demo_dot/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/camera/thumbnail.png b/prefabs/camera/thumbnail.png new file mode 100644 index 00000000..933cb90a Binary files /dev/null and b/prefabs/camera/thumbnail.png differ diff --git a/prefabs/camera3d/LICENSE b/prefabs/camera3d/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/camera3d/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/camera3d/README.md b/prefabs/camera3d/README.md new file mode 100644 index 00000000..148ab1b6 --- /dev/null +++ b/prefabs/camera3d/README.md @@ -0,0 +1,22 @@ +# camera3d + +Trackball orbit camera and ray picking that agrees with it. + +Orbit camera as two vectors rather than Euler angles: Rodrigues rotation, an orthonormalised basis, and ray-sphere picking that reuses that basis. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +cam = camera3d_make(300, 60); +cam = camera3d_orbit(cam, dx, dy); +var ray = camera3d_ray(cam, mx, my, w, h); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::camera3d::` prefix. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/camera3d/objects/obj_demo/Create.gml b/prefabs/camera3d/objects/obj_demo/Create.gml new file mode 100644 index 00000000..623343fe --- /dev/null +++ b/prefabs/camera3d/objects/obj_demo/Create.gml @@ -0,0 +1,11 @@ +cam = camera3d_make(300, 60); +dragging = false; +last = { x: 0, y: 0 }; + +// Three spheres at known positions. Picking one proves the ray and the +// projection agree — which is the only thing this demo is for. +balls = [ + { pos: { x: -90, y: 0, z: 0 }, r: 40, hit: false }, + { pos: { x: 0, y: 0, z: 0 }, r: 40, hit: false }, + { pos: { x: 90, y: 0, z: 0 }, r: 40, hit: false }, +]; diff --git a/prefabs/camera3d/objects/obj_demo/Draw_GUI.gml b/prefabs/camera3d/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..3612ce30 --- /dev/null +++ b/prefabs/camera3d/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,26 @@ +// Project each sphere centre with the same basis the ray used, so a +// mismatch shows up as a marker that misses its own circle. +var _b = camera3d_basis(cam); +var _eye = camera3d_position(cam); +var _w = display_get_gui_width(); +var _h = display_get_gui_height(); +var _tan = tan(degtorad(cam.fov) / 2); + +for (var i = 0; i < array_length(balls); i++) { + var _p = balls[i].pos; + var _v = { x: _p.x - _eye.x, y: _p.y - _eye.y, z: _p.z - _eye.z }; + var _z = _v.x * _b.forward.x + _v.y * _b.forward.y + _v.z * _b.forward.z; + if (_z <= 1) continue; + var _rx = _v.x * _b.right.x + _v.y * _b.right.y + _v.z * _b.right.z; + var _ry = _v.x * _b.up.x + _v.y * _b.up.y + _v.z * _b.up.z; + + var _sx = _w / 2 + (_rx / (_z * _tan * (_w / _h))) * (_w / 2); + var _sy = _h / 2 - (_ry / (_z * _tan)) * (_h / 2); + var _sr = (balls[i].r / (_z * _tan)) * (_h / 2); + + draw_set_colour(balls[i].hit ? c_yellow : c_aqua); + draw_circle(_sx, _sy, _sr, true); +} +draw_set_colour(c_white); +draw_text(16, 16, "camera3d demo — right-drag orbits, wheel zooms"); +draw_text(16, 36, "hover a sphere: the pick ray uses the same basis as the drawing"); diff --git a/prefabs/camera3d/objects/obj_demo/Step.gml b/prefabs/camera3d/objects/obj_demo/Step.gml new file mode 100644 index 00000000..c18603ff --- /dev/null +++ b/prefabs/camera3d/objects/obj_demo/Step.gml @@ -0,0 +1,22 @@ +var _mx = device_mouse_x_to_gui(0); +var _my = device_mouse_y_to_gui(0); + +if (mouse_check_button_pressed(mb_right)) { dragging = true; last = { x: _mx, y: _my }; } +if (!mouse_check_button(mb_right)) dragging = false; +if (dragging) { + cam = camera3d_orbit(cam, _mx - last.x, _my - last.y); + last = { x: _mx, y: _my }; +} + +cam.distance = clamp(cam.distance - (mouse_wheel_up() - mouse_wheel_down()) * 40, 120, 900); + +// Pick through the SAME basis the drawing below uses. +var _ray = camera3d_ray(cam, _mx, _my, display_get_gui_width(), display_get_gui_height()); +var _best = -1; +var _which = -1; +for (var i = 0; i < array_length(balls); i++) { + balls[i].hit = false; + var _t = camera3d_hit_sphere(_ray, balls[i].pos, balls[i].r); + if (_t >= 0 && (_best < 0 || _t < _best)) { _best = _t; _which = i; } +} +if (_which >= 0) balls[_which].hit = true; diff --git a/prefabs/camera3d/objects/obj_demo/object.toml b/prefabs/camera3d/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/camera3d/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/camera3d/prefab.toml b/prefabs/camera3d/prefab.toml new file mode 100644 index 00000000..25b0aa39 --- /dev/null +++ b/prefabs/camera3d/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "3D orbit camera" + +# A library: installed under `prefabs//` in the target project and +# called as `::camera3d::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = [] + +description = "Orbit camera as two vectors rather than Euler angles: Rodrigues rotation, an orthonormalised basis, and ray-sphere picking that reuses that basis." +category = "library" +tags = ["capability:camera", "capability:3d", "capability:picking", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/camera3d/project.toml b/prefabs/camera3d/project.toml new file mode 100644 index 00000000..fbb0192d --- /dev/null +++ b/prefabs/camera3d/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "camera3d" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/camera3d/rooms/rm_demo/layers/Background.toml b/prefabs/camera3d/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..8ed7007a --- /dev/null +++ b/prefabs/camera3d/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#0E1018" diff --git a/prefabs/camera3d/rooms/rm_demo/layers/Instances.toml b/prefabs/camera3d/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2c5f18a7 --- /dev/null +++ b/prefabs/camera3d/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/camera3d/rooms/rm_demo/room.toml b/prefabs/camera3d/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/camera3d/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/camera3d/scripts/scr_camera3d/scr_camera3d.gml b/prefabs/camera3d/scripts/scr_camera3d/scr_camera3d.gml new file mode 100644 index 00000000..c0afadbe --- /dev/null +++ b/prefabs/camera3d/scripts/scr_camera3d/scr_camera3d.gml @@ -0,0 +1,58 @@ +/// A trackball orbit camera. +/// +/// State is two vectors — a direction and an up — not a yaw/pitch pair. +/// Euler angles gimbal-lock at the poles and make "rotate around the +/// camera's CURRENT axes" awkward; two vectors rotate around whatever +/// axis you hand them and never lock. +/// +/// The basis is exposed because the renderer and the picker must use the +/// SAME one. `matrix_build_lookat` silently re-orthogonalises what you +/// give it, so a camera with a subtly wrong basis still LOOKS correct +/// and then throws the pick ray somewhere else entirely. + +/// @function camera3d_make(distance, fov) +function camera3d_make(distance = 400, fov = 60) { + return { + target: { x: 0, y: 0, z: 0 }, + dir: { x: 0, y: -1, z: 0 }, // from target toward the camera + up: { x: 0, y: 0, z: 1 }, + distance: distance, + fov: fov + }; +} + +/// @function camera3d_basis(cam) +/// @description The orthonormal {forward, right, up} the camera is +/// actually looking along. Both rendering and picking read this. +function camera3d_basis(cam) { + var _fwd = camera3d_normalise({ x: -cam.dir.x, y: -cam.dir.y, z: -cam.dir.z }); + var _right = camera3d_normalise(camera3d_cross(_fwd, cam.up)); + // Re-derive up from the other two so the basis is orthonormal even + // after many incremental drags have accumulated float error. + var _up = camera3d_cross(_right, _fwd); + return { forward: _fwd, right: _right, up: _up }; +} + +/// @function camera3d_position(cam) +function camera3d_position(cam) { + return { + x: cam.target.x + cam.dir.x * cam.distance, + y: cam.target.y + cam.dir.y * cam.distance, + z: cam.target.z + cam.dir.z * cam.distance + }; +} + +/// @function camera3d_orbit(cam, dx, dy) +/// @description Drag by screen pixels. Yaw turns about the camera's own +/// up, pitch about its own right — which is what "trackball" means and +/// why this needs no pole clamp. +function camera3d_orbit(cam, dx, dy) { + var _b = camera3d_basis(cam); + cam.dir = camera3d_normalise(camera3d_rotate_vector(cam.dir, _b.up, -dx * 0.4)); + cam.up = camera3d_normalise(camera3d_rotate_vector(cam.up, _b.up, -dx * 0.4)); + var _b2 = camera3d_basis(cam); + cam.dir = camera3d_normalise(camera3d_rotate_vector(cam.dir, _b2.right, -dy * 0.4)); + cam.up = camera3d_normalise(camera3d_rotate_vector(cam.up, _b2.right, -dy * 0.4)); + return cam; +} + diff --git a/prefabs/camera3d/scripts/scr_camera3d_math/scr_camera3d_math.gml b/prefabs/camera3d/scripts/scr_camera3d_math/scr_camera3d_math.gml new file mode 100644 index 00000000..af6b135e --- /dev/null +++ b/prefabs/camera3d/scripts/scr_camera3d_math/scr_camera3d_math.gml @@ -0,0 +1,34 @@ +/// @function camera3d_rotate_vector(v, axis, degrees) +/// @description Rodrigues rotation of `v` about a unit `axis`. +function camera3d_rotate_vector(v, axis, degrees) { + var _a = degtorad(degrees); + var _c = cos(_a); + var _s = sin(_a); + var _dot = v.x * axis.x + v.y * axis.y + v.z * axis.z; + return { + x: v.x * _c + (axis.y * v.z - axis.z * v.y) * _s + axis.x * _dot * (1 - _c), + y: v.y * _c + (axis.z * v.x - axis.x * v.z) * _s + axis.y * _dot * (1 - _c), + z: v.z * _c + (axis.x * v.y - axis.y * v.x) * _s + axis.z * _dot * (1 - _c) + }; +} + +/// @function camera3d_normalise(v) +function camera3d_normalise(v) { + var _l = sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + if (_l == 0) return { x: 0, y: 0, z: 0 }; + return { x: v.x / _l, y: v.y / _l, z: v.z / _l }; +} + +/// @function camera3d_cross(a, b) +/// @description a × b. Operand order is the footgun in this whole file: +/// swapping it flips the right vector, and at screen centre the error +/// has a zero coefficient — so it looks perfect until you click +/// off-centre. Always test picking away from the middle. +function camera3d_cross(a, b) { + return { + x: a.y * b.z - a.z * b.y, + y: a.z * b.x - a.x * b.z, + z: a.x * b.y - a.y * b.x + }; +} + diff --git a/prefabs/camera3d/scripts/scr_camera3d_pick/scr_camera3d_pick.gml b/prefabs/camera3d/scripts/scr_camera3d_pick/scr_camera3d_pick.gml new file mode 100644 index 00000000..f6c4b8f7 --- /dev/null +++ b/prefabs/camera3d/scripts/scr_camera3d_pick/scr_camera3d_pick.gml @@ -0,0 +1,34 @@ +/// @function camera3d_ray(cam, gui_x, gui_y, view_w, view_h) +/// @description A world-space ray through a GUI-space point, as +/// {origin, dir}. GUI space, not room space: in a room with a moving +/// view the two differ and the pick silently misses. +function camera3d_ray(cam, gui_x, gui_y, view_w, view_h) { + var _b = camera3d_basis(cam); + var _aspect = view_w / max(view_h, 1); + var _tan = tan(degtorad(cam.fov) / 2); + + // Normalised device coords, +y up. + var _ndc_x = (gui_x / max(view_w, 1)) * 2 - 1; + var _ndc_y = 1 - (gui_y / max(view_h, 1)) * 2; + + var _dir = { + x: _b.forward.x + _b.right.x * _ndc_x * _tan * _aspect + _b.up.x * _ndc_y * _tan, + y: _b.forward.y + _b.right.y * _ndc_x * _tan * _aspect + _b.up.y * _ndc_y * _tan, + z: _b.forward.z + _b.right.z * _ndc_x * _tan * _aspect + _b.up.z * _ndc_y * _tan + }; + return { origin: camera3d_position(cam), dir: camera3d_normalise(_dir) }; +} + +/// @function camera3d_hit_sphere(ray, centre, radius) +/// @description Distance along `ray` to the near intersection, or -1. +function camera3d_hit_sphere(ray, centre, radius) { + var _ox = ray.origin.x - centre.x; + var _oy = ray.origin.y - centre.y; + var _oz = ray.origin.z - centre.z; + var _b = 2 * (_ox * ray.dir.x + _oy * ray.dir.y + _oz * ray.dir.z); + var _c = _ox * _ox + _oy * _oy + _oz * _oz - radius * radius; + var _disc = _b * _b - 4 * _c; + if (_disc < 0) return -1; + var _t = (-_b - sqrt(_disc)) / 2; + return (_t < 0) ? -1 : _t; +} diff --git a/prefabs/camera3d/thumbnail.png b/prefabs/camera3d/thumbnail.png new file mode 100644 index 00000000..9908dac6 Binary files /dev/null and b/prefabs/camera3d/thumbnail.png differ diff --git a/prefabs/camera3d_basics/LICENSE b/prefabs/camera3d_basics/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/camera3d_basics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/camera3d_basics/README.md b/prefabs/camera3d_basics/README.md new file mode 100644 index 00000000..874d9e93 --- /dev/null +++ b/prefabs/camera3d_basics/README.md @@ -0,0 +1,31 @@ +# camera3d_basics + +A trackball camera you drag to orbit, with picking that agrees with what you see. + +## Why two vectors instead of yaw and pitch + +The camera stores a direction and an up vector, not Euler angles. Angles +gimbal-lock at the poles, and "rotate around the camera's *current* axes" +— which is what dragging a trackball means — is awkward to express in +them. Two vectors rotate about any axis you hand them and never lock. + +## The footgun, and how to actually test for it + +Cross-product operand order. Swap it and the right vector flips. + +The reason this is worth a section: **at screen centre the error has a +zero coefficient.** A picking ray cast through the middle of the screen +lands correctly with the sign wrong. Everything looks fine, and clicks +miss by more and more the further out you go. + +So test picking **off-centre**. Hover something near a corner and confirm +it highlights. If it works dead centre and nowhere else, you have found +this exact bug. + +## Why picking and rendering share one basis + +`matrix_build_lookat` silently re-orthogonalises whatever you give it, so +a camera with a subtly wrong basis still *renders* correctly and then +throws the pick ray somewhere else. `::camera3d::camera3d_basis` is the +one source both read, which is what makes the mismatch impossible rather +than merely documented. diff --git a/prefabs/camera3d_basics/objects/obj_orbit_scene/Create.gml b/prefabs/camera3d_basics/objects/obj_orbit_scene/Create.gml new file mode 100644 index 00000000..23d85f4b --- /dev/null +++ b/prefabs/camera3d_basics/objects/obj_orbit_scene/Create.gml @@ -0,0 +1,14 @@ +var _cfg = camera3d_basics_tuning(); + +cam = ::camera3d::camera3d_make(_cfg.distance, _cfg.fov); +dragging = false; +last = { x: 0, y: 0 }; +picked = -1; + +// Your scene goes here. Each entry needs a position and a radius for +// picking; swap the drawing for real geometry when you have it. +things = [ + { pos: { x: -90, y: 0, z: 0 }, r: 40 }, + { pos: { x: 0, y: 0, z: 0 }, r: 40 }, + { pos: { x: 90, y: 0, z: 0 }, r: 40 }, +]; diff --git a/prefabs/camera3d_basics/objects/obj_orbit_scene/Draw_GUI.gml b/prefabs/camera3d_basics/objects/obj_orbit_scene/Draw_GUI.gml new file mode 100644 index 00000000..21bfa34d --- /dev/null +++ b/prefabs/camera3d_basics/objects/obj_orbit_scene/Draw_GUI.gml @@ -0,0 +1,21 @@ +var _b = ::camera3d::camera3d_basis(cam); +var _eye = ::camera3d::camera3d_position(cam); +var _w = ::kernel::kernel_gui_width(); +var _h = ::kernel::kernel_gui_height(); +var _tan = tan(degtorad(cam.fov) / 2); + +for (var i = 0; i < array_length(things); i++) { + var _p = things[i].pos; + var _v = { x: _p.x - _eye.x, y: _p.y - _eye.y, z: _p.z - _eye.z }; + var _z = _v.x * _b.forward.x + _v.y * _b.forward.y + _v.z * _b.forward.z; + if (_z <= 1) continue; // behind the camera + var _rx = _v.x * _b.right.x + _v.y * _b.right.y + _v.z * _b.right.z; + var _ry = _v.x * _b.up.x + _v.y * _b.up.y + _v.z * _b.up.z; + + var _sx = _w / 2 + (_rx / (_z * _tan * (_w / _h))) * (_w / 2); + var _sy = _h / 2 - (_ry / (_z * _tan)) * (_h / 2); + draw_set_colour(i == picked ? c_yellow : c_aqua); + draw_circle(_sx, _sy, (things[i].r / (_z * _tan)) * (_h / 2), true); +} +draw_set_colour(c_white); +draw_text(16, 16, "right-drag to orbit, wheel to zoom"); diff --git a/prefabs/camera3d_basics/objects/obj_orbit_scene/Step.gml b/prefabs/camera3d_basics/objects/obj_orbit_scene/Step.gml new file mode 100644 index 00000000..a8e67a87 --- /dev/null +++ b/prefabs/camera3d_basics/objects/obj_orbit_scene/Step.gml @@ -0,0 +1,24 @@ +var _cfg = camera3d_basics_tuning(); +var _p = ::kernel::kernel_pointer(); + +if (mouse_check_button_pressed(mb_right)) { dragging = true; last = _p; } +if (!mouse_check_button(mb_right)) dragging = false; +if (dragging) { + cam = ::camera3d::camera3d_orbit(cam, _p.x - last.x, _p.y - last.y); + last = _p; +} + +cam.distance = clamp( + cam.distance - (mouse_wheel_up() - mouse_wheel_down()) * 40, + _cfg.min_distance, _cfg.max_distance); + +// The ray is built from the same basis the draw below projects with, so +// what you click is what you see. +var _ray = ::camera3d::camera3d_ray(cam, _p.x, _p.y, + ::kernel::kernel_gui_width(), ::kernel::kernel_gui_height()); +var _best = -1; +picked = -1; +for (var i = 0; i < array_length(things); i++) { + var _t = ::camera3d::camera3d_hit_sphere(_ray, things[i].pos, things[i].r); + if (_t >= 0 && (_best < 0 || _t < _best)) { _best = _t; picked = i; } +} diff --git a/prefabs/camera3d_basics/objects/obj_orbit_scene/object.toml b/prefabs/camera3d_basics/objects/obj_orbit_scene/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/camera3d_basics/objects/obj_orbit_scene/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/camera3d_basics/prefab.toml b/prefabs/camera3d_basics/prefab.toml new file mode 100644 index 00000000..022c3d41 --- /dev/null +++ b/prefabs/camera3d_basics/prefab.toml @@ -0,0 +1,25 @@ +schema_version = 1 +name = "3D orbit camera" +description = "A trackball camera you drag to orbit, with mouse picking that agrees with what you see." +category = "component" +tags = ["capability:camera", "role:template"] + +install = "copy" +requires = ["camera3d", "kernel"] + +seam = "objects/obj_orbit_scene/Step.gml" + +steps = [ + "Place obj_orbit_scene on an instance layer in your room.", + "Replace the sphere list with your own scene, and submit your vertex buffers in Draw using ::camera3d::camera3d_position and camera3d_basis to build the view matrix.", + "If picking ever disagrees with the picture, test OFF-CENTRE: a wrong cross-product operand order has a zero coefficient at screen centre, so it looks perfect there and fails everywhere else.", +] + +[roles] +obj_orbit_scene = "holds the camera and the scene: right-drag orbits, the wheel dollies, and a click picks a thing by ray" + +[tuning] +fov = 60 +distance = 300 +min_distance = 120 +max_distance = 900 diff --git a/prefabs/camera3d_basics/thumbnail.png b/prefabs/camera3d_basics/thumbnail.png new file mode 100644 index 00000000..f2a2d135 Binary files /dev/null and b/prefabs/camera3d_basics/thumbnail.png differ diff --git a/prefabs/custom_mouse_pointer/LICENSE b/prefabs/custom_mouse_pointer/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/custom_mouse_pointer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/custom_mouse_pointer/README.md b/prefabs/custom_mouse_pointer/README.md new file mode 100644 index 00000000..cc318eee --- /dev/null +++ b/prefabs/custom_mouse_pointer/README.md @@ -0,0 +1,25 @@ +# custom_mouse_pointer + +Replace the OS cursor with something you draw in GUI space. + +## The decision this makes for you + +It draws in **Draw_GUI**, which is screen space. That is right for a +cursor and wrong for an in-world reticle: `mouse_x`/`mouse_y` are *room* +coordinates, so the moment your room has a camera that moves, anything +drawn from them drifts away from the pointer. If you want a reticle that +sits in the world, move the drawing to `Draw` and use `mouse_x`/`mouse_y` +directly — but then it scrolls with the room, which is usually not what a +cursor should do. + +## Swapping in a sprite + +``` +gmx prefab add incidentals_fantasy/sprites/spr_hand --centre-origin +``` + +`--centre-origin` matters: catalog sprites usually ship `top_left`, and a +cursor whose hotspot is its corner feels subtly wrong to click with. +Replace the `draw_primitive` block with `draw_sprite_ext`, deriving the +scale from `sprite_get_width` rather than hard-coding it so the sprite can +be swapped without retuning. diff --git a/prefabs/custom_mouse_pointer/objects/obj_pointer/Create.gml b/prefabs/custom_mouse_pointer/objects/obj_pointer/Create.gml new file mode 100644 index 00000000..a48a07f8 --- /dev/null +++ b/prefabs/custom_mouse_pointer/objects/obj_pointer/Create.gml @@ -0,0 +1,9 @@ +// Lower depth draws on top. This is an instance variable, so it wins over +// whatever depth the layer sets. +depth = -9999; + +// Hide the OS cursor. Do this here rather than in a boot object so the +// object is self-contained and can be dropped into any project. +window_set_cursor(cr_none); + +size = custom_mouse_pointer_tuning().size_px; diff --git a/prefabs/custom_mouse_pointer/objects/obj_pointer/Draw_GUI.gml b/prefabs/custom_mouse_pointer/objects/obj_pointer/Draw_GUI.gml new file mode 100644 index 00000000..e7fc7a92 --- /dev/null +++ b/prefabs/custom_mouse_pointer/objects/obj_pointer/Draw_GUI.gml @@ -0,0 +1,20 @@ +// Draw_GUI is screen space, so the GUI mouse coordinates are the right +// ones. mouse_x/mouse_y are ROOM coordinates and would drift away from +// the pointer as soon as the room has a camera that moves. +var _x = device_mouse_x_to_gui(0); +var _y = device_mouse_y_to_gui(0); + +// A plain arrow, so the recipe works with no assets. Swap this whole +// block for draw_sprite_ext once you have a cursor sprite. +draw_set_colour(c_white); +draw_primitive_begin(pr_trianglelist); +draw_vertex(_x, _y); +draw_vertex(_x, _y + size); +draw_vertex(_x + size * 0.62, _y + size * 0.72); +draw_primitive_end(); + +draw_set_colour(c_black); +draw_line(_x, _y, _x, _y + size); +draw_line(_x, _y, _x + size * 0.62, _y + size * 0.72); +draw_line(_x, _y + size, _x + size * 0.62, _y + size * 0.72); +draw_set_colour(c_white); diff --git a/prefabs/custom_mouse_pointer/objects/obj_pointer/object.toml b/prefabs/custom_mouse_pointer/objects/obj_pointer/object.toml new file mode 100644 index 00000000..cbb87f2b --- /dev/null +++ b/prefabs/custom_mouse_pointer/objects/obj_pointer/object.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#object + +# Persistent so the cursor survives a room change; without this every +# room transition drops you back to the OS pointer for a frame. +persistent = true diff --git a/prefabs/custom_mouse_pointer/prefab.toml b/prefabs/custom_mouse_pointer/prefab.toml new file mode 100644 index 00000000..c121e456 --- /dev/null +++ b/prefabs/custom_mouse_pointer/prefab.toml @@ -0,0 +1,26 @@ +schema_version = 1 +name = "Custom mouse pointer" +description = "Replace the OS cursor with something you draw in GUI space." +category = "component" +tags = ["capability:ui", "role:template"] + +# An addition: it has no project of its own, it joins yours. +install = "copy" +requires = [] + +seam = "objects/obj_pointer/Draw_GUI.gml" + +steps = [ + "Place obj_pointer on an instance layer in your first room. It is persistent, so one placement covers every room after it.", + "To use a sprite instead of the drawn arrow: `gmx prefab add incidentals_fantasy/sprites/spr_hand --centre-origin`, then swap the draw_primitive block in Draw_GUI.gml for draw_sprite_ext.", +] + +traps = [ + "Draw the cursor from `device_mouse_x_to_gui` / `device_mouse_y_to_gui`, not `mouse_x`/`mouse_y`. The latter are ROOM coordinates: with a static camera they agree, so the bug is invisible until the room scrolls, and then the drawn cursor drifts away from the real pointer.", +] + +[roles] +obj_pointer = "hides the OS cursor and draws the replacement in GUI space, at depth -9999 so nothing covers it" + +[tuning] +size_px = 26 diff --git a/prefabs/custom_mouse_pointer/thumbnail.png b/prefabs/custom_mouse_pointer/thumbnail.png new file mode 100644 index 00000000..970d597c Binary files /dev/null and b/prefabs/custom_mouse_pointer/thumbnail.png differ diff --git a/prefabs/dialogue/LICENSE b/prefabs/dialogue/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/dialogue/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/dialogue/README.md b/prefabs/dialogue/README.md new file mode 100644 index 00000000..9f776e85 --- /dev/null +++ b/prefabs/dialogue/README.md @@ -0,0 +1,3 @@ +# dialogue + +Someone says something, text appears, you press a key. diff --git a/prefabs/dialogue/objects/obj_demo/Alarm_0.gml b/prefabs/dialogue/objects/obj_demo/Alarm_0.gml new file mode 100644 index 00000000..5311a301 --- /dev/null +++ b/prefabs/dialogue/objects/obj_demo/Alarm_0.gml @@ -0,0 +1 @@ +dialogue_restart(chat); diff --git a/prefabs/dialogue/objects/obj_demo/Create.gml b/prefabs/dialogue/objects/obj_demo/Create.gml new file mode 100644 index 00000000..1b6c504b --- /dev/null +++ b/prefabs/dialogue/objects/obj_demo/Create.gml @@ -0,0 +1,6 @@ +chat = dialogue_make([ + { who: "Innkeeper", text: "You look like you have walked a long way." }, + { who: "Innkeeper", text: "There is a room upstairs if you want it. No charge, but the roof leaks." }, + { who: "You", text: "I have slept under worse." }, + { who: "Innkeeper", text: "Most have. Take the one at the end." }, +], 40); diff --git a/prefabs/dialogue/objects/obj_demo/Draw_GUI.gml b/prefabs/dialogue/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..66ecc805 --- /dev/null +++ b/prefabs/dialogue/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,5 @@ +draw_text(40, 30, "dialogue demo — SPACE completes then advances, ENTER restarts"); +draw_text(40, 60, dialogue_active(chat) + ? "talking (your own input should be gated on this)" + : "finished — looping in a moment"); +dialogue_draw_box(chat, 60, 340, 840, 130); diff --git a/prefabs/dialogue/objects/obj_demo/Step.gml b/prefabs/dialogue/objects/obj_demo/Step.gml new file mode 100644 index 00000000..4a807249 --- /dev/null +++ b/prefabs/dialogue/objects/obj_demo/Step.gml @@ -0,0 +1,7 @@ +var _press = keyboard_check_pressed(vk_space); +if (dialogue_step(chat, _press)) { + // Ended this frame. A conversation that can only happen once is one + // you cannot test, so it loops. + alarm[0] = 60; +} +if (keyboard_check_pressed(vk_enter)) dialogue_restart(chat); diff --git a/prefabs/dialogue/objects/obj_demo/object.toml b/prefabs/dialogue/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/dialogue/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/dialogue/prefab.toml b/prefabs/dialogue/prefab.toml new file mode 100644 index 00000000..2c2c7686 --- /dev/null +++ b/prefabs/dialogue/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Conversations" + +# A library: installed under `prefabs//` in the target project and +# called as `::dialogue::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Lines with a speaker, revealed at a characters-per-second rate, advanced by a key. The first press completes the line and the second advances it, so an impatient player never skips text unread. Includes a drawn box and an active flag to gate the game's own input on, or the player walks around while talking." +category = "library" +tags = ["capability:dialogue", "capability:ui", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "Gate your own input on `dialogue_active`, or the player moves while the box is up.", +] diff --git a/prefabs/dialogue/project.toml b/prefabs/dialogue/project.toml new file mode 100644 index 00000000..ac119248 --- /dev/null +++ b/prefabs/dialogue/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "dialogue" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/dialogue/rooms/rm_demo/layers/Background.toml b/prefabs/dialogue/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/dialogue/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/dialogue/rooms/rm_demo/layers/Instances.toml b/prefabs/dialogue/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/dialogue/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/dialogue/rooms/rm_demo/room.toml b/prefabs/dialogue/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/dialogue/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/dialogue/scripts/scr_dialogue/scr_dialogue.gml b/prefabs/dialogue/scripts/scr_dialogue/scr_dialogue.gml new file mode 100644 index 00000000..56c51316 --- /dev/null +++ b/prefabs/dialogue/scripts/scr_dialogue/scr_dialogue.gml @@ -0,0 +1,63 @@ +/// Someone says something, and you press a key. +/// +/// The typing effect is not decoration: it gives the player a reason for the +/// box to stay up, and it gives the first key press something to do other +/// than skip the line unread. So the first press completes the line and the +/// second advances — anything else loses text to a double tap. + +/// @function dialogue_make(lines, chars_per_second) +/// @description A conversation from an array of `{who, text}` structs, +/// revealed at `chars_per_second`. Pass 0 to show each line whole. +function dialogue_make(lines, chars_per_second) { + return { + lines: lines, + at: 0, + shown: 0, + rate: chars_per_second, + done: array_length(lines) == 0, + }; +} + +/// @function dialogue_step(state, advance) +/// @description Advance one frame. `advance` is the key press. Returns true +/// on the single frame the conversation ends. +/// +/// A press with text still appearing completes the line instead of skipping +/// it, so an impatient player reads everything. +function dialogue_step(state, advance) { + if (state.done) return false; + + var _text = dialogue_text(state); + var _full = string_length(_text); + if (state.rate > 0 && state.shown < _full) { + state.shown = min(_full, state.shown + state.rate * delta_time / 1000000); + if (advance) state.shown = _full; + return false; + } + state.shown = _full; + if (!advance) return false; + + state.at += 1; + state.shown = 0; + if (state.at >= array_length(state.lines)) { + state.done = true; + return true; + } + return false; +} + +/// @function dialogue_restart(state) +/// @description Back to the first line. A conversation that can only happen +/// once is a conversation you cannot test. +function dialogue_restart(state) { + state.at = 0; + state.shown = 0; + state.done = array_length(state.lines) == 0; +} +/// @function dialogue_active(state) +/// @description Whether the box should be on screen. Gate the game's own +/// input on this, or the player walks around while talking. +function dialogue_active(state) { + return !state.done; +} + diff --git a/prefabs/dialogue/scripts/scr_dialogue_draw/scr_dialogue_draw.gml b/prefabs/dialogue/scripts/scr_dialogue_draw/scr_dialogue_draw.gml new file mode 100644 index 00000000..245e4820 --- /dev/null +++ b/prefabs/dialogue/scripts/scr_dialogue_draw/scr_dialogue_draw.gml @@ -0,0 +1,18 @@ +/// @function dialogue_draw_box(state, x, y, width, height) +/// @description A panel with the speaker above it and the revealed text +/// wrapped inside. Draw it in a GUI event so it does not scroll with the room. +function dialogue_draw_box(state, x, y, width, height) { + if (state.done) return; + draw_set_colour(c_black); + draw_set_alpha(0.8); + draw_rectangle(x, y, x + width, y + height, false); + draw_set_alpha(1); + draw_set_colour(c_white); + draw_rectangle(x, y, x + width, y + height, true); + + var _who = dialogue_speaker(state); + if (_who != "") draw_text(x + 12, y - 22, _who); + draw_text_ext(x + 12, y + 12, dialogue_visible(state), 22, width - 24); + if (dialogue_complete(state)) draw_text(x + width - 24, y + height - 24, ">"); +} + diff --git a/prefabs/dialogue/scripts/scr_dialogue_read/scr_dialogue_read.gml b/prefabs/dialogue/scripts/scr_dialogue_read/scr_dialogue_read.gml new file mode 100644 index 00000000..16dff502 --- /dev/null +++ b/prefabs/dialogue/scripts/scr_dialogue_read/scr_dialogue_read.gml @@ -0,0 +1,28 @@ +/// @function dialogue_speaker(state) +function dialogue_speaker(state) { + if (state.done || state.at >= array_length(state.lines)) return ""; + var _l = state.lines[state.at]; + return variable_struct_exists(_l, "who") ? _l.who : ""; +} + +/// @function dialogue_text(state) +/// @description The current line in full, however much of it is visible. +function dialogue_text(state) { + if (state.done || state.at >= array_length(state.lines)) return ""; + return state.lines[state.at].text; +} + +/// @function dialogue_visible(state) +/// @description As much of the current line as has been revealed — what to +/// actually draw. +function dialogue_visible(state) { + return string_copy(dialogue_text(state), 1, floor(state.shown)); +} + +/// @function dialogue_complete(state) +/// @description Whether the current line is fully revealed, so a caller can +/// show a "press for more" prompt only once there is more to press for. +function dialogue_complete(state) { + return state.shown >= string_length(dialogue_text(state)); +} + diff --git a/prefabs/dialogue/thumbnail.png b/prefabs/dialogue/thumbnail.png new file mode 100644 index 00000000..ecc2d981 Binary files /dev/null and b/prefabs/dialogue/thumbnail.png differ diff --git a/prefabs/economy/LICENSE b/prefabs/economy/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/economy/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/economy/README.md b/prefabs/economy/README.md new file mode 100644 index 00000000..30f5161f --- /dev/null +++ b/prefabs/economy/README.md @@ -0,0 +1,3 @@ +# economy + +Earn it, spend it only if it is there, and show it readably. diff --git a/prefabs/economy/objects/obj_demo/Create.gml b/prefabs/economy/objects/obj_demo/Create.gml new file mode 100644 index 00000000..7c8cabb8 --- /dev/null +++ b/prefabs/economy/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +purse = economy_make(500); +level = 0; +note = ""; diff --git a/prefabs/economy/objects/obj_demo/Draw_GUI.gml b/prefabs/economy/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..b11ca199 --- /dev/null +++ b/prefabs/economy/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,9 @@ +draw_text(40, 30, "economy demo — SPACE earns 250, ENTER buys, BACKSPACE resets"); +draw_text(40, 80, "balance " + economy_format(economy_balance(purse))); +draw_text(40, 105, "level " + string(level)); +draw_text(40, 130, "next " + economy_format(economy_price(50, level, 1.15))); +draw_text(40, 155, "affordable now: " + + string(economy_afford_count(purse, 50, level, 1.15))); +draw_text(40, 200, note); +draw_text(40, 240, "earned " + economy_format(purse.earned) + + " spent " + economy_format(purse.spent)); diff --git a/prefabs/economy/objects/obj_demo/Step.gml b/prefabs/economy/objects/obj_demo/Step.gml new file mode 100644 index 00000000..63584333 --- /dev/null +++ b/prefabs/economy/objects/obj_demo/Step.gml @@ -0,0 +1,15 @@ +if (keyboard_check_pressed(vk_space)) economy_earn(purse, 250); + +if (keyboard_check_pressed(vk_enter)) { + // Check-and-charge in one step, so the grant only happens on a true. + var _cost = economy_price(50, level, 1.15); + note = economy_spend(purse, _cost) + ? "bought level " + string(level + 1) + " for " + economy_format(_cost) + : "cannot afford " + economy_format(_cost); + if (string_pos("bought", note) == 1) level += 1; +} +if (keyboard_check_pressed(vk_backspace)) { + economy_reset(purse, 500); + level = 0; + note = "reset"; +} diff --git a/prefabs/economy/objects/obj_demo/object.toml b/prefabs/economy/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/economy/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/economy/prefab.toml b/prefabs/economy/prefab.toml new file mode 100644 index 00000000..83843039 --- /dev/null +++ b/prefabs/economy/prefab.toml @@ -0,0 +1,12 @@ +schema_version = 1 +name = "Currency and prices" + +# A library: installed under `prefabs//` in the target project and +# called as `::economy::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "A purse where spending checks affordability and deducts in one step, so a balance cannot go negative between the check and the charge. Plus a price that grows per purchase, how many more are affordable right now for a buy-max button, and thousands separators — a raw seven-digit number is unreadable at exactly the moment a player looks at it." +category = "library" +tags = ["capability:economy", "capability:currency", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/economy/project.toml b/prefabs/economy/project.toml new file mode 100644 index 00000000..83f626c9 --- /dev/null +++ b/prefabs/economy/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "economy" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/economy/rooms/rm_demo/layers/Background.toml b/prefabs/economy/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/economy/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/economy/rooms/rm_demo/layers/Instances.toml b/prefabs/economy/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/economy/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/economy/rooms/rm_demo/room.toml b/prefabs/economy/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/economy/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/economy/scripts/scr_economy/scr_economy.gml b/prefabs/economy/scripts/scr_economy/scr_economy.gml new file mode 100644 index 00000000..80576e18 --- /dev/null +++ b/prefabs/economy/scripts/scr_economy/scr_economy.gml @@ -0,0 +1,52 @@ +/// Currency, prices and whether you can afford it. +/// +/// `economy_spend` returns whether it went through and only deducts when it +/// did. Checking affordability and deducting as two separate steps is how a +/// balance goes negative — the check passes, something else spends, and the +/// deduction happens anyway. + +/// @function economy_make(starting) +/// @description A purse holding `starting` currency, with nothing spent yet. +function economy_make(starting) { + return { balance: starting, earned: starting, spent: 0 }; +} + +/// @function economy_balance(purse) +function economy_balance(purse) { + return purse.balance; +} + +/// @function economy_earn(purse, amount) +/// @description Add to the balance. Negative amounts are ignored rather than +/// quietly acting as a spend that skips the affordability check. +function economy_earn(purse, amount) { + if (amount <= 0) return 0; + purse.balance += amount; + purse.earned += amount; + return amount; +} + +/// @function economy_can_afford(purse, price) +function economy_can_afford(purse, price) { + return purse.balance >= price; +} + +/// @function economy_spend(purse, price) +/// @description Deduct `price` only if it is there. Returns whether the +/// purchase happened, so the thing bought is granted on a true and nothing +/// is granted on a false. +function economy_spend(purse, price) { + if (!economy_can_afford(purse, price)) return false; + purse.balance -= price; + purse.spent += price; + return true; +} + +/// @function economy_reset(purse, starting) +/// @description Back to `starting`, totals cleared. A new run, not a new +/// save — persistent currency should outlive this. +function economy_reset(purse, starting) { + purse.balance = starting; + purse.earned = starting; + purse.spent = 0; +} diff --git a/prefabs/economy/scripts/scr_economy_price/scr_economy_price.gml b/prefabs/economy/scripts/scr_economy_price/scr_economy_price.gml new file mode 100644 index 00000000..bec48ef2 --- /dev/null +++ b/prefabs/economy/scripts/scr_economy_price/scr_economy_price.gml @@ -0,0 +1,40 @@ +/// @function economy_price(base, level, growth) +/// @description What the next one costs when each purchase makes the next +/// dearer: `base * growth^level`, rounded. `growth` of 1.15 is the usual +/// shape — steep enough to matter by level ten, gentle enough at level two. +function economy_price(base, level, growth) { + return round(base * power(growth, level)); +} + +/// @function economy_afford_count(purse, base, level, growth) +/// @description How many more could be bought right now at a growing price, +/// spending nothing. What a "buy max" button needs. +function economy_afford_count(purse, base, level, growth) { + var _funds = purse.balance; + var _n = 0; + while (true) { + var _cost = economy_price(base, level + _n, growth); + if (_cost > _funds || _cost <= 0) break; + _funds -= _cost; + _n += 1; + } + return _n; +} + +/// @function economy_format(amount) +/// @description Thousands separated — 1234567 becomes "1,234,567". A raw +/// seven-digit number is unreadable at a glance, which is the only moment a +/// player ever looks at it. +function economy_format(amount) { + var _neg = amount < 0; + var _digits = string(abs(round(amount))); + var _out = ""; + var _n = string_length(_digits); + for (var i = 1; i <= _n; i++) { + _out += string_char_at(_digits, i); + var _left = _n - i; + if (_left > 0 && _left mod 3 == 0) _out += ","; + } + return (_neg ? "-" : "") + _out; +} + diff --git a/prefabs/economy/thumbnail.png b/prefabs/economy/thumbnail.png new file mode 100644 index 00000000..c9676b5c Binary files /dev/null and b/prefabs/economy/thumbnail.png differ diff --git a/prefabs/endless_runner/LICENSE b/prefabs/endless_runner/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/endless_runner/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/endless_runner/README.md b/prefabs/endless_runner/README.md new file mode 100644 index 00000000..837f6e85 --- /dev/null +++ b/prefabs/endless_runner/README.md @@ -0,0 +1,39 @@ +# endless_runner + +Jump the gaps on ground that never stops, and get further than last time. + +## The world moves, not the player + +The player stays around x=220 and the ground scrolls past. That is what makes +it endless: the room never grows, and there is no camera to follow anything. + +The corollary is that ground tiles must be **destroyed** once they leave the +left edge. Without it a two-minute run accumulates thousands of instances and +the frame rate falls away long after the mistake that caused it. + +## The opening stretch is always solid + +`safe_left` guarantees a screen and a bit of unbroken floor before any gap can +be generated. This is not politeness — a gap generated under the start drops +the player before they have pressed anything, and that reads as the game being +broken rather than as a hard game. It is worth checking every generator you +write for the equivalent. + +## Gaps are capped against the jump + +`gap_max_cells` exists so a generated gap is always clearable at the current +speed. A gap wider than the jump is unfair rather than hard, and the player +cannot tell the difference from the inside — they only know they died. Raise +`jump_strength` before raising the gap. + +## One column at a time + +The world is emitted a column at a time rather than in chunks. A gap that +straddles a chunk boundary is exactly the case that produces something +unjumpable, and generating per column means that case does not exist. + +## What it does not do + +No obstacles other than gaps, no pickups, no ceiling. Difficulty is +`scroll_gain` alone, which is enough to be going on with and not enough for a +finished game. diff --git a/prefabs/endless_runner/demo.gametest.json b/prefabs/endless_runner/demo.gametest.json new file mode 100644 index 00000000..c7c6c175 --- /dev/null +++ b/prefabs/endless_runner/demo.gametest.json @@ -0,0 +1,110 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 130, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_y0=undefined; global.gt_rose=false; global.gt_dist=0; global.gt_ended=false;" + }, + "watch": { + "gml": "if (room == rm_results) global.gt_ended = true; if (instance_exists(obj_player)) { with (obj_player) { global.gt_seen = true; if (global.gt_y0 == undefined && grounded) global.gt_y0 = y; if (global.gt_y0 != undefined && y < global.gt_y0 - 12) global.gt_rose = true; } } if (instance_exists(obj_runner)) global.gt_dist = max(global.gt_dist, obj_runner.travelled);" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "rose": { + "gml": "return global.gt_rose;" + }, + "scrolls": { + "gml": "return global.gt_dist > 0;" + }, + "alive": { + "gml": "return !global.gt_ended;" + } + }, + "timeline": [ + { + "frame": 40, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 48, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 120, + "assert": { + "id": "seen", + "name": "the runner is on the ground in rm_play", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "scrolls", + "name": "the world scrolls past \u2014 distance climbs", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "rose", + "name": "space lifts the runner off the ground", + "expect": true + } + }, + { + "frame": 60, + "assert": { + "id": "alive", + "name": "it does not fall at once \u2014 the ground and collision work", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 118, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/endless_runner/objects/obj_ground/Step.gml b/prefabs/endless_runner/objects/obj_ground/Step.gml new file mode 100644 index 00000000..3906533e --- /dev/null +++ b/prefabs/endless_runner/objects/obj_ground/Step.gml @@ -0,0 +1,7 @@ +var _ctl = instance_find(obj_runner, 0); +if (_ctl == noone || !::kernel::kernel_playing()) exit; + +// The world moves, not the player. Tiles are destroyed once past the left +// edge rather than accumulating for the length of the run. +x -= _ctl.scroll; +if (x < -sprite_width) instance_destroy(); diff --git a/prefabs/endless_runner/objects/obj_ground/object.toml b/prefabs/endless_runner/objects/obj_ground/object.toml new file mode 100644 index 00000000..209a33ca --- /dev/null +++ b/prefabs/endless_runner/objects/obj_ground/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_ground" diff --git a/prefabs/endless_runner/objects/obj_player/Create.gml b/prefabs/endless_runner/objects/obj_player/Create.gml new file mode 100644 index 00000000..a4220189 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_player/Create.gml @@ -0,0 +1,7 @@ +vy = 0; +grounded = false; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +gravity_per_frame = ::kernel::kernel_tuning("gravity_per_frame", 0.75); +jump_strength = ::kernel::kernel_tuning("jump_strength", 15); diff --git a/prefabs/endless_runner/objects/obj_player/Step.gml b/prefabs/endless_runner/objects/obj_player/Step.gml new file mode 100644 index 00000000..d5d66d20 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_player/Step.gml @@ -0,0 +1,26 @@ +if (!::kernel::kernel_playing()) exit; + +var _g = gravity_per_frame; +var _j = jump_strength; + +// Jump only from the ground; the buffer lives in ::motion:: for a +// platformer, but a runner has one button and no coyote time to speak of. +if (grounded && (keyboard_check_pressed(vk_space) || keyboard_check_pressed(vk_up))) { + vy = -_j; + grounded = false; + ::feel::feel_squash(0.4); +} + +vy += _g; +y += vy; + +// Land on whatever is directly below, one axis at a time. +grounded = false; +if (vy >= 0) { + var _below = instance_place(x, y + 1, obj_ground); + if (_below != noone) { + y = _below.bbox_top - sprite_height / 2; + vy = 0; + grounded = true; + } +} diff --git a/prefabs/endless_runner/objects/obj_player/object.toml b/prefabs/endless_runner/objects/obj_player/object.toml new file mode 100644 index 00000000..297e1266 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_player/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_player" diff --git a/prefabs/endless_runner/objects/obj_results/Draw_GUI.gml b/prefabs/endless_runner/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/endless_runner/objects/obj_results/Step.gml b/prefabs/endless_runner/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/endless_runner/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/endless_runner/objects/obj_results/object.toml b/prefabs/endless_runner/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/endless_runner/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/endless_runner/objects/obj_runner/Create.gml b/prefabs/endless_runner/objects/obj_runner/Create.gml new file mode 100644 index 00000000..9f2204f5 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_runner/Create.gml @@ -0,0 +1,22 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +cell = ::kernel::kernel_tuning("cell", 64); +scroll = ::kernel::kernel_tuning("scroll_speed", 6); +travelled = 0; +next_x = 0; +gap_left = 0; +// Columns of guaranteed floor before any gap may appear. +safe_left = ceil(room_width / cell) + 2; + +// A run of solid ground to start on, so the first jump is a choice rather +// than a reflex. +repeat (ceil(room_width / cell) + 4) runner_emit_column(); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +scroll_max = ::kernel::kernel_tuning("scroll_max", 14); +scroll_gain = ::kernel::kernel_tuning("scroll_gain", 0.0025); + +// Metres already banked as score, so the run does not score them twice. +scored_to = 0; diff --git a/prefabs/endless_runner/objects/obj_runner/Draw_GUI.gml b/prefabs/endless_runner/objects/obj_runner/Draw_GUI.gml new file mode 100644 index 00000000..1264bf92 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_runner/Draw_GUI.gml @@ -0,0 +1,5 @@ +::kernel::kernel_draw_text(16, 16, "distance " + string(runner_distance())); +::kernel::kernel_draw_text(16, 40, + "speed " + string_format(scroll, 2, 1)); +::kernel::kernel_draw_text(16, 64, + "best " + string(::kernel::kernel_save_get("high_score", 0))); diff --git a/prefabs/endless_runner/objects/obj_runner/Step.gml b/prefabs/endless_runner/objects/obj_runner/Step.gml new file mode 100644 index 00000000..4fbd59f7 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_runner/Step.gml @@ -0,0 +1,28 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); + +scroll = min(scroll_max, + scroll + scroll_gain); + +travelled += scroll; +// Score *is* distance here, so bank the metres gained this step. It used to +// add zero while the run banked `runner_distance()` under "high_score", and +// the results screen read "score 0 / best 137" — two units under labels that +// look like they should agree. +var _gained = floor(runner_distance()) - scored_to; +if (_gained > 0) { + ::kernel::kernel_score_add(_gained); + scored_to += _gained; +} +next_x -= scroll; +while (next_x < room_width + cell * 2) runner_emit_column(); + +// Falling off the bottom is the only ending. +// Looked up rather than cached from Create: the player is a placed room +// entry now, so drag it somewhere else and this still finds it. +var _player = instance_find(obj_player, 0); +if (_player != noone && _player.y > room_height + 80) { + ::kernel::kernel_game_over("fell"); + ::kernel::kernel_save_high_score(runner_distance()); + room_goto(rm_results); +} diff --git a/prefabs/endless_runner/objects/obj_runner/object.toml b/prefabs/endless_runner/objects/obj_runner/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/endless_runner/objects/obj_runner/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/endless_runner/objects/obj_title/Create.gml b/prefabs/endless_runner/objects/obj_title/Create.gml new file mode 100644 index 00000000..be37debf --- /dev/null +++ b/prefabs/endless_runner/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(endless_runner_data, endless_runner_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/endless_runner/objects/obj_title/Draw_GUI.gml b/prefabs/endless_runner/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..40eadcc5 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "RUNNER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "jump the gaps, the ground never stops", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/endless_runner/objects/obj_title/Step.gml b/prefabs/endless_runner/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/endless_runner/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/endless_runner/objects/obj_title/object.toml b/prefabs/endless_runner/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/endless_runner/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/endless_runner/prefab.toml b/prefabs/endless_runner/prefab.toml new file mode 100644 index 00000000..88ac8959 --- /dev/null +++ b/prefabs/endless_runner/prefab.toml @@ -0,0 +1,38 @@ +schema_version = 1 +name = "Endless runner" +description = "Jump the gaps on ground that never stops, and get further than last time." +category = "game" +tags = ["genre:runner", "role:template"] + +install = "copy" +requires = ["kernel", "feel"] + +seam = "scripts/scr_endless_runner_data/scr_endless_runner_data.gml" + +steps = [ + "`endless_runner.gametest.json` came with the prefab and already passes: the runner stands on the ground in rm_play, the world scrolls past so distance climbs, space lifts it off the ground, and it does not fall in the first four seconds. Run it with `gmx test endless_runner.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The world scrolls past a stationary player rather than the player moving through it — that is what keeps the run endless without the room growing without limit.", + "Ground tiles are destroyed once they leave the left edge. Without that, a two-minute run accumulates thousands of instances and the frame rate falls away long after the mistake.", + "`gap_max_cells` is capped against `jump_strength` on purpose: a gap wider than the jump can clear is unfair rather than hard.", +] + +[roles] +obj_runner = "scrolls the world, emits ground, counts distance, ends the run" +obj_player = "jumps; falling off the bottom ends the run" +obj_ground = "one tile of floor, destroyed once it leaves the screen" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "distance reached and the best so far" + +[tuning] +scroll_speed = 6 +scroll_gain = 0.0025 +scroll_max = 14 +gravity_per_frame = 0.75 +jump_strength = 15 +# The widest gap that can be generated. Beyond what the jump clears it is +# unfair rather than hard, so raise `jump_strength` before raising this. +gap_max_cells = 3 +gap_chance = 0.22 +cell = 64 +distance_per_point = 10 diff --git a/prefabs/endless_runner/project.toml b/prefabs/endless_runner/project.toml new file mode 100644 index 00000000..a0f1f190 --- /dev/null +++ b/prefabs/endless_runner/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "endless_runner" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/endless_runner/rooms/rm_play/layers/Background.toml b/prefabs/endless_runner/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/endless_runner/rooms/rm_play/layers/Instances.toml b/prefabs/endless_runner/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..eb821ab0 --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_runner_0" +object = "obj_runner" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_obj_player_0" +object = "obj_player" +x = 220.0 +y = 200.0 diff --git a/prefabs/endless_runner/rooms/rm_play/room.toml b/prefabs/endless_runner/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/endless_runner/rooms/rm_results/layers/Background.toml b/prefabs/endless_runner/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/endless_runner/rooms/rm_results/layers/Instances.toml b/prefabs/endless_runner/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/endless_runner/rooms/rm_results/room.toml b/prefabs/endless_runner/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/endless_runner/rooms/rm_title/layers/Background.toml b/prefabs/endless_runner/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/endless_runner/rooms/rm_title/layers/Instances.toml b/prefabs/endless_runner/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/endless_runner/rooms/rm_title/room.toml b/prefabs/endless_runner/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/endless_runner/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/endless_runner/scripts/scr_runner_world/scr_runner_world.gml b/prefabs/endless_runner/scripts/scr_runner_world/scr_runner_world.gml new file mode 100644 index 00000000..475104ed --- /dev/null +++ b/prefabs/endless_runner/scripts/scr_runner_world/scr_runner_world.gml @@ -0,0 +1,35 @@ +/// @function runner_emit_column() +/// @description Add one column of world at the right-hand edge — either a +/// floor tile or part of a gap. Runs from `obj_runner`, so `cell`, +/// `next_x` and `gap_left` are its variables. +/// +/// One column at a time rather than whole chunks, because a gap that +/// straddles a chunk boundary is the case that generates something +/// unjumpable. +function runner_emit_column() { + // The opening stretch is always solid. Generating a gap under the + // start drops the player before they have pressed anything, which + // reads as the game being broken rather than as a hard game. + if (safe_left > 0) { + safe_left -= 1; + instance_create_depth(next_x + cell / 2, 420, 0, obj_ground); + next_x += cell; + return; + } + if (gap_left > 0) { + gap_left -= 1; + } else if (random(1) < ::kernel::kernel_tuning("gap_chance", 0.22)) { + // Capped so the gap is always clearable at the current speed. + gap_left = irandom_range(1, ::kernel::kernel_tuning("gap_max_cells", 3)); + } else { + instance_create_depth(next_x + cell / 2, 420, 0, obj_ground); + } + next_x += cell; +} + +/// @function runner_distance() +/// @description How far the world has scrolled, in points rather than +/// pixels — the number worth showing. +function runner_distance() { + return floor(travelled / ::kernel::kernel_tuning("distance_per_point", 10)); +} diff --git a/prefabs/endless_runner/sprites/spr_ground/frame_000.png b/prefabs/endless_runner/sprites/spr_ground/frame_000.png new file mode 100644 index 00000000..31392b0b Binary files /dev/null and b/prefabs/endless_runner/sprites/spr_ground/frame_000.png differ diff --git a/prefabs/endless_runner/sprites/spr_ground/sprite.toml b/prefabs/endless_runner/sprites/spr_ground/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/endless_runner/sprites/spr_ground/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/endless_runner/sprites/spr_player/frame_000.png b/prefabs/endless_runner/sprites/spr_player/frame_000.png new file mode 100644 index 00000000..b9cc0d8c Binary files /dev/null and b/prefabs/endless_runner/sprites/spr_player/frame_000.png differ diff --git a/prefabs/endless_runner/sprites/spr_player/sprite.toml b/prefabs/endless_runner/sprites/spr_player/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/endless_runner/sprites/spr_player/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/endless_runner/thumbnail.png b/prefabs/endless_runner/thumbnail.png new file mode 100644 index 00000000..7eee25b8 Binary files /dev/null and b/prefabs/endless_runner/thumbnail.png differ diff --git a/prefabs/enemy_ai/LICENSE b/prefabs/enemy_ai/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/enemy_ai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/enemy_ai/README.md b/prefabs/enemy_ai/README.md new file mode 100644 index 00000000..f04502a6 --- /dev/null +++ b/prefabs/enemy_ai/README.md @@ -0,0 +1,3 @@ +# enemy_ai + +Chase, flee, patrol and wander — and notice you in the first place. diff --git a/prefabs/enemy_ai/objects/obj_demo/Create.gml b/prefabs/enemy_ai/objects/obj_demo/Create.gml new file mode 100644 index 00000000..97fd2aae --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo/Create.gml @@ -0,0 +1,9 @@ +// Three enemies, one of each behaviour, all watching the mouse. +var _kinds = ["wander", "patrol", "flee"]; +for (var i = 0; i < 3; i++) { + var _e = instance_create_depth(200 + i * 260, 400, 0, obj_demo_enemy); + _e.kind = _kinds[i]; + if (_kinds[i] == "patrol") { + ai_patrol_set(_e.ai, [[200 + i * 260, 400], [200 + i * 260, 180]]); + } +} diff --git a/prefabs/enemy_ai/objects/obj_demo/Draw_GUI.gml b/prefabs/enemy_ai/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..0198729c --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,2 @@ +draw_text(40, 30, "enemy-ai demo — move the mouse; red means it has seen you"); +draw_text(40, 50, "left wanders, middle patrols, right flees"); diff --git a/prefabs/enemy_ai/objects/obj_demo/Step.gml b/prefabs/enemy_ai/objects/obj_demo/Step.gml new file mode 100644 index 00000000..79efe917 --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo/Step.gml @@ -0,0 +1,7 @@ +// Once per enemy. `ai_separate` pushes the *calling* instance away from its +// peers, so running it from one instance separated that enemy from the pack +// and left every other pair fully overlapped. The library's own `_share = 0.5` +// is what stops the per-pair double-count this looks like it would cause. +with (obj_demo_enemy) { + ai_separate(ai, obj_demo_enemy, 36); +} diff --git a/prefabs/enemy_ai/objects/obj_demo/object.toml b/prefabs/enemy_ai/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/enemy_ai/objects/obj_demo_enemy/Create.gml b/prefabs/enemy_ai/objects/obj_demo_enemy/Create.gml new file mode 100644 index 00000000..7d1e73b2 --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo_enemy/Create.gml @@ -0,0 +1,5 @@ +// `kind` decides which behaviour runs when the mouse is out of range; +// everything in sight range chases, which is the switch worth seeing. +ai = ai_make(2, 220); +kind = "wander"; +tint = c_white; diff --git a/prefabs/enemy_ai/objects/obj_demo_enemy/Draw.gml b/prefabs/enemy_ai/objects/obj_demo_enemy/Draw.gml new file mode 100644 index 00000000..1579ed9b --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo_enemy/Draw.gml @@ -0,0 +1,4 @@ +draw_set_colour(tint); +draw_circle(x, y, 12, false); +draw_set_colour(c_white); +draw_circle(x, y, ai.sight, true); diff --git a/prefabs/enemy_ai/objects/obj_demo_enemy/Step.gml b/prefabs/enemy_ai/objects/obj_demo_enemy/Step.gml new file mode 100644 index 00000000..e093224a --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo_enemy/Step.gml @@ -0,0 +1,10 @@ +var _mx = mouse_x; +var _my = mouse_y; + +if (ai_sees(ai, _mx, _my)) { + if (kind == "flee") ai_flee(ai, _mx, _my); else ai_chase(ai, _mx, _my); + tint = c_red; +} else { + tint = c_white; + if (kind == "patrol") ai_patrol(ai); else ai_wander(ai, 1.2); +} diff --git a/prefabs/enemy_ai/objects/obj_demo_enemy/object.toml b/prefabs/enemy_ai/objects/obj_demo_enemy/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/enemy_ai/objects/obj_demo_enemy/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/enemy_ai/prefab.toml b/prefabs/enemy_ai/prefab.toml new file mode 100644 index 00000000..2fe61ee2 --- /dev/null +++ b/prefabs/enemy_ai/prefab.toml @@ -0,0 +1,17 @@ +schema_version = 1 +name = "Enemy behaviour" + +# A library: installed under `prefabs//` in the target project and +# called as `::enemy_ai::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "One state field and the movements that go with it: chase, flee, wander with a hold time, patrol a route, and a sight radius with an optional line-of-sight check so walls hide you. Includes separation, without which a group of chasers converges into a single stack that reads as one enemy and lands every hit at once." +category = "library" +tags = ["capability:ai", "capability:enemies", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_enemy", "rooms/rm_demo"] + +steps = [ + "These act on the calling instance, so they go in the enemy's own Step event.", + "`ai_separate` moves the *other* instances, so call it from one enemy, not from each — calling it in every enemy's Step doubles every push.", +] diff --git a/prefabs/enemy_ai/project.toml b/prefabs/enemy_ai/project.toml new file mode 100644 index 00000000..e3c618f3 --- /dev/null +++ b/prefabs/enemy_ai/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "enemy-ai" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/enemy_ai/rooms/rm_demo/layers/Background.toml b/prefabs/enemy_ai/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/enemy_ai/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/enemy_ai/rooms/rm_demo/layers/Instances.toml b/prefabs/enemy_ai/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/enemy_ai/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/enemy_ai/rooms/rm_demo/room.toml b/prefabs/enemy_ai/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/enemy_ai/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/enemy_ai/scripts/scr_enemy_ai/scr_enemy_ai.gml b/prefabs/enemy_ai/scripts/scr_enemy_ai/scr_enemy_ai.gml new file mode 100644 index 00000000..04844587 --- /dev/null +++ b/prefabs/enemy_ai/scripts/scr_enemy_ai/scr_enemy_ai.gml @@ -0,0 +1,37 @@ +/// What an enemy does when it can see you, and what it does when it cannot. +/// +/// One state field and a handful of movements, because the interesting part +/// of enemy behaviour is the switching, not the moving. Everything here acts +/// on the calling instance, so it goes in the enemy's own Step. + +/// @function ai_make(speed, sight) +/// @description Enemy state: how fast it moves and how far it can see. Starts +/// idle, with no waypoints. +function ai_make(speed, sight) { + return { + speed: speed, + sight: sight, + mode: "idle", + wander_dir: irandom(359), + wander_left: 0, + route: [], + leg: 0, + }; +} + +/// @function ai_sees(state, target_x, target_y) +/// @scope instance +/// @description True when the target is inside the sight radius. Distance +/// only — pass through `ai_has_line_of_sight` as well if walls should block. +function ai_sees(state, target_x, target_y) { + return point_distance(x, y, target_x, target_y) <= state.sight; +} + +/// @function ai_has_line_of_sight(target_x, target_y, obj_solid) +/// @scope instance +/// @description True when nothing solid stands between here and there. +function ai_has_line_of_sight(target_x, target_y, obj_solid) { + if (obj_solid == noone) return true; + return !collision_line(x, y, target_x, target_y, obj_solid, true, true); +} + diff --git a/prefabs/enemy_ai/scripts/scr_enemy_ai_move/scr_enemy_ai_move.gml b/prefabs/enemy_ai/scripts/scr_enemy_ai_move/scr_enemy_ai_move.gml new file mode 100644 index 00000000..1f33f40b --- /dev/null +++ b/prefabs/enemy_ai/scripts/scr_enemy_ai_move/scr_enemy_ai_move.gml @@ -0,0 +1,67 @@ +/// @function ai_chase(state, target_x, target_y) +/// @scope instance +/// @description Move straight at the target at the state's speed. +function ai_chase(state, target_x, target_y) { + ai_move_towards(state, target_x, target_y, 1); +} + +/// @function ai_flee(state, target_x, target_y) +/// @scope instance +/// @description Move directly away from the target instead. +function ai_flee(state, target_x, target_y) { + ai_move_towards(state, target_x, target_y, -1); +} + +/// @function ai_move_towards(state, target_x, target_y, sign) +/// @scope instance +/// @description The shared half of chase and flee. `sign` is 1 towards, -1 +/// away. Stops dead on arrival rather than jittering across the target, +/// which is what a bare `move_towards_point` does at close range. +function ai_move_towards(state, target_x, target_y, sign) { + var _dist = point_distance(x, y, target_x, target_y); + if (sign > 0 && _dist <= state.speed) { + x = target_x; + y = target_y; + return; + } + var _dir = point_direction(x, y, target_x, target_y) + (sign > 0 ? 0 : 180); + x += lengthdir_x(state.speed, _dir); + y += lengthdir_y(state.speed, _dir); +} + +/// @function ai_wander(state, hold_seconds) +/// @scope instance +/// @description Drift, changing direction every `hold_seconds` and turning +/// back at the room edge. What an enemy does before it has noticed anything. +function ai_wander(state, hold_seconds) { + state.wander_left -= delta_time / 1000000; + if (state.wander_left <= 0) { + state.wander_left = hold_seconds; + state.wander_dir = irandom(359); + } + var _nx = x + lengthdir_x(state.speed, state.wander_dir); + var _ny = y + lengthdir_y(state.speed, state.wander_dir); + if (_nx < 0 || _nx > room_width) state.wander_dir = 180 - state.wander_dir; + if (_ny < 0 || _ny > room_height) state.wander_dir = -state.wander_dir; + x = clamp(_nx, 0, room_width); + y = clamp(_ny, 0, room_height); +} + +/// @function ai_separate(state, obj_peers, spacing) +/// @scope instance +/// @description Push away from other `obj_peers` that are closer than +/// `spacing`. Without it a group of chasers converges into a single stack +/// that reads as one enemy and lands several hits at once. +function ai_separate(state, obj_peers, spacing) { + // Half the overlap each, because both instances get pushed — a full + // correction per pair doubles the force and the group jitters. + var _share = 0.5; + with (obj_peers) { + if (id == other.id) continue; + var _d = point_distance(x, y, other.x, other.y); + if (_d >= spacing || _d == 0) continue; + var _push = point_direction(x, y, other.x, other.y); + other.x += lengthdir_x((spacing - _d) * _share, _push); + other.y += lengthdir_y((spacing - _d) * _share, _push); + } +} diff --git a/prefabs/enemy_ai/scripts/scr_enemy_ai_patrol/scr_enemy_ai_patrol.gml b/prefabs/enemy_ai/scripts/scr_enemy_ai_patrol/scr_enemy_ai_patrol.gml new file mode 100644 index 00000000..625f72fa --- /dev/null +++ b/prefabs/enemy_ai/scripts/scr_enemy_ai_patrol/scr_enemy_ai_patrol.gml @@ -0,0 +1,22 @@ +/// @function ai_patrol_set(state, points) +/// @description Give it a route: an array of `[x, y]` pairs, walked in order +/// and looped. +function ai_patrol_set(state, points) { + state.route = points; + state.leg = 0; +} + +/// @function ai_patrol(state) +/// @scope instance +/// @description Walk the route one frame. Advances to the next leg on +/// arrival and wraps at the end. Does nothing without a route. +function ai_patrol(state) { + var _n = array_length(state.route); + if (_n == 0) return; + var _p = state.route[state.leg mod _n]; + ai_move_towards(state, _p[0], _p[1], 1); + if (point_distance(x, y, _p[0], _p[1]) < 1) { + state.leg = (state.leg + 1) mod _n; + } +} + diff --git a/prefabs/enemy_ai/thumbnail.png b/prefabs/enemy_ai/thumbnail.png new file mode 100644 index 00000000..a5f5d409 Binary files /dev/null and b/prefabs/enemy_ai/thumbnail.png differ diff --git a/prefabs/falling_block/LICENSE b/prefabs/falling_block/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/falling_block/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/falling_block/README.md b/prefabs/falling_block/README.md new file mode 100644 index 00000000..aac0fffe --- /dev/null +++ b/prefabs/falling_block/README.md @@ -0,0 +1,26 @@ +# falling_block + +A whole falling-block game — seven pieces, wall kicks, line clears, speed ramp. + +## What you change first + +**The pieces.** All seven are data in `prefab.toml` — a colour and a list +of cells each, not seven branches of code. Add an eighth by adding a row; +rotation is computed about the piece centre, so an asymmetric piece needs +no special case. + +**The feel.** `fall_interval`, `speed_ramp`, `soft_drop` and +`points_per_row` are tuning knobs. Clearing several rows at once scores +the row count squared, which is what makes going for four worth the risk. + +## What you do not have to maintain + +The board — cell/pixel conversion, neighbour queries, full-row detection, +collapse, and snapshot/restore for undo — is `::grid::`. Score and combo +are `::kernel::`. Hit-stop, shake and the popups are `::feel::`. + +## The one part worth reading before you change it + +Rows are collapsed **bottom-up**. Removing an upper row first shifts the +indices of the ones still to remove, and a four-line clear silently +becomes a three-line clear. diff --git a/prefabs/falling_block/demo.gametest.json b/prefabs/falling_block/demo.gametest.json new file mode 100644 index 00000000..cc40dbc5 --- /dev/null +++ b/prefabs/falling_block/demo.gametest.json @@ -0,0 +1,121 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 130, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_stack); global.gt_seen=false; global.gt_px0=undefined; global.gt_shifted=false; global.gt_fell=false; global.gt_py0=undefined; global.gt_rot0=undefined; global.gt_rotated=false; global.gt_pieces=0;" + }, + "watch": { + "gml": "if (instance_exists(obj_stack)) { with (obj_stack) { global.gt_seen = true; global.gt_pieces = array_length(pieces); if (global.gt_px0 == undefined) global.gt_px0 = px; if (px < global.gt_px0) global.gt_shifted = true; if (global.gt_py0 == undefined) global.gt_py0 = py; if (py > global.gt_py0) global.gt_fell = true; if (global.gt_rot0 == undefined) global.gt_rot0 = rot; if (rot != global.gt_rot0) global.gt_rotated = true; } }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "pieces": { + "gml": "return global.gt_pieces > 0;" + }, + "falls": { + "gml": "return global.gt_fell;" + }, + "shifts": { + "gml": "return global.gt_shifted;" + }, + "rotates": { + "gml": "return global.gt_rotated;" + } + }, + "timeline": [ + { + "frame": 30, + "inputs": [ + { + "key": "vk_left", + "state": "down" + } + ] + }, + { + "frame": 33, + "inputs": [ + { + "key": "vk_left", + "state": "up" + } + ] + }, + { + "frame": 60, + "inputs": [ + { + "key": "vk_up", + "state": "down" + } + ] + }, + { + "frame": 63, + "inputs": [ + { + "key": "vk_up", + "state": "up" + } + ] + }, + { + "frame": 120, + "assert": { + "id": "seen", + "name": "the well runs in rm_stack", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "pieces", + "name": "the piece table is loaded", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "falls", + "name": "the active piece falls on its own", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "shifts", + "name": "left moves the piece", + "expect": true + } + }, + { + "frame": 120, + "assert": { + "id": "rotates", + "name": "up rotates the piece", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 118, + "code": [ + "watch" + ] + } + ] +} diff --git a/prefabs/falling_block/objects/obj_stack/Create.gml b/prefabs/falling_block/objects/obj_stack/Create.gml new file mode 100644 index 00000000..da754477 --- /dev/null +++ b/prefabs/falling_block/objects/obj_stack/Create.gml @@ -0,0 +1,113 @@ +::kernel::kernel_boot(); +::kernel::kernel_data_source(falling_block_data, falling_block_tuning()); + +pieces = ::kernel::kernel_data("pieces"); + +var _cols = ::kernel::kernel_tuning("cols", 10); +var _rows = ::kernel::kernel_tuning("rows", 18); +cell = ::kernel::kernel_tuning("cell", 26); + +// Centre the well; the HUD lives in the margin either side. +var _ox = (::kernel::kernel_gui_width() - _cols * cell) / 2; +board = ::grid::grid_make(_cols, _rows, cell, cell, _ox, 40); + +// Cell values are 1-based piece indices so 0 stays "empty" — which is +// what lets grid_empty and grid_row_full work without knowing what a +// piece is. +piece = 0; +rot = []; +px = 0; +py = 0; +fall_timer = 0; +rows_cleared = 0; +flash_rows = []; +flash_left = 0; + +/// Cells of the active piece at rotation `r`, offset to (`ax`, `ay`). +piece_cells = function(index, r, ax, ay) { + var _base = pieces[index].cells; + var _out = []; + for (var i = 0; i < array_length(_base); i++) { + var _cx = _base[i][0]; + var _cy = _base[i][1]; + // Rotate about the 4x4 centre (1.5, 1.5) so every piece turns in + // place; doing it about the origin walks the piece off the well. + repeat (r) { + var _nx = 1.5 + (1.5 - _cy); + var _ny = 1.5 + (_cx - 1.5); + _cx = _nx; + _cy = _ny; + } + array_push(_out, [ax + round(_cx), ay + round(_cy)]); + } + return _out; +}; + +/// Would the piece fit at this position and rotation? +fits = function(index, r, ax, ay) { + var _cells = piece_cells(index, r, ax, ay); + for (var i = 0; i < array_length(_cells); i++) { + var _c = _cells[i]; + if (!::grid::grid_in_bounds(board, _c[0], _c[1])) return false; + if (!::grid::grid_empty(board, _c[0], _c[1])) return false; + } + return true; +}; + +spawn = function() { + piece = irandom(array_length(pieces) - 1); + rot = 0; + px = (board.cols div 2) - 2; + py = 0; + // No room for the new piece means the stack reached the top. + if (!fits(piece, rot, px, py)) ::kernel::kernel_game_over("stack topped out"); +}; + +/// Freeze the piece into the board, score any full rows, spawn the next. +lock_piece = function() { + var _cells = piece_cells(piece, rot, px, py); + for (var i = 0; i < array_length(_cells); i++) { + ::grid::grid_set(board, _cells[i][0], _cells[i][1], piece + 1); + } + ::feel::feel_hitstop(0.04); + + var _full = ::grid::grid_full_rows(board); + if (array_length(_full) > 0) { + flash_rows = _full; + flash_left = 0.18; + rows_cleared += array_length(_full); + + // Clearing several rows at once is worth more than clearing them + // one at a time — squared, so four is decisively better than four ones. + var _n = array_length(_full); + ::kernel::kernel_combo_bump(); + var _gained = ::kernel::kernel_score_add( + ::kernel::kernel_tuning("points_per_row", 100) * _n * _n); + ::feel::feel_pop(::kernel::kernel_gui_width() / 2, 200, + (_n > 1 ? string(_n) + " ROWS +" : "+") + string(_gained), c_yellow); + ::feel::feel_shake(0.18 + 0.06 * _n, 4 + 3 * _n); + + // Collapse from the bottom up: removing an upper row first would + // shift the indices of the ones still to remove. + for (var i = array_length(_full) - 1; i >= 0; i--) { + ::grid::grid_collapse_row(board, _full[i]); + } + } else { + ::kernel::kernel_combo_break(); + } + spawn(); +}; + +/// Gravity gets faster with every row cleared, down to a floor. +fall_interval = function() { + var _base = ::kernel::kernel_tuning("fall_interval", 0.65); + var _ramp = ::kernel::kernel_tuning("speed_ramp", 0.04); + return max(::kernel::kernel_tuning("fall_floor", 0.12), _base - rows_cleared * _ramp); +}; + +spawn(); +::kernel::kernel_state_set(::kernel::kernel_states().play); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +soft_drop = ::kernel::kernel_tuning("soft_drop", 0.04); diff --git a/prefabs/falling_block/objects/obj_stack/Draw_GUI.gml b/prefabs/falling_block/objects/obj_stack/Draw_GUI.gml new file mode 100644 index 00000000..756e1b4e --- /dev/null +++ b/prefabs/falling_block/objects/obj_stack/Draw_GUI.gml @@ -0,0 +1,63 @@ +var _w = ::kernel::kernel_gui_width(); + +if (::kernel::kernel_state() == ::kernel::kernel_states().over) { + ::kernel::kernel_draw_panel(0, 0, _w, ::kernel::kernel_gui_height(), c_black, 0.72); + ::kernel::kernel_draw_text(_w / 2, 190, "GAME OVER", c_white, fa_center); + ::kernel::kernel_draw_text(_w / 2, 226, ::kernel::kernel_result(), c_ltgray, fa_center); + ::kernel::kernel_draw_text(_w / 2, 272, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); + ::kernel::kernel_draw_text(_w / 2, 300, + string(rows_cleared) + " rows", c_ltgray, fa_center); + ::kernel::kernel_draw_text(_w / 2, 340, + "best ever " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); + exit; +} + +var _o = ::feel::feel_shake_offset(); + +// Well backing, so the playfield reads as a container and not as loose +// blocks on the background. +::kernel::kernel_draw_panel(board.ox - 4 + _o.x, board.oy - 4 + _o.y, + board.cols * cell + 8, board.rows * cell + 8, c_black, 0.5); + +for (var cy = 0; cy < board.rows; cy++) { + var _flashing = false; + for (var i = 0; i < array_length(flash_rows); i++) { + if (flash_rows[i] == cy) _flashing = true; + } + for (var cx = 0; cx < board.cols; cx++) { + var _v = ::grid::grid_get(board, cx, cy); + var _p = ::grid::grid_cell_to_px(board, cx, cy); + var _x = _p.x + _o.x; + var _y = _p.y + _o.y; + + if (_v == 0) { + draw_set_alpha(0.10); + draw_set_colour(c_white); + draw_rectangle(_x, _y, _x + cell - 1, _y + cell - 1, true); + draw_set_alpha(1); + continue; + } + var _rgb = pieces[_v - 1].rgb; + draw_set_colour(_flashing ? c_white : make_colour_rgb(_rgb[0], _rgb[1], _rgb[2])); + draw_rectangle(_x + 1, _y + 1, _x + cell - 2, _y + cell - 2, false); + } +} + +// The active piece, drawn over the settled board. +if (flash_left <= 0) { + var _cells = piece_cells(piece, rot, px, py); + var _rgb = pieces[piece].rgb; + for (var i = 0; i < array_length(_cells); i++) { + var _p = ::grid::grid_cell_to_px(board, _cells[i][0], _cells[i][1]); + draw_set_colour(make_colour_rgb(_rgb[0], _rgb[1], _rgb[2])); + draw_rectangle(_p.x + 1 + _o.x, _p.y + 1 + _o.y, + _p.x + cell - 2 + _o.x, _p.y + cell - 2 + _o.y, false); + } +} +draw_set_colour(c_white); + +::kernel::kernel_draw_text(16, 60, "ROWS " + string(rows_cleared)); +::kernel::kernel_draw_text(16, 80, pieces[piece].name); +::kernel::kernel_draw_text(16, ::kernel::kernel_gui_height() - 46, "arrows move"); +::kernel::kernel_draw_text(16, ::kernel::kernel_gui_height() - 26, "up rotates, down drops"); diff --git a/prefabs/falling_block/objects/obj_stack/Step.gml b/prefabs/falling_block/objects/obj_stack/Step.gml new file mode 100644 index 00000000..91abbeae --- /dev/null +++ b/prefabs/falling_block/objects/obj_stack/Step.gml @@ -0,0 +1,37 @@ +if (!::kernel::kernel_playing()) exit; + +var _dt = delta_time / 1000000; + +// The row-clear flash is a pause in the simulation, not an animation +// layered over it — the board has already collapsed underneath. +if (flash_left > 0) { + flash_left -= _dt; + if (flash_left <= 0) flash_rows = []; + exit; +} + +// Hit-stop freezes gravity and input together, so a lock never eats a move. +if (::feel::feel_frozen()) exit; + +if (::kernel::kernel_action_pressed("left") && fits(piece, rot, px - 1, py)) px -= 1; +if (::kernel::kernel_action_pressed("right") && fits(piece, rot, px + 1, py)) px += 1; + +if (::kernel::kernel_action_pressed("rotate")) { + var _next = (rot + 1) % 4; + // Wall kick: try the rotation in place, then nudged one cell either + // way. Without this, rotating against a wall silently does nothing. + if (fits(piece, _next, px, py)) rot = _next; + else if (fits(piece, _next, px - 1, py)) { rot = _next; px -= 1; } + else if (fits(piece, _next, px + 1, py)) { rot = _next; px += 1; } +} + +var _interval = ::kernel::kernel_action_held("down") + ? soft_drop + : fall_interval(); + +fall_timer += _dt; +if (fall_timer >= _interval) { + fall_timer = 0; + if (fits(piece, rot, px, py + 1)) py += 1; + else lock_piece(); +} diff --git a/prefabs/falling_block/objects/obj_stack/object.toml b/prefabs/falling_block/objects/obj_stack/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/falling_block/objects/obj_stack/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/falling_block/prefab.toml b/prefabs/falling_block/prefab.toml new file mode 100644 index 00000000..c12276d4 --- /dev/null +++ b/prefabs/falling_block/prefab.toml @@ -0,0 +1,66 @@ +schema_version = 1 +name = "Falling block" +install = "copy" +description = "Stack falling pieces, clear full rows, survive the speed ramp." +category = "game" +tags = ["genre:puzzle", "role:template"] + +requires = ["kernel", "feel", "grid"] + +seam = "scripts/scr_falling_block_data/scr_falling_block_data.gml" + +steps = [ + "`falling_block.gametest.json` came with the prefab and already passes: the well runs in rm_stack, the piece table is loaded, the active piece falls on its own, left moves it, and up rotates it. Run it with `gmx test falling_block.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", +] + +[roles] +obj_stack = "the whole game: the well, the active piece, gravity, locking and row clears" +rm_stack = "the only room — the well is centred in it and the HUD sits in the margin" + +[tuning] +cols = 10 +rows = 18 +cell = 26 +fall_interval = 0.65 +fall_floor = 0.12 +speed_ramp = 0.04 +soft_drop = 0.04 +points_per_row = 100 + +# The seven tetrominoes, as data rather than seven branches of GML. +# `cells` is the spawn rotation; the glue rotates about the piece centre, +# which is why an O piece needs no special case. +[[content.pieces]] +name = "I" +rgb = [80, 220, 230] +cells = [[0, 1], [1, 1], [2, 1], [3, 1]] + +[[content.pieces]] +name = "O" +rgb = [235, 205, 75] +cells = [[1, 0], [2, 0], [1, 1], [2, 1]] + +[[content.pieces]] +name = "T" +rgb = [180, 110, 220] +cells = [[1, 0], [0, 1], [1, 1], [2, 1]] + +[[content.pieces]] +name = "S" +rgb = [110, 215, 120] +cells = [[1, 0], [2, 0], [0, 1], [1, 1]] + +[[content.pieces]] +name = "Z" +rgb = [230, 95, 95] +cells = [[0, 0], [1, 0], [1, 1], [2, 1]] + +[[content.pieces]] +name = "J" +rgb = [95, 130, 230] +cells = [[0, 0], [0, 1], [1, 1], [2, 1]] + +[[content.pieces]] +name = "L" +rgb = [230, 155, 75] +cells = [[2, 0], [0, 1], [1, 1], [2, 1]] diff --git a/prefabs/falling_block/project.toml b/prefabs/falling_block/project.toml new file mode 100644 index 00000000..8751b1f3 --- /dev/null +++ b/prefabs/falling_block/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "falling_block" + +[room_order] +order = ["rm_stack"] diff --git a/prefabs/falling_block/rooms/rm_stack/layers/Background.toml b/prefabs/falling_block/rooms/rm_stack/layers/Background.toml new file mode 100644 index 00000000..8ebb247d --- /dev/null +++ b/prefabs/falling_block/rooms/rm_stack/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#10121A" diff --git a/prefabs/falling_block/rooms/rm_stack/layers/Instances.toml b/prefabs/falling_block/rooms/rm_stack/layers/Instances.toml new file mode 100644 index 00000000..3d30f804 --- /dev/null +++ b/prefabs/falling_block/rooms/rm_stack/layers/Instances.toml @@ -0,0 +1,16 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_kernel" +object = "::kernel::obj_kernel" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_stack" +object = "obj_stack" +x = 0.0 +y = 0.0 diff --git a/prefabs/falling_block/rooms/rm_stack/room.toml b/prefabs/falling_block/rooms/rm_stack/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/falling_block/rooms/rm_stack/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/falling_block/thumbnail.png b/prefabs/falling_block/thumbnail.png new file mode 100644 index 00000000..abfec6ce Binary files /dev/null and b/prefabs/falling_block/thumbnail.png differ diff --git a/prefabs/feel/LICENSE b/prefabs/feel/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/feel/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/feel/README.md b/prefabs/feel/README.md new file mode 100644 index 00000000..d34284b2 --- /dev/null +++ b/prefabs/feel/README.md @@ -0,0 +1,22 @@ +# feel + +Juice: tweens, hit-stop, screen shake, floating popups. + +Tweens and easing, hit-stop, screen shake, floating score popups and squash-and-stretch. Deliberately depends on nothing, so it can be retuned and re-released without moving the kernel. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +::feel::feel_hitstop(0.08); +::feel::feel_shake(0.3, 8); +::feel::feel_pop(x, y, "+100", c_yellow); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::feel::` prefix. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/feel/objects/obj_demo/Create.gml b/prefabs/feel/objects/obj_demo/Create.gml new file mode 100644 index 00000000..5e6f214b --- /dev/null +++ b/prefabs/feel/objects/obj_demo/Create.gml @@ -0,0 +1,2 @@ +feel_ensure(); +n = 0; diff --git a/prefabs/feel/objects/obj_demo/Draw_GUI.gml b/prefabs/feel/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..15cf4bb0 --- /dev/null +++ b/prefabs/feel/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _o = feel_shake_offset(); +draw_text(16 + _o.x, 16 + _o.y, "feel demo — click for hitstop, shake and a popup"); +draw_text(16 + _o.x, 36 + _o.y, feel_frozen() ? "FROZEN" : "running"); + +// Squash a box on the same beat, so the curve is visible and not just felt. +var _s = feel_squash(feel_frozen() ? 0.35 : 0); +draw_rectangle(200 - 40 * _s.xscale, 160 - 40 * _s.yscale, + 200 + 40 * _s.xscale, 160 + 40 * _s.yscale, true); diff --git a/prefabs/feel/objects/obj_demo/Step.gml b/prefabs/feel/objects/obj_demo/Step.gml new file mode 100644 index 00000000..357ff479 --- /dev/null +++ b/prefabs/feel/objects/obj_demo/Step.gml @@ -0,0 +1,6 @@ +if (mouse_check_button_pressed(mb_left) || keyboard_check_pressed(vk_space)) { + n += 1; + feel_hitstop(0.09); + feel_shake(0.25, 8); + feel_pop(device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), "+" + string(n * 10), c_yellow); +} diff --git a/prefabs/feel/objects/obj_demo/object.toml b/prefabs/feel/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/feel/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/feel/objects/obj_feel/Create.gml b/prefabs/feel/objects/obj_feel/Create.gml new file mode 100644 index 00000000..f8fa0137 --- /dev/null +++ b/prefabs/feel/objects/obj_feel/Create.gml @@ -0,0 +1,8 @@ +hitstop = 0; +shake_time = 0; +shake_left = 0; +shake_mag = 0; +pops = []; + +// The offset currently added to the camera, so it can be taken back off. +applied_shake = { x: 0, y: 0 }; diff --git a/prefabs/feel/objects/obj_feel/Draw_GUI.gml b/prefabs/feel/objects/obj_feel/Draw_GUI.gml new file mode 100644 index 00000000..d8a5be50 --- /dev/null +++ b/prefabs/feel/objects/obj_feel/Draw_GUI.gml @@ -0,0 +1,11 @@ +for (var i = 0; i < array_length(pops); i++) { + var _p = pops[i]; + var _t = _p.life / _p.span; + draw_set_halign(fa_center); + draw_set_alpha(1 - _t); + draw_set_colour(_p.colour); + draw_text(_p.x, _p.y - feel_tween(0, 34, _t, "out_quad"), _p.text); + draw_set_alpha(1); + draw_set_colour(c_white); + draw_set_halign(fa_left); +} diff --git a/prefabs/feel/objects/obj_feel/Step.gml b/prefabs/feel/objects/obj_feel/Step.gml new file mode 100644 index 00000000..908f414e --- /dev/null +++ b/prefabs/feel/objects/obj_feel/Step.gml @@ -0,0 +1,38 @@ +var _dt = delta_time / 1000000; + +if (hitstop > 0) hitstop = max(0, hitstop - _dt); +if (shake_left > 0) { + shake_left = max(0, shake_left - _dt); + if (shake_left == 0) shake_mag = 0; +} + +// Age the popups and drop the finished ones. Rebuilt rather than +// spliced: the list is short and rebuilding cannot skip an entry the +// way removing while iterating does. +var _live = []; +for (var i = 0; i < array_length(pops); i++) { + var _p = pops[i]; + _p.life += _dt; + if (_p.life < _p.span) array_push(_live, _p); +} +pops = _live; + +// Apply the shake to the camera here, rather than leaving every game to +// remember `feel_shake_offset()`. +// +// It was returned-not-applied so a caller could shake the board or one sprite +// instead — but across nine whole games nobody ever called it, so every +// `feel_shake` in the catalogue was invisible. A view-less room still gets +// nothing (there is no camera to move), and a game that wants to shake +// something else can still ask for the offset and ignore this. +if (view_enabled && view_visible[0]) { + var _cam = view_camera[0]; + // Undo last frame's offset before applying this one, or the camera walks. + camera_set_view_pos(_cam, + camera_get_view_x(_cam) - applied_shake.x, + camera_get_view_y(_cam) - applied_shake.y); + applied_shake = feel_shake_offset(); + camera_set_view_pos(_cam, + camera_get_view_x(_cam) + applied_shake.x, + camera_get_view_y(_cam) + applied_shake.y); +} diff --git a/prefabs/feel/objects/obj_feel/object.toml b/prefabs/feel/objects/obj_feel/object.toml new file mode 100644 index 00000000..d5d72d15 --- /dev/null +++ b/prefabs/feel/objects/obj_feel/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +persistent = true diff --git a/prefabs/feel/prefab.toml b/prefabs/feel/prefab.toml new file mode 100644 index 00000000..b1a4bcc7 --- /dev/null +++ b/prefabs/feel/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "Feel" + +# A library: installed under `prefabs//` in the target project and +# called as `::feel::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = [] + +description = "Tweens and easing, hit-stop, screen shake, floating score popups and squash-and-stretch. Deliberately depends on nothing, so it can be retuned and re-released without moving the kernel." +category = "library" +tags = ["capability:tween", "capability:screenshake", "role:vfx", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/feel/project.toml b/prefabs/feel/project.toml new file mode 100644 index 00000000..26e936ce --- /dev/null +++ b/prefabs/feel/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "feel" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/feel/rooms/rm_demo/layers/Background.toml b/prefabs/feel/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/feel/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/feel/rooms/rm_demo/layers/Instances.toml b/prefabs/feel/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2c5f18a7 --- /dev/null +++ b/prefabs/feel/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/feel/rooms/rm_demo/room.toml b/prefabs/feel/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/feel/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/feel/scripts/scr_feel/scr_feel.gml b/prefabs/feel/scripts/scr_feel/scr_feel.gml new file mode 100644 index 00000000..66d6e686 --- /dev/null +++ b/prefabs/feel/scripts/scr_feel/scr_feel.gml @@ -0,0 +1,40 @@ +/// Juice: the difference between a loop that works and one that feels +/// finished. +/// +/// This pack requires nothing on purpose. Everything here is a pure +/// function of its arguments or of feel's own controller, so retuning a +/// curve never forces a kernel release — and every template can take a +/// feel update without taking anything else. + +/// @function feel_ease(t, kind) +/// @description Remap a 0..1 progress through an easing curve. +/// Unknown kinds fall through to linear rather than erroring: a typo in +/// a tuning file should look wrong, not crash the game. +function feel_ease(t, kind = "out_quad") { + t = clamp(t, 0, 1); + switch (kind) { + case "in_quad": return t * t; + case "out_quad": return 1 - (1 - t) * (1 - t); + case "in_out": return (t < 0.5) ? 2 * t * t : 1 - power(-2 * t + 2, 2) / 2; + case "out_back": return 1 + 2.70158 * power(t - 1, 3) + 1.70158 * power(t - 1, 2); + case "out_elastic": + if (t == 0 || t == 1) return t; + return power(2, -10 * t) * sin((t * 10 - 0.75) * (2 * pi / 3)) + 1; + } + return t; +} + +/// @function feel_tween(from, to, t, kind) +/// @description Interpolate `from`..`to` at eased progress `t`. +function feel_tween(from, to, t, kind = "out_quad") { + return from + (to - from) * feel_ease(t, kind); +} + +/// @function feel_ensure() +/// @description Make sure feel's controller exists. Called by every +/// entry point below, so a template never has to place the object. +function feel_ensure() { + if (!instance_exists(obj_feel)) instance_create_depth(0, 0, -9000, obj_feel); + return obj_feel; +} + diff --git a/prefabs/feel/scripts/scr_feel_pop/scr_feel_pop.gml b/prefabs/feel/scripts/scr_feel_pop/scr_feel_pop.gml new file mode 100644 index 00000000..4ff0bf7b --- /dev/null +++ b/prefabs/feel/scripts/scr_feel_pop/scr_feel_pop.gml @@ -0,0 +1,17 @@ +/// @function feel_pop(x, y, text, colour) +/// @description A floating label that rises and fades. GUI-space +/// coordinates, matching where scores are drawn. +function feel_pop(x, y, text, colour = c_white) { + feel_ensure(); + array_push(obj_feel.pops, { + x: x, y: y, text: string(text), colour: colour, life: 0, span: 0.8 + }); +} + +/// @function feel_squash(amount) +/// @description Squash-and-stretch scale pair for `amount` 0..1, as +/// {xscale, yscale}. Volume-preserving, which is what stops it reading +/// as a plain resize. +function feel_squash(amount) { + return { xscale: 1 + amount, yscale: 1 - amount * 0.6 }; +} diff --git a/prefabs/feel/scripts/scr_feel_shake/scr_feel_shake.gml b/prefabs/feel/scripts/scr_feel_shake/scr_feel_shake.gml new file mode 100644 index 00000000..ffe68e86 --- /dev/null +++ b/prefabs/feel/scripts/scr_feel_shake/scr_feel_shake.gml @@ -0,0 +1,38 @@ +/// @function feel_hitstop(seconds) +/// @description Freeze the world briefly. The single cheapest thing +/// that makes an impact read as an impact. +function feel_hitstop(seconds = 0.08) { + feel_ensure(); + obj_feel.hitstop = max(obj_feel.hitstop, seconds); +} + +/// @function feel_frozen() +/// @description True while hit-stop is running. A caller that moves +/// things checks this; drawing does not. +function feel_frozen() { + if (!instance_exists(obj_feel)) return false; + return obj_feel.hitstop > 0; +} + +/// @function feel_shake(seconds, magnitude) +function feel_shake(seconds = 0.25, magnitude = 6) { + feel_ensure(); + obj_feel.shake_time = max(obj_feel.shake_time, seconds); + obj_feel.shake_left = obj_feel.shake_time; + obj_feel.shake_mag = max(obj_feel.shake_mag, magnitude); +} + +/// @function feel_shake_offset() +/// @description Current shake offset as {x, y}. +/// +/// `obj_feel` already applies this to the camera each step, so a game with a +/// view shakes without doing anything. Ask for it directly only to shake +/// something else as well — the board, one sprite — and note that each call +/// returns a fresh random sample. +function feel_shake_offset() { + if (!instance_exists(obj_feel) || obj_feel.shake_left <= 0) return { x: 0, y: 0 }; + var _f = obj_feel.shake_left / obj_feel.shake_time; + var _m = obj_feel.shake_mag * _f; + return { x: random_range(-_m, _m), y: random_range(-_m, _m) }; +} + diff --git a/prefabs/feel/thumbnail.png b/prefabs/feel/thumbnail.png new file mode 100644 index 00000000..ad077367 Binary files /dev/null and b/prefabs/feel/thumbnail.png differ diff --git a/prefabs/font_system/LICENSE b/prefabs/font_system/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/font_system/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/font_system/README.md b/prefabs/font_system/README.md new file mode 100644 index 00000000..6f91eb73 --- /dev/null +++ b/prefabs/font_system/README.md @@ -0,0 +1,24 @@ +# font_system + +Reference an installed host font by family name — no TTF in the repo. + +## When this is the wrong choice + +The compiler rasterises at **build** time, so the runtime needs no host +fonts — but the build machine does. If CI does not have the family, the +build silently falls back to something else and your text changes shape +between your machine and the release. + +Three ways out, in increasing order of commitment: + +- **A fallback chain.** Make `family` a list; the first one present on the + build machine wins. +- **A CSS generic** — `"sans-serif"`, `"serif"`, `"monospace"`. Always + resolves, never exactly what you designed against. +- **`gmx tools font-freeze --project .`** bakes the rasterised glyphs into the repo. The + font stops depending on the build machine entirely, at the cost of a + binary in your tree and a re-freeze whenever the size changes. + +Use a real `mode = "ttf"` font instead when the typeface is part of the +game's identity — a licensed display face is not something to leave to +whatever the build agent happens to have installed. diff --git a/prefabs/font_system/fonts/fnt_main/font.toml b/prefabs/font_system/fonts/fnt_main/font.toml new file mode 100644 index 00000000..f07b06b2 --- /dev/null +++ b/prefabs/font_system/fonts/fnt_main/font.toml @@ -0,0 +1,8 @@ +#:schema ../../.gm-schema/v1.json#font + +# A single host family. Swap for a list to give the build machine +# alternatives: family = ["Inter", "Helvetica Neue", "sans-serif"] +mode = "system" +family = "Arial" +size = 20 +anti_alias = true diff --git a/prefabs/font_system/objects/obj_demo/Draw_GUI.gml b/prefabs/font_system/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..f489dcf7 --- /dev/null +++ b/prefabs/font_system/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,12 @@ +draw_set_font(fnt_main); + +draw_text(40, 36, "font_system demo - fnt_main, resolved from a host family at build time"); + +draw_text(40, 100, "The quick brown fox jumps over the lazy dog"); +draw_text(40, 140, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); +draw_text(40, 180, "abcdefghijklmnopqrstuvwxyz"); +draw_text(40, 220, "0123456789 !?@#$%&*() <>[]{}/\\"); + +draw_text(40, 300, "score 1200 lives 3 x1.5"); +draw_text(40, 340, "-- paused --"); +draw_text(40, 380, "press SPACE to start"); diff --git a/prefabs/font_system/objects/obj_demo/object.toml b/prefabs/font_system/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/font_system/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/font_system/prefab.toml b/prefabs/font_system/prefab.toml new file mode 100644 index 00000000..1c9ec0c4 --- /dev/null +++ b/prefabs/font_system/prefab.toml @@ -0,0 +1,21 @@ +schema_version = 1 +name = "Font from a host family" +description = "Reference an installed font by family name — no TTF in the repo." +category = "component" +tags = ["capability:text", "role:template"] + +install = "copy" +requires = [] + +seam = "fonts/fnt_main/font.toml" + +steps = [ + "Use it as `fnt_main` in draw_set_font.", + "The compiler rasterises at BUILD time, so the runtime needs no host fonts — but the build machine does. If CI does not have the family, the build falls back and the text changes shape. `gmx tools font-freeze --project .` bakes the rasterised glyphs into the repo and removes that dependency.", + "For a fallback chain, make `family` a list — the first one present on the build machine wins. For a CSS generic, use \"sans-serif\", \"serif\" or \"monospace\".", +] + +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +[roles] +fnt_main = "the font, resolved from a host family at build time rather than a TTF in the repo" diff --git a/prefabs/font_system/project.toml b/prefabs/font_system/project.toml new file mode 100644 index 00000000..064fc04b --- /dev/null +++ b/prefabs/font_system/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "font-system" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/font_system/rooms/rm_demo/layers/Background.toml b/prefabs/font_system/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/font_system/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/font_system/rooms/rm_demo/layers/Instances.toml b/prefabs/font_system/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/font_system/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/font_system/rooms/rm_demo/room.toml b/prefabs/font_system/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/font_system/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/font_system/thumbnail.png b/prefabs/font_system/thumbnail.png new file mode 100644 index 00000000..243d72ec Binary files /dev/null and b/prefabs/font_system/thumbnail.png differ diff --git a/prefabs/grid/LICENSE b/prefabs/grid/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/grid/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/grid/README.md b/prefabs/grid/README.md new file mode 100644 index 00000000..c2cc99c9 --- /dev/null +++ b/prefabs/grid/README.md @@ -0,0 +1,28 @@ +# grid + +A board of cells: conversion, neighbours, row collapse, undo. + +A cell container with cell/pixel conversion and neighbour queries, occupancy tests, row detection and collapse, and snapshot/restore for undo. Four small concepts in one pack because a puzzle is not real without undo. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +board = grid_make(10, 18, 26, 26); +if (grid_row_full(board, y)) grid_collapse_row(board, y); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::grid::` prefix. + +## Requires + +- `kernel` + +These install alongside rather than being folded in, so several +prefabs requiring the same one share a single copy. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/grid/objects/obj_demo/Create.gml b/prefabs/grid/objects/obj_demo/Create.gml new file mode 100644 index 00000000..305e03fb --- /dev/null +++ b/prefabs/grid/objects/obj_demo/Create.gml @@ -0,0 +1,11 @@ +// The demo now runs inside a project that installs grid's `requires`, so +// it can use the kernel the way a real grid game would — gating on the +// run state rather than hand-rolling one. +::kernel::kernel_boot(7); +::kernel::kernel_state_set(::kernel::kernel_states().play); + +board = grid_make(10, 8, 40, 40, 40, 90); +for (var i = 0; i < 12; i++) grid_set(board, irandom(9), 4 + irandom(3), 1); + +// One snapshot taken up front is all undo and restart both need. +start = grid_snapshot(board); diff --git a/prefabs/grid/objects/obj_demo/Draw.gml b/prefabs/grid/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..9d02f09a --- /dev/null +++ b/prefabs/grid/objects/obj_demo/Draw.gml @@ -0,0 +1,11 @@ +for (var cy = 0; cy < board.rows; cy++) { + var _full = grid_row_full(board, cy); + for (var cx = 0; cx < board.cols; cx++) { + var _p = grid_cell_to_px(board, cx, cy); + draw_set_colour(grid_empty(board, cx, cy) ? c_dkgray : (_full ? c_lime : c_aqua)); + draw_rectangle(_p.x + 1, _p.y + 1, _p.x + board.cell_w - 2, _p.y + board.cell_h - 2, + grid_empty(board, cx, cy)); + } +} +draw_set_colour(c_white); +draw_text(40, 60, "grid demo — click a cell; a full row turns green; Esc restores"); diff --git a/prefabs/grid/objects/obj_demo/Step.gml b/prefabs/grid/objects/obj_demo/Step.gml new file mode 100644 index 00000000..41c14901 --- /dev/null +++ b/prefabs/grid/objects/obj_demo/Step.gml @@ -0,0 +1,12 @@ +if (!::kernel::kernel_playing()) exit; + +if (mouse_check_button_pressed(mb_left)) { + var _c = grid_px_to_cell(board, mouse_x, mouse_y); + if (grid_in_bounds(board, _c.cx, _c.cy)) { + grid_set(board, _c.cx, _c.cy, grid_empty(board, _c.cx, _c.cy) ? 1 : 0); + ::kernel::kernel_score_add(10); + } +} + +// Restart is a snapshot restore — the same mechanism undo would use. +if (::kernel::kernel_action_pressed("cancel")) grid_restore(board, start); diff --git a/prefabs/grid/objects/obj_demo/object.toml b/prefabs/grid/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/grid/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/grid/prefab.toml b/prefabs/grid/prefab.toml new file mode 100644 index 00000000..5c3fb221 --- /dev/null +++ b/prefabs/grid/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "Grid board" + +# A library: installed under `prefabs//` in the target project and +# called as `::grid::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = ["kernel"] + +description = "A cell container with cell/pixel conversion and neighbour queries, occupancy tests, row detection and collapse, and snapshot/restore for undo. Four small concepts in one pack because a puzzle is not real without undo." +category = "library" +tags = ["capability:grid", "capability:pathfinding", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/grid/project.toml b/prefabs/grid/project.toml new file mode 100644 index 00000000..47cd2bea --- /dev/null +++ b/prefabs/grid/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "grid" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/grid/rooms/rm_demo/layers/Background.toml b/prefabs/grid/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/grid/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/grid/rooms/rm_demo/layers/Instances.toml b/prefabs/grid/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2c5f18a7 --- /dev/null +++ b/prefabs/grid/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/grid/rooms/rm_demo/room.toml b/prefabs/grid/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/grid/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/grid/scripts/scr_grid/scr_grid.gml b/prefabs/grid/scripts/scr_grid/scr_grid.gml new file mode 100644 index 00000000..b1ca3cb9 --- /dev/null +++ b/prefabs/grid/scripts/scr_grid/scr_grid.gml @@ -0,0 +1,52 @@ +/// A board of cells. +/// +/// The board is a plain struct holding a flat array, not a ds_grid: +/// structs are garbage-collected and copy cheaply, which is what makes +/// snapshot/restore — and therefore undo — a two-line operation instead +/// of a resource-lifetime problem. + +/// @function grid_make(cols, rows, cell_w, cell_h, ox, oy) +/// @description A board of `cols` x `rows` empty cells, drawn from +/// (`ox`, `oy`). Cell value 0 means empty; anything else is the +/// caller's to interpret. +function grid_make(cols, rows, cell_w, cell_h, ox = 0, oy = 0) { + var _cells = array_create(cols * rows, 0); + return { + cols: cols, rows: rows, + cell_w: cell_w, cell_h: cell_h, + ox: ox, oy: oy, + cells: _cells + }; +} + +/// @function grid_in_bounds(g, cx, cy) +function grid_in_bounds(g, cx, cy) { + return (cx >= 0 && cy >= 0 && cx < g.cols && cy < g.rows); +} + +/// @function grid_get(g, cx, cy) +/// @description Cell value, or -1 out of bounds. Out-of-bounds reads +/// as solid rather than empty, so a caller testing "can I move here" +/// gets the wall for free. +function grid_get(g, cx, cy) { + if (!grid_in_bounds(g, cx, cy)) return -1; + return g.cells[cy * g.cols + cx]; +} + +/// @function grid_set(g, cx, cy, value) +function grid_set(g, cx, cy, value) { + if (!grid_in_bounds(g, cx, cy)) return false; + g.cells[cy * g.cols + cx] = value; + return true; +} + +/// @function grid_empty(g, cx, cy) +function grid_empty(g, cx, cy) { + return grid_get(g, cx, cy) == 0; +} + +/// @function grid_clear(g) +function grid_clear(g) { + for (var i = 0; i < array_length(g.cells); i++) g.cells[i] = 0; +} + diff --git a/prefabs/grid/scripts/scr_grid_rows/scr_grid_rows.gml b/prefabs/grid/scripts/scr_grid_rows/scr_grid_rows.gml new file mode 100644 index 00000000..10f5c6d6 --- /dev/null +++ b/prefabs/grid/scripts/scr_grid_rows/scr_grid_rows.gml @@ -0,0 +1,29 @@ +/// @function grid_row_full(g, cy) +function grid_row_full(g, cy) { + for (var cx = 0; cx < g.cols; cx++) { + if (grid_empty(g, cx, cy)) return false; + } + return true; +} + +/// @function grid_full_rows(g) +/// @description Every full row index, top to bottom. +function grid_full_rows(g) { + var _out = []; + for (var cy = 0; cy < g.rows; cy++) { + if (grid_row_full(g, cy)) array_push(_out, cy); + } + return _out; +} + +/// @function grid_collapse_row(g, cy) +/// @description Delete row `cy` and drop everything above it by one. +function grid_collapse_row(g, cy) { + for (var _y = cy; _y > 0; _y--) { + for (var cx = 0; cx < g.cols; cx++) { + grid_set(g, cx, _y, grid_get(g, cx, _y - 1)); + } + } + for (var cx = 0; cx < g.cols; cx++) grid_set(g, cx, 0, 0); +} + diff --git a/prefabs/grid/scripts/scr_grid_space/scr_grid_space.gml b/prefabs/grid/scripts/scr_grid_space/scr_grid_space.gml new file mode 100644 index 00000000..b6c38b8c --- /dev/null +++ b/prefabs/grid/scripts/scr_grid_space/scr_grid_space.gml @@ -0,0 +1,31 @@ +/// @function grid_cell_to_px(g, cx, cy) +/// @description Top-left pixel of a cell, as {x, y}. +function grid_cell_to_px(g, cx, cy) { + return { x: g.ox + cx * g.cell_w, y: g.oy + cy * g.cell_h }; +} + +/// @function grid_px_to_cell(g, px, py) +/// @description Cell under a pixel, as {cx, cy}. May be out of bounds — +/// test with grid_in_bounds rather than trusting it. +function grid_px_to_cell(g, px, py) { + return { + cx: floor((px - g.ox) / g.cell_w), + cy: floor((py - g.oy) / g.cell_h) + }; +} + +/// @function grid_neighbours(g, cx, cy, diagonal) +/// @description In-bounds neighbours as an array of {cx, cy}. +function grid_neighbours(g, cx, cy, diagonal = false) { + var _offsets = diagonal + ? [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]] + : [[0,-1],[-1,0],[1,0],[0,1]]; + var _out = []; + for (var i = 0; i < array_length(_offsets); i++) { + var _nx = cx + _offsets[i][0]; + var _ny = cy + _offsets[i][1]; + if (grid_in_bounds(g, _nx, _ny)) array_push(_out, { cx: _nx, cy: _ny }); + } + return _out; +} + diff --git a/prefabs/grid/scripts/scr_grid_undo/scr_grid_undo.gml b/prefabs/grid/scripts/scr_grid_undo/scr_grid_undo.gml new file mode 100644 index 00000000..db48c9b0 --- /dev/null +++ b/prefabs/grid/scripts/scr_grid_undo/scr_grid_undo.gml @@ -0,0 +1,13 @@ +/// @function grid_snapshot(g) +/// @description A restorable copy of the board's contents. This is what +/// makes undo and restart the same mechanism. +function grid_snapshot(g) { + var _copy = array_create(array_length(g.cells)); + array_copy(_copy, 0, g.cells, 0, array_length(g.cells)); + return _copy; +} + +/// @function grid_restore(g, snapshot) +function grid_restore(g, snapshot) { + array_copy(g.cells, 0, snapshot, 0, array_length(snapshot)); +} diff --git a/prefabs/grid/thumbnail.png b/prefabs/grid/thumbnail.png new file mode 100644 index 00000000..b26b7124 Binary files /dev/null and b/prefabs/grid/thumbnail.png differ diff --git a/prefabs/hazard/LICENSE b/prefabs/hazard/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/hazard/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/hazard/README.md b/prefabs/hazard/README.md new file mode 100644 index 00000000..0afc1750 --- /dev/null +++ b/prefabs/hazard/README.md @@ -0,0 +1,3 @@ +# hazard + +Spikes, pits, and the place you come back to. diff --git a/prefabs/hazard/objects/obj_demo/Create.gml b/prefabs/hazard/objects/obj_demo/Create.gml new file mode 100644 index 00000000..383e5683 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo/Create.gml @@ -0,0 +1,10 @@ +x = 120; +y = 460; +respawn = hazard_make(x, y); + +// Three spikes and two checkpoints along the way, so the checkpoint moving +// is visible rather than asserted. +instance_create_depth(360, 460, 0, obj_demo_spike); +instance_create_depth(600, 460, 0, obj_demo_spike); +instance_create_depth(840, 460, 0, obj_demo_spike); +checkpoints = [[480, 460], [720, 460]]; diff --git a/prefabs/hazard/objects/obj_demo/Draw.gml b/prefabs/hazard/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..e6b43679 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo/Draw.gml @@ -0,0 +1,3 @@ +// Flashing while safe is what tells the player why nothing is hurting them. +if (hazard_safe(respawn) && (current_time div 100) mod 2 == 0) exit; +draw_self(); diff --git a/prefabs/hazard/objects/obj_demo/Draw_GUI.gml b/prefabs/hazard/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..20be4ea4 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,4 @@ +draw_text(40, 30, "hazard demo — arrow keys; red hurts, checkpoints move where you return"); +draw_text(40, 60, "deaths " + string(hazard_deaths(respawn)) + + " checkpoint at " + string(respawn.x) + + (hazard_safe(respawn) ? " (safe)" : "")); diff --git a/prefabs/hazard/objects/obj_demo/Step.gml b/prefabs/hazard/objects/obj_demo/Step.gml new file mode 100644 index 00000000..a3b2045b --- /dev/null +++ b/prefabs/hazard/objects/obj_demo/Step.gml @@ -0,0 +1,15 @@ +hazard_step(respawn); + +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +x = clamp(x + _dx * 4, 0, room_width); + +for (var i = 0; i < array_length(checkpoints); i++) { + var _c = checkpoints[i]; + if (abs(x - _c[0]) < 16) hazard_checkpoint(respawn, _c[0], _c[1]); +} + +// The grace window is what stops a respawn onto a spike costing every life +// at once. +if (!hazard_safe(respawn) && hazard_touching(obj_demo_spike) != noone) { + hazard_respawn(respawn, 1.2); +} diff --git a/prefabs/hazard/objects/obj_demo/object.toml b/prefabs/hazard/objects/obj_demo/object.toml new file mode 100644 index 00000000..d8556182 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_body" diff --git a/prefabs/hazard/objects/obj_demo_spike/Draw.gml b/prefabs/hazard/objects/obj_demo_spike/Draw.gml new file mode 100644 index 00000000..f6e49047 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo_spike/Draw.gml @@ -0,0 +1,3 @@ +draw_set_colour(c_red); +draw_rectangle(x - 24, y - 12, x + 24, y + 12, false); +draw_set_colour(c_white); diff --git a/prefabs/hazard/objects/obj_demo_spike/object.toml b/prefabs/hazard/objects/obj_demo_spike/object.toml new file mode 100644 index 00000000..33a59cc0 --- /dev/null +++ b/prefabs/hazard/objects/obj_demo_spike/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_spike" diff --git a/prefabs/hazard/prefab.toml b/prefabs/hazard/prefab.toml new file mode 100644 index 00000000..2aa91e05 --- /dev/null +++ b/prefabs/hazard/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Hazards and checkpoints" + +# A library: installed under `prefabs//` in the target project and +# called as `::hazard::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Respawn points that move as the player passes checkpoints, a respawn that stops the instance dead rather than putting it back at speed, a grace window after it, knockback for hazards that hurt instead of killing, and a fell-out-of-the-room check for the pit that is not an object." +category = "library" +tags = ["capability:hazards", "capability:checkpoints", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_spike", "rooms/rm_demo", "sprites/spr_demo_body", "sprites/spr_demo_spike"] + +steps = [ + "`hazard_respawn` and `hazard_knockback` move the calling instance, so they go in the player's own event.", +] diff --git a/prefabs/hazard/project.toml b/prefabs/hazard/project.toml new file mode 100644 index 00000000..07d4ce8b --- /dev/null +++ b/prefabs/hazard/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "hazard" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/hazard/rooms/rm_demo/layers/Background.toml b/prefabs/hazard/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/hazard/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/hazard/rooms/rm_demo/layers/Instances.toml b/prefabs/hazard/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/hazard/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/hazard/rooms/rm_demo/room.toml b/prefabs/hazard/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/hazard/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/hazard/scripts/scr_hazard/scr_hazard.gml b/prefabs/hazard/scripts/scr_hazard/scr_hazard.gml new file mode 100644 index 00000000..3e4f1af8 --- /dev/null +++ b/prefabs/hazard/scripts/scr_hazard/scr_hazard.gml @@ -0,0 +1,54 @@ +/// Spikes, pits and the place you come back to. +/// +/// A hazard without a checkpoint is a hazard that sends the player to the +/// start of the level, and a level that punishes a mistake with three +/// minutes of walking is one they stop playing rather than one they master. + +/// @function hazard_make(spawn_x, spawn_y) +/// @description Respawn state anchored at the level's start. Update it with +/// `hazard_checkpoint` as the player passes one. +function hazard_make(spawn_x, spawn_y) { + return { x: spawn_x, y: spawn_y, deaths: 0, since: 0 }; +} + +/// @function hazard_touching(obj_hazard) +/// @scope instance +/// @description The hazard instance this one is overlapping, or `noone`. +function hazard_touching(obj_hazard) { + return instance_place(x, y, obj_hazard); +} + +/// @function hazard_step(state) +/// @description Count the respawn grace down. Call once a frame. +function hazard_step(state) { + if (state.since > 0) { + state.since = max(0, state.since - delta_time / 1000000); + } +} + +/// @function hazard_safe(state) +/// @description True while the respawn grace is still running — do not +/// apply damage, and flash the sprite so the player knows why. +function hazard_safe(state) { + return state.since > 0; +} + +/// @function hazard_knockback(from_x, from_y, force) +/// @scope instance +/// @description Shove the calling instance away from a point. What a hazard +/// that hurts rather than kills should do, so the player is not left +/// standing in it taking a hit per frame. +function hazard_knockback(from_x, from_y, force) { + var _dir = point_direction(from_x, from_y, x, y); + x = clamp(x + lengthdir_x(force, _dir), 0, room_width); + y = clamp(y + lengthdir_y(force, _dir), 0, room_height); +} + +/// @function hazard_fell_out(margin) +/// @scope instance +/// @description Whether the calling instance has fallen below the room by +/// `margin`. The pit that is not an object. +function hazard_fell_out(margin) { + return y > room_height + margin; +} + diff --git a/prefabs/hazard/scripts/scr_hazard_checkpoint/scr_hazard_checkpoint.gml b/prefabs/hazard/scripts/scr_hazard_checkpoint/scr_hazard_checkpoint.gml new file mode 100644 index 00000000..1fb3a30d --- /dev/null +++ b/prefabs/hazard/scripts/scr_hazard_checkpoint/scr_hazard_checkpoint.gml @@ -0,0 +1,28 @@ +/// @function hazard_checkpoint(state, cx, cy) +/// @description Move the respawn point. Ignores a checkpoint the player is +/// already standing on, so a trigger firing every frame does not reset the +/// grace window over and over. +function hazard_checkpoint(state, cx, cy) { + if (state.x == cx && state.y == cy) return false; + state.x = cx; + state.y = cy; + return true; +} + +/// @function hazard_respawn(state, grace_seconds) +/// @scope instance +/// @description Put the calling instance back at the checkpoint, stop it +/// dead, and start a window of `grace_seconds`. Stopping the motion matters: +/// respawning at speed walks the player straight back into what killed them. +function hazard_respawn(state, grace_seconds) { + x = state.x; + y = state.y; + speed = 0; + state.deaths += 1; + state.since = grace_seconds; +} + +/// @function hazard_deaths(state) +function hazard_deaths(state) { + return state.deaths; +} diff --git a/prefabs/hazard/sprites/spr_demo_body/frame_000.png b/prefabs/hazard/sprites/spr_demo_body/frame_000.png new file mode 100644 index 00000000..30271a6f Binary files /dev/null and b/prefabs/hazard/sprites/spr_demo_body/frame_000.png differ diff --git a/prefabs/hazard/sprites/spr_demo_body/sprite.toml b/prefabs/hazard/sprites/spr_demo_body/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/hazard/sprites/spr_demo_body/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/hazard/sprites/spr_demo_spike/frame_000.png b/prefabs/hazard/sprites/spr_demo_spike/frame_000.png new file mode 100644 index 00000000..68292557 Binary files /dev/null and b/prefabs/hazard/sprites/spr_demo_spike/frame_000.png differ diff --git a/prefabs/hazard/sprites/spr_demo_spike/sprite.toml b/prefabs/hazard/sprites/spr_demo_spike/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/hazard/sprites/spr_demo_spike/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/hazard/thumbnail.png b/prefabs/hazard/thumbnail.png new file mode 100644 index 00000000..a9ad7d29 Binary files /dev/null and b/prefabs/hazard/thumbnail.png differ diff --git a/prefabs/health/LICENSE b/prefabs/health/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/health/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/health/README.md b/prefabs/health/README.md new file mode 100644 index 00000000..c56e7ddf --- /dev/null +++ b/prefabs/health/README.md @@ -0,0 +1,3 @@ +# health + +A hit takes a chunk, not the whole bar — damage with an i-frame window. diff --git a/prefabs/health/objects/obj_demo/Create.gml b/prefabs/health/objects/obj_demo/Create.gml new file mode 100644 index 00000000..34edae57 --- /dev/null +++ b/prefabs/health/objects/obj_demo/Create.gml @@ -0,0 +1,5 @@ +// Two bars side by side: one with a window, one without. Holding the key +// empties the second almost instantly and barely touches the first, which +// is the entire reason the window exists. +guarded = health_make(10, 0.8); +raw = health_make(10, 0); diff --git a/prefabs/health/objects/obj_demo/Draw_GUI.gml b/prefabs/health/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..3f452e50 --- /dev/null +++ b/prefabs/health/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,9 @@ +draw_text(40, 30, "health demo — hold SPACE to take damage, ENTER to reset"); + +draw_text(40, 80, "with a 0.8s window"); +health_draw_bar(40, 100, 300, 24, guarded); +draw_text(360, 100, health_invulnerable(guarded) ? "safe" : ""); + +draw_text(40, 160, "without one"); +health_draw_bar(40, 180, 300, 24, raw); +draw_text(360, 180, health_dead(raw) ? "dead" : ""); diff --git a/prefabs/health/objects/obj_demo/Step.gml b/prefabs/health/objects/obj_demo/Step.gml new file mode 100644 index 00000000..6b4158c1 --- /dev/null +++ b/prefabs/health/objects/obj_demo/Step.gml @@ -0,0 +1,11 @@ +health_step(guarded); +health_step(raw); + +if (keyboard_check(vk_space)) { + health_hurt(guarded, 1); + health_hurt(raw, 1); +} +if (keyboard_check_pressed(vk_enter)) { + health_reset(guarded); + health_reset(raw); +} diff --git a/prefabs/health/objects/obj_demo/object.toml b/prefabs/health/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/health/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/health/prefab.toml b/prefabs/health/prefab.toml new file mode 100644 index 00000000..100747db --- /dev/null +++ b/prefabs/health/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Health, damage and invulnerability" + +# A library: installed under `prefabs//` in the target project and +# called as `::health::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Hit points with a maximum, damage that respects a window of invulnerability after each hit, healing that cannot overfill, and a bar to draw it with. The window is the whole point: without one an enemy that stays overlapping drains a full bar in three frames, and the run ends before the player understands they were touched — a game that validates, builds and boots perfectly while being unplayable." +category = "library" +tags = ["capability:health", "capability:damage", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "Call `health_step(state)` once a frame or the invulnerability window never expires.", +] diff --git a/prefabs/health/project.toml b/prefabs/health/project.toml new file mode 100644 index 00000000..c6a6a083 --- /dev/null +++ b/prefabs/health/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "health" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/health/rooms/rm_demo/layers/Background.toml b/prefabs/health/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/health/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/health/rooms/rm_demo/layers/Instances.toml b/prefabs/health/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/health/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/health/rooms/rm_demo/room.toml b/prefabs/health/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/health/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/health/scripts/scr_health/scr_health.gml b/prefabs/health/scripts/scr_health/scr_health.gml new file mode 100644 index 00000000..f2559ff0 --- /dev/null +++ b/prefabs/health/scripts/scr_health/scr_health.gml @@ -0,0 +1,54 @@ +/// Hit points, damage and the window of safety after a hit. +/// +/// State is a struct rather than instance variables so one instance can own +/// several bars — a shield alongside health, a vehicle alongside its driver +/// — and so the caller picks the variable name. + +/// @function health_make(max_hp, iframe_seconds) +/// @description Health state with `max_hp` points and `iframe_seconds` of +/// invulnerability granted by each hit that lands. Pass 0 for no window. +function health_make(max_hp, iframe_seconds) { + return { + hp: max_hp, + max: max(1, max_hp), + grace: max(0, iframe_seconds), + iframes: 0, + }; +} + +/// @function health_step(state) +/// @description Count the invulnerability window down. Call once a frame, +/// or the window never expires and nothing can be hurt twice. +function health_step(state) { + if (state.iframes > 0) { + state.iframes = max(0, state.iframes - delta_time / 1000000); + } +} + +/// @function health_hurt(state, amount) +/// @description Take `amount` unless still invulnerable. Returns whether the +/// hit landed, so a caller can flash, shake or play a sound only when it did +/// rather than on every frame of contact. +function health_hurt(state, amount) { + if (state.iframes > 0 || health_dead(state)) return false; + state.hp = max(0, state.hp - amount); + state.iframes = state.grace; + return true; +} + +/// @function health_heal(state, amount) +/// @description Restore `amount`, never above the maximum. Returns how much +/// was actually restored. +function health_heal(state, amount) { + var _before = state.hp; + state.hp = min(state.max, state.hp + amount); + return state.hp - _before; +} + +/// @function health_reset(state) +/// @description Back to full, window cleared. What a new run needs. +function health_reset(state) { + state.hp = state.max; + state.iframes = 0; +} + diff --git a/prefabs/health/scripts/scr_health_draw/scr_health_draw.gml b/prefabs/health/scripts/scr_health_draw/scr_health_draw.gml new file mode 100644 index 00000000..815890de --- /dev/null +++ b/prefabs/health/scripts/scr_health_draw/scr_health_draw.gml @@ -0,0 +1,10 @@ +/// @function health_draw_bar(x, y, width, height, state) +/// @description A bar at (x, y) with an outline, filled left to right. +/// Colours from green to red as it empties. +function health_draw_bar(x, y, width, height, state) { + var _f = health_fraction(state); + draw_set_colour(merge_colour(c_red, c_lime, _f)); + draw_rectangle(x, y, x + width * _f, y + height, false); + draw_set_colour(c_white); + draw_rectangle(x, y, x + width, y + height, true); +} diff --git a/prefabs/health/scripts/scr_health_query/scr_health_query.gml b/prefabs/health/scripts/scr_health_query/scr_health_query.gml new file mode 100644 index 00000000..832ec3f2 --- /dev/null +++ b/prefabs/health/scripts/scr_health_query/scr_health_query.gml @@ -0,0 +1,19 @@ +/// @function health_dead(state) +function health_dead(state) { + return state.hp <= 0; +} + +/// @function health_invulnerable(state) +/// @description True while the window from the last hit is still open. +/// Flashing the sprite on this is what tells a player why they are not +/// taking damage. +function health_invulnerable(state) { + return state.iframes > 0; +} + +/// @function health_fraction(state) +/// @description Remaining health as 0..1, for a bar. +function health_fraction(state) { + return state.hp / state.max; +} + diff --git a/prefabs/health/thumbnail.png b/prefabs/health/thumbnail.png new file mode 100644 index 00000000..7d5b91d7 Binary files /dev/null and b/prefabs/health/thumbnail.png differ diff --git a/prefabs/hud/LICENSE b/prefabs/hud/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/hud/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/hud/README.md b/prefabs/hud/README.md new file mode 100644 index 00000000..a591e995 --- /dev/null +++ b/prefabs/hud/README.md @@ -0,0 +1,3 @@ +# hud + +Bars, pips and labels anchored to a corner, in GUI space. diff --git a/prefabs/hud/objects/obj_demo/Create.gml b/prefabs/hud/objects/obj_demo/Create.gml new file mode 100644 index 00000000..2f085f42 --- /dev/null +++ b/prefabs/hud/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +hp = 1.0; +lag = 1.0; +hearts = 3; diff --git a/prefabs/hud/objects/obj_demo/Draw_GUI.gml b/prefabs/hud/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..5cba5ef2 --- /dev/null +++ b/prefabs/hud/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,15 @@ +var _tl = hud_anchor("tl", 40, 30); +draw_text(_tl.x, _tl.y, "hud demo — SPACE damages, L costs a life, ENTER resets"); + +hud_panel(_tl.x, _tl.y + 40, 320, 96, 0.45); +draw_text(_tl.x + 12, _tl.y + 50, "health"); +hud_bar_lagged(_tl.x + 12, _tl.y + 74, 280, 18, hp, lag, c_lime); +draw_text(_tl.x + 12, _tl.y + 100, "lives"); +hud_pips(_tl.x + 70, _tl.y + 100, 3, hearts, 14, 6); + +var _tr = hud_anchor("tr", 40, 30); +hud_label(_tr.x, _tr.y, "anchored right", fa_right); +var _br = hud_anchor("br", 40, 40); +hud_label(_br.x, _br.y, "anchored bottom-right", fa_right); +var _bt = hud_anchor("bottom", 0, 90); +hud_label(_bt.x, _bt.y, "these stay put when the window resizes", fa_center); diff --git a/prefabs/hud/objects/obj_demo/Step.gml b/prefabs/hud/objects/obj_demo/Step.gml new file mode 100644 index 00000000..7fdd7cab --- /dev/null +++ b/prefabs/hud/objects/obj_demo/Step.gml @@ -0,0 +1,6 @@ +if (keyboard_check_pressed(vk_space)) hp = max(0, hp - 0.22); +if (keyboard_check_pressed(vk_enter)) { hp = 1; lag = 1; hearts = 3; } +if (keyboard_check_pressed(ord("L"))) hearts = max(0, hearts - 1); + +// The chase bar catches up slowly, which is what makes a big hit readable. +lag = hud_chase(lag, hp, 0.35); diff --git a/prefabs/hud/objects/obj_demo/object.toml b/prefabs/hud/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/hud/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/hud/prefab.toml b/prefabs/hud/prefab.toml new file mode 100644 index 00000000..38dc187e --- /dev/null +++ b/prefabs/hud/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "On-screen numbers" + +# A library: installed under `prefabs//` in the target project and +# called as `::hud::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Bars with an optional lagging chase bar behind them so a big hit is legible, discrete pips for counts small enough to read at a glance, corner anchoring that survives a window resize, and a label helper that restores the previous alignment. All in GUI space, because a HUD in room coordinates scrolls away the moment the game gets a camera." +category = "library" +tags = ["capability:hud", "capability:ui", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "These draw in GUI space, so they belong in a Draw GUI event, not Draw.", +] diff --git a/prefabs/hud/project.toml b/prefabs/hud/project.toml new file mode 100644 index 00000000..7fcd7874 --- /dev/null +++ b/prefabs/hud/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "hud" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/hud/rooms/rm_demo/layers/Background.toml b/prefabs/hud/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/hud/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/hud/rooms/rm_demo/layers/Instances.toml b/prefabs/hud/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/hud/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/hud/rooms/rm_demo/room.toml b/prefabs/hud/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/hud/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/hud/scripts/scr_hud/scr_hud.gml b/prefabs/hud/scripts/scr_hud/scr_hud.gml new file mode 100644 index 00000000..9f550577 --- /dev/null +++ b/prefabs/hud/scripts/scr_hud/scr_hud.gml @@ -0,0 +1,37 @@ +/// The numbers on top of the game. +/// +/// Everything here is in GUI space and anchored to a corner, because a HUD +/// positioned in room coordinates scrolls away with the camera the moment +/// the game gets a view — which it always eventually does. + +/// @function hud_width() +/// @description GUI width, falling back to the room when no GUI size is set. +function hud_width() { + var _w = display_get_gui_width(); + return _w > 0 ? _w : room_width; +} + +/// @function hud_height() +function hud_height() { + var _h = display_get_gui_height(); + return _h > 0 ? _h : room_height; +} + +/// @function hud_anchor(corner, dx, dy) +/// @description A point `dx`, `dy` in from a named corner — "tl", "tr", +/// "bl", "br", "top", "bottom" or "centre" — as `{x, y}`. Positions written +/// against one corner stay put when the window changes size. +function hud_anchor(corner, dx, dy) { + var _w = hud_width(); + var _h = hud_height(); + switch (corner) { + case "tr": return { x: _w - dx, y: dy }; + case "bl": return { x: dx, y: _h - dy }; + case "br": return { x: _w - dx, y: _h - dy }; + case "top": return { x: _w / 2 + dx, y: dy }; + case "bottom": return { x: _w / 2 + dx, y: _h - dy }; + case "centre": return { x: _w / 2 + dx, y: _h / 2 + dy }; + default: return { x: dx, y: dy }; + } +} + diff --git a/prefabs/hud/scripts/scr_hud_bar/scr_hud_bar.gml b/prefabs/hud/scripts/scr_hud_bar/scr_hud_bar.gml new file mode 100644 index 00000000..c4203545 --- /dev/null +++ b/prefabs/hud/scripts/scr_hud_bar/scr_hud_bar.gml @@ -0,0 +1,43 @@ +/// @function hud_bar(x, y, width, height, fraction, fill) +/// @description A filled bar with an outline. `fraction` is clamped, so a +/// caller that overheals cannot draw past the end of the bar. +function hud_bar(x, y, width, height, fraction, fill) { + var _f = clamp(fraction, 0, 1); + draw_set_colour(fill); + draw_rectangle(x, y, x + width * _f, y + height, false); + draw_set_colour(c_white); + draw_rectangle(x, y, x + width, y + height, true); +} + +/// @function hud_bar_lagged(x, y, width, height, fraction, chase, fill) +/// @description The same bar with a second, slower one behind it showing +/// where the value just was. The lag is what makes a big hit legible — +/// a bar that simply jumps reads as a bar that was always that low. +function hud_bar_lagged(x, y, width, height, fraction, chase, fill) { + var _f = clamp(fraction, 0, 1); + var _c = clamp(chase, 0, 1); + draw_set_colour(c_maroon); + draw_rectangle(x, y, x + width * max(_f, _c), y + height, false); + hud_bar(x, y, width, height, _f, fill); +} + +/// @function hud_chase(current, target, per_second) +/// @description Move `current` towards `target` at `per_second`, framerate +/// independent. Feed its result back in as the `chase` of a lagged bar. +function hud_chase(current, target, per_second) { + var _step = per_second * delta_time / 1000000; + if (current > target) return max(target, current - _step); + return min(target, current + _step); +} + +/// @function hud_pips(x, y, count, filled, size, gap) +/// @description Discrete units — lives, hearts, ammo — as outlined boxes, +/// filled up to `filled`. Better than a bar when the number is small enough +/// to count, because a player can see "two left" without reading a number. +function hud_pips(x, y, count, filled, size, gap) { + for (var i = 0; i < count; i++) { + var _x = x + i * (size + gap); + draw_rectangle(_x, y, _x + size, y + size, i >= filled); + } +} + diff --git a/prefabs/hud/scripts/scr_hud_text/scr_hud_text.gml b/prefabs/hud/scripts/scr_hud_text/scr_hud_text.gml new file mode 100644 index 00000000..77547f8c --- /dev/null +++ b/prefabs/hud/scripts/scr_hud_text/scr_hud_text.gml @@ -0,0 +1,21 @@ +/// @function hud_label(x, y, text, halign) +/// @description Draw one line at an alignment and restore the previous one, +/// so a caller never inherits another caller's halign — the single most +/// common cross-component drawing bug. +function hud_label(x, y, text, halign) { + var _was = draw_get_halign(); + draw_set_halign(halign); + draw_text(x, y, text); + draw_set_halign(_was); +} + +/// @function hud_panel(x, y, width, height, alpha) +/// @description A dimmed backing rectangle, for putting text over a busy +/// game without it becoming unreadable. +function hud_panel(x, y, width, height, alpha) { + draw_set_colour(c_black); + draw_set_alpha(alpha); + draw_rectangle(x, y, x + width, y + height, false); + draw_set_alpha(1); + draw_set_colour(c_white); +} diff --git a/prefabs/hud/thumbnail.png b/prefabs/hud/thumbnail.png new file mode 100644 index 00000000..25a5a4a9 Binary files /dev/null and b/prefabs/hud/thumbnail.png differ diff --git a/prefabs/inventory/LICENSE b/prefabs/inventory/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/inventory/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/inventory/README.md b/prefabs/inventory/README.md new file mode 100644 index 00000000..f2601d5c --- /dev/null +++ b/prefabs/inventory/README.md @@ -0,0 +1,3 @@ +# inventory + +Slots holding stacks, and the rule for when it will not all fit. diff --git a/prefabs/inventory/objects/obj_demo/Create.gml b/prefabs/inventory/objects/obj_demo/Create.gml new file mode 100644 index 00000000..9462aed3 --- /dev/null +++ b/prefabs/inventory/objects/obj_demo/Create.gml @@ -0,0 +1,5 @@ +// Four slots of five, so the interesting cases are reachable by hand: +// a part-used stack filling before a new slot opens, and an add that +// only partly fits. +bag = inventory_make(4, 5); +last = ""; diff --git a/prefabs/inventory/objects/obj_demo/Draw_GUI.gml b/prefabs/inventory/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..39510b52 --- /dev/null +++ b/prefabs/inventory/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,16 @@ +draw_text(40, 30, "inventory demo — 1 arrow, 2 bomb, 3 rope, 4 key"); +draw_text(40, 50, "BACKSPACE removes 2 arrows, DELETE empties it"); +draw_text(40, 90, last); + +for (var i = 0; i < bag.slots; i++) { + var _x = 40 + i * 120; + draw_rectangle(_x, 130, _x + 100, 210, true); + var _s = inventory_slot(bag, i); + if (_s == undefined) continue; + draw_text(_x + 12, 150, _s.item); + draw_text(_x + 12, 175, string(_s.count) + " / " + string(bag.stack)); +} + +draw_text(40, 240, inventory_full(bag) ? "every slot used" : "slots free"); +draw_text(40, 260, "room for 4 more arrows: " + + string(inventory_room_for(bag, "arrow", 4))); diff --git a/prefabs/inventory/objects/obj_demo/Step.gml b/prefabs/inventory/objects/obj_demo/Step.gml new file mode 100644 index 00000000..1c029392 --- /dev/null +++ b/prefabs/inventory/objects/obj_demo/Step.gml @@ -0,0 +1,6 @@ +if (keyboard_check_pressed(ord("1"))) last = "arrow x3 → fit " + string(inventory_add(bag, "arrow", 3)); +if (keyboard_check_pressed(ord("2"))) last = "bomb x7 → fit " + string(inventory_add(bag, "bomb", 7)); +if (keyboard_check_pressed(ord("3"))) last = "rope x1 → fit " + string(inventory_add(bag, "rope", 1)); +if (keyboard_check_pressed(ord("4"))) last = "key x1 → fit " + string(inventory_add(bag, "key", 1)); +if (keyboard_check_pressed(vk_backspace)) last = "arrow -2 → took " + string(inventory_remove(bag, "arrow", 2)); +if (keyboard_check_pressed(vk_delete)) { inventory_clear(bag); last = "cleared"; } diff --git a/prefabs/inventory/objects/obj_demo/object.toml b/prefabs/inventory/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/inventory/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/inventory/prefab.toml b/prefabs/inventory/prefab.toml new file mode 100644 index 00000000..049cb3dd --- /dev/null +++ b/prefabs/inventory/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Carrying things" + +# A library: installed under `prefabs//` in the target project and +# called as `::inventory::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Slots holding stacks of one item each, with add and remove that report how many actually moved rather than assuming it all fit. Also room-for, so a pickup prompt can be honest before the player walks into it. The returned count is the whole design: silently discarding the overflow is the bug this exists to prevent." +category = "library" +tags = ["capability:inventory", "capability:items", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "`inventory_add` returns how many fit — drop the difference on the floor rather than discarding it silently.", +] diff --git a/prefabs/inventory/project.toml b/prefabs/inventory/project.toml new file mode 100644 index 00000000..8e343da6 --- /dev/null +++ b/prefabs/inventory/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "inventory" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/inventory/rooms/rm_demo/layers/Background.toml b/prefabs/inventory/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/inventory/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/inventory/rooms/rm_demo/layers/Instances.toml b/prefabs/inventory/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/inventory/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/inventory/rooms/rm_demo/room.toml b/prefabs/inventory/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/inventory/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/inventory/scripts/scr_inventory/scr_inventory.gml b/prefabs/inventory/scripts/scr_inventory/scr_inventory.gml new file mode 100644 index 00000000..842ae9f5 --- /dev/null +++ b/prefabs/inventory/scripts/scr_inventory/scr_inventory.gml @@ -0,0 +1,57 @@ +/// What the player is carrying. +/// +/// Slots holding stacks, rather than a flat list, because "twelve arrows" +/// and "twelve separate arrows" are different games — and because the +/// interesting rule is what happens when there is no room for all of it. + +/// @function inventory_make(slots, stack_max) +/// @description An empty inventory of `slots` slots, each holding up to +/// `stack_max` of one item. Pass a stack of 1 for a game where every item +/// takes its own slot. +function inventory_make(slots, stack_max) { + return { slots: max(1, slots), stack: max(1, stack_max), items: [] }; +} + +/// @function inventory_add(inv, item, count) +/// @description Add up to `count` of `item`, filling part-used stacks before +/// opening new slots. Returns how many actually fit — the difference is what +/// to drop on the floor, and silently discarding it is the bug this return +/// value exists to prevent. +function inventory_add(inv, item, count) { + var _left = count; + for (var i = 0; i < array_length(inv.items) && _left > 0; i++) { + var _s = inv.items[i]; + if (_s.item != item) continue; + var _room = inv.stack - _s.count; + var _put = min(_room, _left); + _s.count += _put; + _left -= _put; + } + while (_left > 0 && array_length(inv.items) < inv.slots) { + var _put = min(inv.stack, _left); + array_push(inv.items, { item: item, count: _put }); + _left -= _put; + } + return count - _left; +} + +/// @function inventory_remove(inv, item, count) +/// @description Take up to `count` away, emptying stacks from the last one +/// back. Returns how many were actually removed. +function inventory_remove(inv, item, count) { + var _left = count; + for (var i = array_length(inv.items) - 1; i >= 0 && _left > 0; i--) { + var _s = inv.items[i]; + if (_s.item != item) continue; + var _take = min(_s.count, _left); + _s.count -= _take; + _left -= _take; + if (_s.count <= 0) array_delete(inv.items, i, 1); + } + return count - _left; +} + +/// @function inventory_clear(inv) +function inventory_clear(inv) { + inv.items = []; +} diff --git a/prefabs/inventory/scripts/scr_inventory_query/scr_inventory_query.gml b/prefabs/inventory/scripts/scr_inventory_query/scr_inventory_query.gml new file mode 100644 index 00000000..c11130c7 --- /dev/null +++ b/prefabs/inventory/scripts/scr_inventory_query/scr_inventory_query.gml @@ -0,0 +1,44 @@ +/// @function inventory_count(inv, item) +/// @description How many are held across every stack. +function inventory_count(inv, item) { + var _n = 0; + for (var i = 0; i < array_length(inv.items); i++) { + if (inv.items[i].item == item) _n += inv.items[i].count; + } + return _n; +} + +/// @function inventory_has(inv, item, count) +/// @description Whether at least `count` are held — the check a recipe or a +/// locked door makes before taking them. +function inventory_has(inv, item, count) { + return inventory_count(inv, item) >= count; +} + +/// @function inventory_room_for(inv, item, count) +/// @description How many of `count` would fit right now, without adding any. +/// Ask before showing a pickup prompt. +function inventory_room_for(inv, item, count) { + var _space = (inv.slots - array_length(inv.items)) * inv.stack; + for (var i = 0; i < array_length(inv.items); i++) { + if (inv.items[i].item == item) _space += inv.stack - inv.items[i].count; + } + return min(count, _space); +} + +/// @function inventory_full(inv) +/// @description Whether every slot is occupied. A full inventory can still +/// take more of something it already holds, so this is not the same as +/// having no room. +function inventory_full(inv) { + return array_length(inv.items) >= inv.slots; +} + +/// @function inventory_slot(inv, index) +/// @description The stack in slot `index` as `{item, count}`, or `undefined` +/// for an empty slot — so a caller can tell an empty slot from a missing one. +function inventory_slot(inv, index) { + if (index < 0 || index >= array_length(inv.items)) return undefined; + return inv.items[index]; +} + diff --git a/prefabs/inventory/thumbnail.png b/prefabs/inventory/thumbnail.png new file mode 100644 index 00000000..2cb7a97c Binary files /dev/null and b/prefabs/inventory/thumbnail.png differ diff --git a/prefabs/iso/LICENSE b/prefabs/iso/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/iso/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/iso/README.md b/prefabs/iso/README.md new file mode 100644 index 00000000..e7a98d08 --- /dev/null +++ b/prefabs/iso/README.md @@ -0,0 +1,22 @@ +# iso + +Isometric projection, its inverse, and the depth key. + +Grid-to-screen isometric projection, the inverse, and the depth key that makes a tile skirt occlude correctly. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +iso = iso_make(96, 48, 480, 120); +var p = iso_to_screen(iso, col, row); +depth = iso_depth(col, row); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::iso::` prefix. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/iso/objects/obj_demo/Create.gml b/prefabs/iso/objects/obj_demo/Create.gml new file mode 100644 index 00000000..7c3e0470 --- /dev/null +++ b/prefabs/iso/objects/obj_demo/Create.gml @@ -0,0 +1,2 @@ +iso = iso_make(96, 48, 480, 120); +hover = { col: -1, row: -1 }; diff --git a/prefabs/iso/objects/obj_demo/Draw.gml b/prefabs/iso/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..ce96ac1f --- /dev/null +++ b/prefabs/iso/objects/obj_demo/Draw.gml @@ -0,0 +1,15 @@ +for (var row = 0; row < 8; row++) { + for (var col = 0; col < 8; col++) { + var _p = iso_to_screen(iso, col, row); + var _on = (col == hover.col && row == hover.row); + draw_set_colour(_on ? c_yellow : c_teal); + // The four corners of the diamond. + draw_line(_p.x, _p.y - iso.tile_h / 2, _p.x + iso.tile_w / 2, _p.y); + draw_line(_p.x + iso.tile_w / 2, _p.y, _p.x, _p.y + iso.tile_h / 2); + draw_line(_p.x, _p.y + iso.tile_h / 2, _p.x - iso.tile_w / 2, _p.y); + draw_line(_p.x - iso.tile_w / 2, _p.y, _p.x, _p.y - iso.tile_h / 2); + } +} +draw_set_colour(c_white); +draw_text(24, 24, "iso demo — hover a tile; cell " + + string(hover.col) + "," + string(hover.row)); diff --git a/prefabs/iso/objects/obj_demo/Step.gml b/prefabs/iso/objects/obj_demo/Step.gml new file mode 100644 index 00000000..ace58e8a --- /dev/null +++ b/prefabs/iso/objects/obj_demo/Step.gml @@ -0,0 +1,2 @@ +var _c = iso_to_cell(iso, mouse_x, mouse_y); +hover = { col: round(_c.col), row: round(_c.row) }; diff --git a/prefabs/iso/objects/obj_demo/object.toml b/prefabs/iso/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/iso/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/iso/prefab.toml b/prefabs/iso/prefab.toml new file mode 100644 index 00000000..92e71af5 --- /dev/null +++ b/prefabs/iso/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "Isometric projection" + +# A library: installed under `prefabs//` in the target project and +# called as `::iso::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = [] + +description = "Grid-to-screen isometric projection, the inverse, and the depth key that makes a tile skirt occlude correctly." +category = "library" +tags = ["capability:isometric", "capability:projection", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/iso/project.toml b/prefabs/iso/project.toml new file mode 100644 index 00000000..9d36fafa --- /dev/null +++ b/prefabs/iso/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "iso" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/iso/rooms/rm_demo/layers/Background.toml b/prefabs/iso/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/iso/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/iso/rooms/rm_demo/layers/Instances.toml b/prefabs/iso/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2c5f18a7 --- /dev/null +++ b/prefabs/iso/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/iso/rooms/rm_demo/room.toml b/prefabs/iso/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/iso/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/iso/scripts/scr_iso/scr_iso.gml b/prefabs/iso/scripts/scr_iso/scr_iso.gml new file mode 100644 index 00000000..8dbcae13 --- /dev/null +++ b/prefabs/iso/scripts/scr_iso/scr_iso.gml @@ -0,0 +1,42 @@ +/// Isometric projection. +/// +/// Two numbers describe a diamond tile: its full width and the height of +/// the diamond ALONE, ignoring any vertical skirt the sprite draws below +/// it. Getting the second one wrong is the classic isometric bug — the +/// grid looks right in isolation and seams open up once tiles neighbour +/// each other — so it is a parameter here rather than a constant. + +/// @function iso_make(tile_w, tile_h, ox, oy) +/// @description A projection. `tile_h` is the DIAMOND height, not the +/// sprite height: a 180x125 sprite with a 52px skirt has tile_h = 73. +function iso_make(tile_w, tile_h, ox = 0, oy = 0) { + return { tile_w: tile_w, tile_h: tile_h, ox: ox, oy: oy }; +} + +/// @function iso_to_screen(iso, col, row) +/// @description Cell → screen pixel, as {x, y}. Fractional cells are +/// fine, which is what lets something move smoothly between tiles. +function iso_to_screen(iso, col, row) { + return { + x: iso.ox + (col - row) * (iso.tile_w / 2), + y: iso.oy + (col + row) * (iso.tile_h / 2) + }; +} + +/// @function iso_to_cell(iso, px, py) +/// @description The inverse, as {col, row}, unrounded. Round both to get +/// the cell under a cursor; keep them fractional to know where in the +/// tile you are. +function iso_to_cell(iso, px, py) { + var _dx = (px - iso.ox) / (iso.tile_w / 2); + var _dy = (py - iso.oy) / (iso.tile_h / 2); + return { col: (_dy + _dx) / 2, row: (_dy - _dx) / 2 }; +} + +/// @function iso_depth(col, row) +/// @description Sort key for a tile or an actor standing on one. Lower +/// draws in front. Negated because GameMaker treats lower depth as +/// nearer, and tiles further down the screen must occlude those behind. +function iso_depth(col, row) { + return -(col + row); +} diff --git a/prefabs/iso/thumbnail.png b/prefabs/iso/thumbnail.png new file mode 100644 index 00000000..a57a3198 Binary files /dev/null and b/prefabs/iso/thumbnail.png differ diff --git a/prefabs/isometric_basics/LICENSE b/prefabs/isometric_basics/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/isometric_basics/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/isometric_basics/README.md b/prefabs/isometric_basics/README.md new file mode 100644 index 00000000..7d880513 --- /dev/null +++ b/prefabs/isometric_basics/README.md @@ -0,0 +1,31 @@ +# isometric_basics + +A diamond grid you can hover and pan, with correct depth sorting. + +## Measure your tileset before trusting any number + +`tile_h` is the height of the **diamond alone**, not of the sprite. A tile +sprite usually draws a vertical skirt below its diamond so that walls and +cliffs have thickness, which makes the sprite taller than the cell it +occupies. + +Use the sprite height and every neighbouring tile overlaps slightly — +seams open along one diagonal and not the other, which is why this bug is +so hard to see in a screenshot of a single tile. For a 180×125 sprite with +a 52px skirt, `tile_h` is 73. + +Tune the sprite origin and `tile_h` together, on a grid of at least 3×3, +and look at the joins rather than the tiles. + +## Depth + +Instances standing on the grid need `::iso::iso_depth(col, row)` as their +depth, or a tile's skirt will draw over an actor that is in front of it. +Tiles drawn in a row-major loop need no sorting — the loop order is +already back-to-front. + +## What comes from where + +The projection and its inverse are `::iso::` — invariant maths you call. +Which tiles, how the camera pans, and what a cell contains are in the +object this prefab gave you. diff --git a/prefabs/isometric_basics/objects/obj_iso_scene/Create.gml b/prefabs/isometric_basics/objects/obj_iso_scene/Create.gml new file mode 100644 index 00000000..391ee6c7 --- /dev/null +++ b/prefabs/isometric_basics/objects/obj_iso_scene/Create.gml @@ -0,0 +1,7 @@ +var _cfg = isometric_basics_tuning(); + +// Origin sits top-centre so the diamond fans out below it. +iso = ::iso::iso_make(_cfg.tile_w, _cfg.tile_h, + ::kernel::kernel_gui_width() / 2, 90); + +hover = { col: -1, row: -1 }; diff --git a/prefabs/isometric_basics/objects/obj_iso_scene/Draw.gml b/prefabs/isometric_basics/objects/obj_iso_scene/Draw.gml new file mode 100644 index 00000000..d5640a77 --- /dev/null +++ b/prefabs/isometric_basics/objects/obj_iso_scene/Draw.gml @@ -0,0 +1,19 @@ +var _cfg = isometric_basics_tuning(); + +// Row-major is already back-to-front for an isometric grid, so a sprite +// drawn here needs no sorting. Instances DO — give them +// ::iso::iso_depth(col, row). +for (var row = 0; row < _cfg.rows; row++) { + for (var col = 0; col < _cfg.cols; col++) { + var _p = ::iso::iso_to_screen(iso, col, row); + var _on = (col == hover.col && row == hover.row); + draw_set_colour(_on ? c_yellow : c_teal); + var _hw = iso.tile_w / 2; + var _hh = iso.tile_h / 2; + draw_line(_p.x, _p.y - _hh, _p.x + _hw, _p.y); + draw_line(_p.x + _hw, _p.y, _p.x, _p.y + _hh); + draw_line(_p.x, _p.y + _hh, _p.x - _hw, _p.y); + draw_line(_p.x - _hw, _p.y, _p.x, _p.y - _hh); + } +} +draw_set_colour(c_white); diff --git a/prefabs/isometric_basics/objects/obj_iso_scene/Step.gml b/prefabs/isometric_basics/objects/obj_iso_scene/Step.gml new file mode 100644 index 00000000..3749d948 --- /dev/null +++ b/prefabs/isometric_basics/objects/obj_iso_scene/Step.gml @@ -0,0 +1,11 @@ +var _cfg = isometric_basics_tuning(); +var _dt = delta_time / 1000000; + +// Pan by moving the projection origin. Nothing else has to know. +if (::kernel::kernel_action_held("left")) iso.ox += _cfg.pan_speed * _dt; +if (::kernel::kernel_action_held("right")) iso.ox -= _cfg.pan_speed * _dt; +if (::kernel::kernel_action_held("up")) iso.oy += _cfg.pan_speed * _dt; +if (::kernel::kernel_action_held("down")) iso.oy -= _cfg.pan_speed * _dt; + +var _c = ::iso::iso_to_cell(iso, mouse_x, mouse_y); +hover = { col: round(_c.col), row: round(_c.row) }; diff --git a/prefabs/isometric_basics/objects/obj_iso_scene/object.toml b/prefabs/isometric_basics/objects/obj_iso_scene/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/isometric_basics/objects/obj_iso_scene/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/isometric_basics/prefab.toml b/prefabs/isometric_basics/prefab.toml new file mode 100644 index 00000000..c92ce4cf --- /dev/null +++ b/prefabs/isometric_basics/prefab.toml @@ -0,0 +1,27 @@ +schema_version = 1 +name = "Isometric scene" +description = "A diamond grid you can hover and pan, with correct depth sorting." +category = "component" +tags = ["capability:iso", "role:template"] + +install = "copy" +requires = ["iso", "kernel"] + +seam = "objects/obj_iso_scene/Step.gml" + +steps = [ + "Place obj_iso_scene on an instance layer in your room.", + "Measure your tileset before trusting the numbers: tile_h is the DIAMOND height, not the sprite height. A sprite with a vertical skirt is taller than its diamond, and using the sprite height opens seams between neighbours. Set both in the generated tuning.", + "Draw a tile sprite in place of the outline: use ::iso::iso_depth(col, row) as the instance depth so a tile's skirt occludes the row behind it.", +] + +[roles] +obj_iso_scene = "owns the projection: draws the diamond grid back-to-front, tracks the hovered cell, and pans by moving the origin" + +[tuning] +cols = 9 +rows = 9 +tile_w = 96 +# The diamond alone. For a 180x125 sprite with a 52px skirt this is 73. +tile_h = 48 +pan_speed = 320 diff --git a/prefabs/isometric_basics/thumbnail.png b/prefabs/isometric_basics/thumbnail.png new file mode 100644 index 00000000..04371df4 Binary files /dev/null and b/prefabs/isometric_basics/thumbnail.png differ diff --git a/prefabs/kernel/LICENSE b/prefabs/kernel/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/kernel/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/kernel/README.md b/prefabs/kernel/README.md new file mode 100644 index 00000000..bafdec46 --- /dev/null +++ b/prefabs/kernel/README.md @@ -0,0 +1,22 @@ +# kernel + +Run lifecycle, score and save — the floor every game shares. + +The floor every game shares: run lifecycle on a seeded RNG, an action-based input map, score and combo, a save store with flags, HUD helpers, audio buses, and the data/tuning accessors a generated template feeds. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +::kernel::kernel_boot(); +::kernel::kernel_state_set(kernel_states().play); +::kernel::kernel_score_add(100); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::kernel::` prefix. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/kernel/fonts/fnt_kernel/font.toml b/prefabs/kernel/fonts/fnt_kernel/font.toml new file mode 100644 index 00000000..4cc37ece --- /dev/null +++ b/prefabs/kernel/fonts/fnt_kernel/font.toml @@ -0,0 +1,13 @@ +#:schema ../../.gm-schema/v1.json#font + +# A chain, not one family: every game built on the kernel draws through +# `kernel_draw_text`, so a family the build machine happens to lack would +# silently reshape the text of the whole catalogue. The trailing CSS generic +# always resolves. +# +# `gmx tools font-freeze --project .` bakes the glyphs into the repo if you want the build to +# stop depending on the machine at all. +mode = "system" +family = ["Inter", "Helvetica Neue", "Arial", "sans-serif"] +size = 20 +anti_alias = true diff --git a/prefabs/kernel/objects/obj_actor/Create.gml b/prefabs/kernel/objects/obj_actor/Create.gml new file mode 100644 index 00000000..203db11e --- /dev/null +++ b/prefabs/kernel/objects/obj_actor/Create.gml @@ -0,0 +1,16 @@ +// Attachments a pack hangs off this actor, keyed by pack name. A pack +// writes its own slot and reads only its own, which is what lets four +// packs share one instance without a parent chain. +attachments = {}; + +/// @function attach(pack, config) +attach = function(pack, config) { + attachments[$ pack] = config; + return config; +}; + +/// @function attachment(pack) +attachment = function(pack) { + if (!variable_struct_exists(attachments, pack)) return undefined; + return attachments[$ pack]; +}; diff --git a/prefabs/kernel/objects/obj_actor/object.toml b/prefabs/kernel/objects/obj_actor/object.toml new file mode 100644 index 00000000..1d9abfcb --- /dev/null +++ b/prefabs/kernel/objects/obj_actor/object.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#object + +# The shared base every pack attaches to. GameMaker allows ONE parent, +# so motion, combat and items cannot each own the player object — +# the kernel publishes this and packs attach by instance id instead. diff --git a/prefabs/kernel/objects/obj_demo/Create.gml b/prefabs/kernel/objects/obj_demo/Create.gml new file mode 100644 index 00000000..cb4fc19c --- /dev/null +++ b/prefabs/kernel/objects/obj_demo/Create.gml @@ -0,0 +1,2 @@ +kernel_boot(1); +kernel_state_set(kernel_states().play); diff --git a/prefabs/kernel/objects/obj_demo/Draw_GUI.gml b/prefabs/kernel/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..8350c948 --- /dev/null +++ b/prefabs/kernel/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,6 @@ +if (kernel_state() == kernel_states().over) { + kernel_draw_text(16, 80, "OVER (" + kernel_result() + ") score " + string(kernel_score())); + kernel_draw_text(16, 100, "best " + string(kernel_save_get("high_score", 0))); +} else { + kernel_draw_text(16, 80, "kernel demo — click or press space to score"); +} diff --git a/prefabs/kernel/objects/obj_demo/Step.gml b/prefabs/kernel/objects/obj_demo/Step.gml new file mode 100644 index 00000000..9f03bc71 --- /dev/null +++ b/prefabs/kernel/objects/obj_demo/Step.gml @@ -0,0 +1,7 @@ +// Exercise score, combo and the game-over path so `gmx run` on the +// kernel alone shows something moving. +if (kernel_playing() && kernel_action_pressed("confirm")) { + kernel_combo_bump(); + kernel_score_add(100); +} +if (kernel_playing() && kernel_elapsed() > 20) kernel_game_over("time"); diff --git a/prefabs/kernel/objects/obj_demo/object.toml b/prefabs/kernel/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/kernel/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/kernel/objects/obj_kernel/Draw_GUI.gml b/prefabs/kernel/objects/obj_kernel/Draw_GUI.gml new file mode 100644 index 00000000..4e271152 --- /dev/null +++ b/prefabs/kernel/objects/obj_kernel/Draw_GUI.gml @@ -0,0 +1,9 @@ +if (global.kernel_hud) kernel_draw_hud(); + +if (global.kernel_pause && kernel_state() == kernel_states().pause) { + kernel_draw_panel(0, 0, kernel_gui_width(), kernel_gui_height(), c_black, 0.6); + kernel_draw_text(kernel_gui_width() / 2, kernel_gui_height() / 2 - 10, + "PAUSED", c_white, fa_center); + kernel_draw_text(kernel_gui_width() / 2, kernel_gui_height() / 2 + 14, + "P to resume", c_ltgray, fa_center); +} diff --git a/prefabs/kernel/objects/obj_kernel/Step.gml b/prefabs/kernel/objects/obj_kernel/Step.gml new file mode 100644 index 00000000..80f89dff --- /dev/null +++ b/prefabs/kernel/objects/obj_kernel/Step.gml @@ -0,0 +1,9 @@ +kernel_tick(); + +// Pause is owned here rather than by any pack: a pack gates on +// kernel_playing(), so toggling this is all pausing means. +if (global.kernel_pause && kernel_action_pressed("pause")) { + var _s = kernel_states(); + if (kernel_state() == _s.play) kernel_state_set(_s.pause); + else if (kernel_state() == _s.pause) kernel_state_set(_s.play); +} diff --git a/prefabs/kernel/objects/obj_kernel/object.toml b/prefabs/kernel/objects/obj_kernel/object.toml new file mode 100644 index 00000000..9e06665b --- /dev/null +++ b/prefabs/kernel/objects/obj_kernel/object.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#object + +# Persistent: the lifecycle, score and save must survive a room change, +# which is the whole reason the kernel is an object and not a script. +persistent = true diff --git a/prefabs/kernel/prefab.toml b/prefabs/kernel/prefab.toml new file mode 100644 index 00000000..1942bca1 --- /dev/null +++ b/prefabs/kernel/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "Kernel" + +# A library: installed under `prefabs//` in the target project and +# called as `::kernel::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = [] + +description = "The floor every game shares: run lifecycle on a seeded RNG, an action-based input map, score and combo, a save store with flags, HUD helpers, audio buses, and the data/tuning accessors a generated template feeds." +category = "library" +tags = ["capability:lifecycle", "capability:save", "capability:input", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/kernel/project.toml b/prefabs/kernel/project.toml new file mode 100644 index 00000000..917a4ca7 --- /dev/null +++ b/prefabs/kernel/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "kernel" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/kernel/rooms/rm_demo/layers/Background.toml b/prefabs/kernel/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/kernel/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/kernel/rooms/rm_demo/layers/Instances.toml b/prefabs/kernel/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..887d1ed1 --- /dev/null +++ b/prefabs/kernel/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,16 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_kernel" +object = "obj_kernel" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/kernel/rooms/rm_demo/room.toml b/prefabs/kernel/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/kernel/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/kernel/scripts/scr_kernel_data/scr_kernel_data.gml b/prefabs/kernel/scripts/scr_kernel_data/scr_kernel_data.gml new file mode 100644 index 00000000..ae6a550a --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_data/scr_kernel_data.gml @@ -0,0 +1,36 @@ +/// The data-plane seam. +/// +/// A template's content tables and tuning knobs are generated into the +/// project as GML, under a name derived from the template slug — which +/// a component cannot know. So the project registers them here at boot +/// and every component reads through these two accessors instead. +/// +/// That indirection is the point: when content moves from generated GML +/// to a real `data/` resource kind, the generator changes and not one +/// line of component GML does. + +/// @function kernel_data_source(data_fn, tuning_struct) +/// @description Register the generated accessors. `data_fn` takes a +/// table name and returns it; `tuning_struct` is a flat struct of knobs. +function kernel_data_source(data_fn, tuning_struct) { + global.kernel_data_fn = data_fn; + global.kernel_tuning = tuning_struct; +} + +/// @function kernel_data(name) +/// @description A content table by name, or an empty array when the +/// template did not declare one — callers iterate, so an empty array is +/// the useful absence. +function kernel_data(name) { + if (global.kernel_data_fn == undefined) return []; + var _t = global.kernel_data_fn(name); + return (_t == undefined) ? [] : _t; +} + +/// @function kernel_tuning(key, fallback) +/// @description One tuning knob. The fallback keeps a component working +/// against a template that has not declared the knob yet. +function kernel_tuning(key, fallback = 0) { + if (!variable_struct_exists(global.kernel_tuning, key)) return fallback; + return global.kernel_tuning[$ key]; +} diff --git a/prefabs/kernel/scripts/scr_kernel_hud/scr_kernel_hud.gml b/prefabs/kernel/scripts/scr_kernel_hud/scr_kernel_hud.gml new file mode 100644 index 00000000..1b60edb1 --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_hud/scr_kernel_hud.gml @@ -0,0 +1,53 @@ +/// HUD drawing in GUI space. Deliberately plain: a template overrides +/// the look by drawing its own, and the kernel guarantees only that +/// something legible is on screen from the first frame. + +/// @function kernel_gui_width() +function kernel_gui_width() { + return display_get_gui_width(); +} + +/// @function kernel_gui_height() +function kernel_gui_height() { + return display_get_gui_height(); +} + +/// @function kernel_draw_text(x, y, text, colour, halign) +/// @description Draw one line and restore the alignment afterwards, so +/// a caller never inherits another caller's halign — the single most +/// common cross-component drawing bug. +function kernel_draw_text(x, y, text, colour = c_white, halign = fa_left) { + // Set every call, like the alignment: leaving the font to whatever drew + // last is how a title screen ends up in the built-in bitmap face. + draw_set_font(fnt_kernel); + draw_set_halign(halign); + draw_set_valign(fa_top); + draw_set_colour(colour); + draw_text(x, y, text); + draw_set_halign(fa_left); + draw_set_colour(c_white); +} + +/// @function kernel_draw_panel(x, y, w, h, colour, alpha) +/// @description A filled rounded-ish panel; the neutral background the +/// UI pack draws its hotspots on. +function kernel_draw_panel(x, y, w, h, colour = c_black, alpha = 0.55) { + draw_set_alpha(alpha); + draw_set_colour(colour); + draw_rectangle(x, y, x + w, y + h, false); + draw_set_alpha(1); + draw_set_colour(c_white); +} + +/// @function kernel_draw_hud() +/// @description Score, combo and clock. Drawn by the kernel controller +/// in Draw GUI, so every template has a HUD before it writes one. +function kernel_draw_hud() { + if (!kernel_playing()) return; + kernel_draw_text(16, 12, "SCORE " + string(kernel_score())); + if (kernel_combo() > 1) { + kernel_draw_text(16, 32, "x" + string(kernel_combo()) + " combo", c_yellow); + } + kernel_draw_text(kernel_gui_width() - 16, 12, + string(floor(kernel_elapsed())) + "s", c_white, fa_right); +} diff --git a/prefabs/kernel/scripts/scr_kernel_input/scr_kernel_input.gml b/prefabs/kernel/scripts/scr_kernel_input/scr_kernel_input.gml new file mode 100644 index 00000000..906270b7 --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_input/scr_kernel_input.gml @@ -0,0 +1,41 @@ +/// Action-based input. A pack asks "is confirm pressed", never "is +/// vk_space pressed", so keyboard, gamepad and pointer stay one +/// concern and a template can rebind without touching a pack. + +/// @function kernel_action_pressed(action) +/// @description True on the frame `action` is first pressed. +/// Actions: left, right, up, down, confirm, cancel, pause, rotate. +function kernel_action_pressed(action) { + switch (action) { + case "left": return keyboard_check_pressed(vk_left) || keyboard_check_pressed(ord("A")); + case "right": return keyboard_check_pressed(vk_right) || keyboard_check_pressed(ord("D")); + case "up": return keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W")); + case "down": return keyboard_check_pressed(vk_down) || keyboard_check_pressed(ord("S")); + case "confirm":return keyboard_check_pressed(vk_space) || keyboard_check_pressed(vk_enter) + || mouse_check_button_pressed(mb_left); + case "cancel": return keyboard_check_pressed(vk_escape); + case "pause": return keyboard_check_pressed(ord("P")); + case "rotate": return keyboard_check_pressed(vk_up) || keyboard_check_pressed(ord("W")); + } + return false; +} + +/// @function kernel_action_held(action) +/// @description True while `action` is held. +function kernel_action_held(action) { + switch (action) { + case "left": return keyboard_check(vk_left) || keyboard_check(ord("A")); + case "right": return keyboard_check(vk_right) || keyboard_check(ord("D")); + case "up": return keyboard_check(vk_up) || keyboard_check(ord("W")); + case "down": return keyboard_check(vk_down) || keyboard_check(ord("S")); + case "confirm":return keyboard_check(vk_space) || mouse_check_button(mb_left); + } + return false; +} + +/// @function kernel_pointer() +/// @description Pointer position in GUI space, as {x, y}. GUI space, +/// not room space: everything the UI pack hit-tests is drawn there. +function kernel_pointer() { + return { x: device_mouse_x_to_gui(0), y: device_mouse_y_to_gui(0) }; +} diff --git a/prefabs/kernel/scripts/scr_kernel_save/scr_kernel_save.gml b/prefabs/kernel/scripts/scr_kernel_save/scr_kernel_save.gml new file mode 100644 index 00000000..eda9f46b --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_save/scr_kernel_save.gml @@ -0,0 +1,75 @@ +/// Persistent store plus run-scoped flags. One file, one struct, so a +/// pack that wants to remember something does not invent a format. + +/// @function kernel_save_file() +function kernel_save_file() { + return "kernel_save.json"; +} + +/// @function kernel_save_load() +/// @description Read the save file into memory. Missing or corrupt file +/// is not an error — it is a first run, and a game that refuses to start +/// because its save is bad is worse than one that starts fresh. +function kernel_save_load() { + global.kernel_save = {}; + if (!file_exists(kernel_save_file())) return; + + var _f = file_text_open_read(kernel_save_file()); + var _raw = ""; + while (!file_text_eof(_f)) { + _raw += file_text_read_string(_f); + file_text_readln(_f); + } + file_text_close(_f); + + try { + var _parsed = json_parse(_raw); + if (is_struct(_parsed)) global.kernel_save = _parsed; + } catch (_e) { + global.kernel_save = {}; + } +} + +/// @function kernel_save_flush() +function kernel_save_flush() { + var _f = file_text_open_write(kernel_save_file()); + file_text_write_string(_f, json_stringify(global.kernel_save)); + file_text_close(_f); +} + +/// @function kernel_save_get(key, fallback) +function kernel_save_get(key, fallback = 0) { + if (!variable_struct_exists(global.kernel_save, key)) return fallback; + return global.kernel_save[$ key]; +} + +/// @function kernel_save_set(key, value) +/// @description Write and flush. Flushing every write is fine at this +/// scale and means a crash never loses a high score. +function kernel_save_set(key, value) { + global.kernel_save[$ key] = value; + kernel_save_flush(); +} + +/// @function kernel_save_high_score(score) +/// @description Record `score` if it beats the stored best. Returns +/// true when it was a new best, so the results screen can say so. +function kernel_save_high_score(score) { + if (score <= kernel_save_get("high_score", 0)) return false; + kernel_save_set("high_score", score); + return true; +} + +/// @function kernel_flag_get(name) +/// @description Run-scoped flag. Cleared by kernel_boot, never written +/// to disk — this is "has the player seen the tutorial this run", not +/// "has the player ever finished the game". +function kernel_flag_get(name) { + if (!variable_struct_exists(global.kernel_flags, name)) return false; + return global.kernel_flags[$ name]; +} + +/// @function kernel_flag_set(name, value) +function kernel_flag_set(name, value = true) { + global.kernel_flags[$ name] = value; +} diff --git a/prefabs/kernel/scripts/scr_kernel_score/scr_kernel_score.gml b/prefabs/kernel/scripts/scr_kernel_score/scr_kernel_score.gml new file mode 100644 index 00000000..83b4a8c8 --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_score/scr_kernel_score.gml @@ -0,0 +1,46 @@ +/// Score and combo. Lives in the kernel rather than a pack because +/// every pack writes to it and packs may not call each other. + +/// @function kernel_score() +function kernel_score() { + return global.kernel_score; +} + +/// @function kernel_score_add(points) +/// @description Add `points`, scaled by the current combo multiplier. +/// Returns what was actually added, so a caller can show the number it +/// awarded rather than the number it asked for. +function kernel_score_add(points) { + var _gained = round(points * kernel_combo_multiplier()); + global.kernel_score += _gained; + return _gained; +} + +/// @function kernel_combo() +function kernel_combo() { + return global.kernel_combo; +} + +/// @function kernel_combo_multiplier() +/// @description 1.0 at combo 0, rising by a tenth per step. Capped so a +/// long run cannot run away with the scoreboard. +function kernel_combo_multiplier() { + return min(1 + global.kernel_combo * 0.1, 4); +} + +/// @function kernel_combo_bump() +/// @description Extend the combo by one and track the run's best. +function kernel_combo_bump() { + global.kernel_combo += 1; + global.kernel_combo_best = max(global.kernel_combo_best, global.kernel_combo); +} + +/// @function kernel_combo_break() +function kernel_combo_break() { + global.kernel_combo = 0; +} + +/// @function kernel_combo_best() +function kernel_combo_best() { + return global.kernel_combo_best; +} diff --git a/prefabs/kernel/scripts/scr_kernel_state/scr_kernel_state.gml b/prefabs/kernel/scripts/scr_kernel_state/scr_kernel_state.gml new file mode 100644 index 00000000..024e4c39 --- /dev/null +++ b/prefabs/kernel/scripts/scr_kernel_state/scr_kernel_state.gml @@ -0,0 +1,115 @@ +/// The run lifecycle. Five states, one seeded RNG, one place that +/// decides what "playing" means — so a pack never has to ask. +/// +/// States are a struct from a function rather than an enum: enums are +/// not namespaced and share one flat global table across every +/// installed prefab, so two components declaring `State` would collide +/// and hard-error at load. + +/// @function kernel_states() +/// @description The five lifecycle states, as a struct of ints. +function kernel_states() { + static _s = { boot: 0, title: 1, play: 2, pause: 3, over: 4 }; + return _s; +} + +/// @function kernel_boot(seed) +/// @description Initialise every kernel global. Call once, from the +/// project's boot object, before anything else touches the kernel. +/// A seed of -1 picks a random one and records it, so a run is always +/// reproducible after the fact. +function kernel_boot(seed = -1) { + if (seed < 0) seed = irandom(999999); + global.kernel_seed = seed; + random_set_seed(seed); + + global.kernel_state = kernel_states().boot; + global.kernel_elapsed = 0; + global.kernel_score = 0; + global.kernel_combo = 0; + global.kernel_combo_best = 0; + global.kernel_flags = {}; + global.kernel_save = {}; + global.kernel_data_fn = undefined; + global.kernel_tuning = {}; + global.kernel_result = ""; + + // Chrome is on by default — a score on screen from the first frame is + // worth real time — but a prefab must never be the last word on what + // the player sees, so both are switchable. + global.kernel_hud = true; + global.kernel_pause = true; + + kernel_save_load(); +} + +/// @function kernel_state() +/// @description The current lifecycle state. +function kernel_state() { + return global.kernel_state; +} + +/// @function kernel_state_set(state) +/// @description Move to `state`. Entering `play` resets the run's score +/// and clock; entering `over` records the result for the results screen. +function kernel_state_set(state) { + var _s = kernel_states(); + if (state == _s.play && global.kernel_state != _s.pause) { + global.kernel_score = 0; + global.kernel_combo = 0; + global.kernel_combo_best = 0; + global.kernel_elapsed = 0; + } + global.kernel_state = state; +} + +/// @function kernel_playing() +/// @description True while the game should simulate. Every pack gates +/// its Step on this, which is the whole reason pause works everywhere +/// without a pack knowing pause exists. +function kernel_playing() { + return global.kernel_state == kernel_states().play; +} + +/// @function kernel_tick() +/// @description Advance the run clock. Call once per step from the +/// kernel controller; frame-rate independent. +function kernel_tick() { + if (kernel_playing()) global.kernel_elapsed += delta_time / 1000000; +} + +/// @function kernel_elapsed() +/// @description Seconds spent in `play` this run. +function kernel_elapsed() { + return global.kernel_elapsed; +} + +/// @function kernel_game_over(result) +/// @description End the run with a short result string ("cleared", +/// "out of time") and bank the score. +function kernel_game_over(result = "") { + global.kernel_result = result; + kernel_save_high_score(global.kernel_score); + kernel_state_set(kernel_states().over); +} + +/// @function kernel_result() +/// @description Why the last run ended. +function kernel_result() { + return global.kernel_result; +} + +/// @function kernel_hud_visible(on) +/// @description Turn the built-in HUD off when you draw your own. The +/// kernel still tracks score and combo; it just stops drawing them. +function kernel_hud_visible(on) { + global.kernel_hud = on; +} + +/// @function kernel_pause_enabled(on) +/// @description Turn off the kernel's own pause — both the key and the +/// overlay. Anything that owns pausing itself (a freeze menu, say) must +/// call this, or one keypress drives two pause systems at once. +function kernel_pause_enabled(on) { + global.kernel_pause = on; +} diff --git a/prefabs/kernel/thumbnail.png b/prefabs/kernel/thumbnail.png new file mode 100644 index 00000000..436663f0 Binary files /dev/null and b/prefabs/kernel/thumbnail.png differ diff --git a/prefabs/levels/LICENSE b/prefabs/levels/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/levels/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/levels/README.md b/prefabs/levels/README.md new file mode 100644 index 00000000..351459d6 --- /dev/null +++ b/prefabs/levels/README.md @@ -0,0 +1,3 @@ +# levels + +Level 2 of 5, the exit that opens when you have earned it, and the cut between them. diff --git a/prefabs/levels/demo.gametest.json b/prefabs/levels/demo.gametest.json new file mode 100644 index 00000000..713ff692 --- /dev/null +++ b/prefabs/levels/demo.gametest.json @@ -0,0 +1,121 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 160, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "global.gt_level = 1; global.gt_took = false; global.gt_open = false; global.gt_busy = false;" + }, + "watch": { + "gml": "if (instance_exists(obj_demo)) { global.gt_level = max(global.gt_level, level_number()); if (obj_demo.run.taken > 0) global.gt_took = true; if (levels_gate_open(obj_demo.run)) global.gt_open = true; if (levels_busy(obj_demo.run)) global.gt_busy = true; }" + }, + "total": { + "gml": "return levels_total();" + }, + "reached": { + "gml": "return global.gt_level;" + }, + "gate_shut_at_first": { + "gml": "return global.gt_took;" + }, + "gate_opened": { + "gml": "return global.gt_open;" + }, + "covered": { + "gml": "return global.gt_busy;" + } + }, + "timeline": [ + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 20, + "inputs": [ + { + "key": "vk_enter", + "state": "down" + } + ] + }, + { + "frame": 22, + "inputs": [ + { + "key": "vk_enter", + "state": "up" + } + ] + }, + { + "frame": 156, + "assert": { + "id": "total", + "name": "the order holds two levels", + "expect": 2 + } + }, + { + "frame": 156, + "assert": { + "id": "gate_shut_at_first", + "name": "the gate counts what it is given", + "expect": true + } + }, + { + "frame": 156, + "assert": { + "id": "gate_opened", + "name": "collecting the last one opens the exit", + "expect": true + } + }, + { + "frame": 156, + "assert": { + "id": "covered", + "name": "the change goes through the cover", + "expect": true + } + }, + { + "frame": 156, + "assert": { + "id": "reached", + "name": "the exit advances to the next level", + "expect": 2 + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 1, + "until": 155, + "code": [ + "watch" + ] + } + ] +} diff --git a/prefabs/levels/objects/obj_demo/Create.gml b/prefabs/levels/objects/obj_demo/Create.gml new file mode 100644 index 00000000..1b268644 --- /dev/null +++ b/prefabs/levels/objects/obj_demo/Create.gml @@ -0,0 +1,6 @@ +::kernel::kernel_boot(); +run = levels_make(0.3); + +// A real game counts what the room holds — `instance_number(obj_coin)`. The +// demo has no pickups, so it asks for one press instead. +levels_gate(run, 1); diff --git a/prefabs/levels/objects/obj_demo/Draw_GUI.gml b/prefabs/levels/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..30e4b581 --- /dev/null +++ b/prefabs/levels/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,10 @@ +var _y = 24; +::kernel::kernel_draw_text(24, _y, "level " + string(level_number()) + " of " + + string(levels_total()), c_white, fa_left); +::kernel::kernel_draw_text(24, _y + 28, levels_gate_open(run) + ? "exit open — ENTER" + : "SPACE to collect " + string(levels_remaining(run)) + " more", c_white, fa_left); +::kernel::kernel_draw_text(24, _y + 56, "R restarts the level", c_white, fa_left); + +// Drawn last so the cover sits over the HUD as well as the room. +::transition::transition_draw_fade(run.transition, make_colour_rgb(0x0C, 0x0E, 0x16)); diff --git a/prefabs/levels/objects/obj_demo/Step.gml b/prefabs/levels/objects/obj_demo/Step.gml new file mode 100644 index 00000000..82818e56 --- /dev/null +++ b/prefabs/levels/objects/obj_demo/Step.gml @@ -0,0 +1,18 @@ +levels_step(run); +if (levels_busy(run)) exit; + +if (keyboard_check_pressed(vk_space)) levels_take(run); + +// The exit only answers once the gate is open, which is the whole point of +// having one. +if (keyboard_check_pressed(vk_enter) && levels_gate_open(run)) { + if (level_is_last()) { + ::kernel::kernel_game_over(true); + } else { + level_next(run); + // The next level asks for its own. + levels_gate(run, 1); + } +} + +if (keyboard_check_pressed(ord("R"))) level_restart(run); diff --git a/prefabs/levels/objects/obj_demo/object.toml b/prefabs/levels/objects/obj_demo/object.toml new file mode 100644 index 00000000..be7105c5 --- /dev/null +++ b/prefabs/levels/objects/obj_demo/object.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#object + +# The state performs the room change, so it has to outlive the room. A +# persistent controller is the other half of that: one instance for the run. +persistent = true diff --git a/prefabs/levels/prefab.toml b/prefabs/levels/prefab.toml new file mode 100644 index 00000000..1dc45659 --- /dev/null +++ b/prefabs/levels/prefab.toml @@ -0,0 +1,27 @@ +schema_version = 1 +name = "Several levels, in order" + +# A library: installed under `prefabs//` in the target project and +# called as `::levels::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +requires = ["kernel", "transition"] + +steps = [ + "Adding a level is authoring the room and appending it to `[room_order]` — nothing in GML names a specific room, so nothing else changes.", + "Hold the state in a global and the controller persistent: it performs the room change, so it has to outlive the room it starts in.", + "Call `levels_gate(state, instance_number(obj_coin))` from the level's room-start, not its create — create runs as each instance is made, so a controller placed first would count a half-built room.", + "Gate gameplay on `levels_busy` and stand still while it is true. A player who keeps running behind the cover arrives somewhere the level did not put them.", +] + +description = "A run through several rooms in order. Which level you are on and how many there are is derived by walking `[room_order]`, so it cannot disagree with the order the game plays. Advancing goes through the cover so the room changes on the frame the screen is opaque, and never re-boots the kernel — a run's score belongs to the run, and a level that refunds its score on death is one you can farm by dying. The exit carries a count so it can wait for the coins, keys or switches a level asks for." +category = "library" +tags = ["capability:levels", "capability:progression", "capability:rooms", "role:library"] + +exclude = ["objects/obj_demo", "rooms/rm_demo", "rooms/rm_demo_2"] + +[tuning] +# The cover, each way. Long enough to read as a beat, short enough that a +# player replaying a level is not waiting on it. +cover_seconds = 0.3 diff --git a/prefabs/levels/project.toml b/prefabs/levels/project.toml new file mode 100644 index 00000000..59ceacab --- /dev/null +++ b/prefabs/levels/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "levels" + +# Two rooms, because one cannot demonstrate an order. +[room_order] +order = ["rm_demo", "rm_demo_2"] diff --git a/prefabs/levels/rooms/rm_demo/layers/Background.toml b/prefabs/levels/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/levels/rooms/rm_demo/layers/Instances.toml b/prefabs/levels/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/levels/rooms/rm_demo/room.toml b/prefabs/levels/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/levels/rooms/rm_demo_2/layers/Background.toml b/prefabs/levels/rooms/rm_demo_2/layers/Background.toml new file mode 100644 index 00000000..684486f2 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo_2/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#161A24" diff --git a/prefabs/levels/rooms/rm_demo_2/layers/Instances.toml b/prefabs/levels/rooms/rm_demo_2/layers/Instances.toml new file mode 100644 index 00000000..a6203120 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo_2/layers/Instances.toml @@ -0,0 +1,5 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +# No controller here: obj_demo is persistent and arrives with the run. diff --git a/prefabs/levels/rooms/rm_demo_2/room.toml b/prefabs/levels/rooms/rm_demo_2/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/levels/rooms/rm_demo_2/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/levels/scripts/scr_levels/scr_levels.gml b/prefabs/levels/scripts/scr_levels/scr_levels.gml new file mode 100644 index 00000000..3c9ee38d --- /dev/null +++ b/prefabs/levels/scripts/scr_levels/scr_levels.gml @@ -0,0 +1,42 @@ +/// A run through several rooms in order, and the exit that ends each one. +/// +/// Which level you are on is derived from `[room_order]` rather than stored, +/// so it cannot disagree with the order the game actually plays: adding a +/// level is authoring a room and appending one line to `project.toml`. +/// Advancing goes through ::transition:: so the room changes on the frame the +/// screen is covered, and never re-boots ::kernel:: — a run's score belongs to +/// the run, not to the level. + +/// @function levels_make(cover_seconds) +/// @description The state one game needs: how the screen covers, and how many +/// of the thing the exit is waiting for. Hold it in a global rather than on a +/// controller, so it survives the room change it is performing. +function levels_make(cover_seconds = 0.3) { + return { + transition: ::transition::transition_make(cover_seconds), + needed: 0, + taken: 0, + }; +} + +/// @function levels_step(state) +/// @description Advance the transition. Call once a frame from the object +/// that owns the state. Returns true on the frame a change finishes. +function levels_step(state) { + return ::transition::transition_step(state.transition); +} + +/// @function levels_busy(state) +/// @description True while the screen is covering, changing or uncovering. +/// Gameplay reads this and stands still: a player who kept running behind a +/// black screen arrives somewhere the level did not put them. +function levels_busy(state) { + return ::transition::transition_busy(state.transition); +} + +/// @function levels_cover(state) +/// @description How covered the screen is, 0..1, for the object that draws it. +function levels_cover(state) { + return ::transition::transition_cover(state.transition); +} + diff --git a/prefabs/levels/scripts/scr_levels_gate/scr_levels_gate.gml b/prefabs/levels/scripts/scr_levels_gate/scr_levels_gate.gml new file mode 100644 index 00000000..f5b1f166 --- /dev/null +++ b/prefabs/levels/scripts/scr_levels_gate/scr_levels_gate.gml @@ -0,0 +1,29 @@ +/// @function levels_gate(state, needed) +/// @description Say how many of the thing this level's exit is waiting for — +/// coins, keys, switches. Call at the start of a level, once the room's +/// contents exist; `instance_number(obj_coin)` is the usual argument. +function levels_gate(state, needed) { + state.needed = needed; + state.taken = 0; +} + +/// @function levels_take(state) +/// @description Count one toward the gate. Returns true on the one that opens +/// it, which is the moment worth telling the player about. +function levels_take(state) { + state.taken += 1; + return state.taken == state.needed; +} + +/// @function levels_gate_open(state) +/// @description Whether the exit is open. A level that asked for nothing is +/// open from the start, which is what `>=` buys over `==`. +function levels_gate_open(state) { + return state.taken >= state.needed; +} + +/// @function levels_remaining(state) +/// @description How many are still out there, for the HUD. +function levels_remaining(state) { + return max(0, state.needed - state.taken); +} diff --git a/prefabs/levels/scripts/scr_levels_order/scr_levels_order.gml b/prefabs/levels/scripts/scr_levels_order/scr_levels_order.gml new file mode 100644 index 00000000..938657db --- /dev/null +++ b/prefabs/levels/scripts/scr_levels_order/scr_levels_order.gml @@ -0,0 +1,62 @@ +/// @function level_number() +/// @description Which level this is, counting from 1, by walking the room +/// order from its first room. +function level_number() { + var _n = 1; + var _r = levels_first(); + while (_r != -1 && _r != room) { + _r = room_next(_r); + _n += 1; + } + return _n; +} + +/// @function levels_total() +/// @description How many rooms the order holds. +function levels_total() { + var _n = 1; + var _r = levels_first(); + while (room_next(_r) != -1) { + _r = room_next(_r); + _n += 1; + } + return _n; +} + +/// @function level_is_last() +/// @description Whether finishing this room finishes the run. `room_next` +/// returns -1 past the end, which is the only thing that marks it. +function level_is_last() { + return room_next(room) == -1; +} + +/// @function levels_first() +/// @description The room the order starts in — `room_first`, named for what +/// it means here. +function levels_first() { + return room_first; +} + +/// @function level_goto(state, target_room) +/// @description Change level through the cover. A second call while one is +/// running is ignored, so a held key cannot skip a level. +function level_goto(state, target_room) { + return ::transition::transition_go(state.transition, target_room); +} + +/// @function level_next(state) +/// @description Advance to the next room in the order. Returns false in the +/// last room, where the caller ends the run instead. +function level_next(state) { + if (level_is_last()) return false; + return level_goto(state, room_next(room)); +} + +/// @function level_restart(state) +/// @description Play this level again, through the same cover. The score is +/// the kernel's and is deliberately untouched — a level that refunds its own +/// score on death is a level you can farm by dying. +function level_restart(state) { + return level_goto(state, room); +} + diff --git a/prefabs/levels/thumbnail.png b/prefabs/levels/thumbnail.png new file mode 100644 index 00000000..1a7affbe Binary files /dev/null and b/prefabs/levels/thumbnail.png differ diff --git a/prefabs/lighting/LICENSE b/prefabs/lighting/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/lighting/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/lighting/README.md b/prefabs/lighting/README.md new file mode 100644 index 00000000..164111a2 --- /dev/null +++ b/prefabs/lighting/README.md @@ -0,0 +1,3 @@ +# lighting + +A sheet of night with holes cut in it — torches, lamps and cones. diff --git a/prefabs/lighting/objects/obj_demo/CleanUp.gml b/prefabs/lighting/objects/obj_demo/CleanUp.gml new file mode 100644 index 00000000..14add1ee --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/CleanUp.gml @@ -0,0 +1,2 @@ +// A surface left behind on every room change is a leak. +light_free(night); diff --git a/prefabs/lighting/objects/obj_demo/Create.gml b/prefabs/lighting/objects/obj_demo/Create.gml new file mode 100644 index 00000000..210435de --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/Create.gml @@ -0,0 +1,4 @@ +night = light_make(0.92, make_colour_rgb(8, 10, 28)); +torch_dir = 0; +lamps = [[220, 180], [740, 180], [480, 420]]; +lit_lamp = false; diff --git a/prefabs/lighting/objects/obj_demo/Draw.gml b/prefabs/lighting/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..0e5b502e --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/Draw.gml @@ -0,0 +1,12 @@ +// Something to actually light. +draw_set_colour(c_gray); +for (var i = 0; i < array_length(lamps); i++) { + draw_circle(lamps[i][0], lamps[i][1], 18, false); +} +draw_set_colour(c_teal); +for (var _x = 60; _x < room_width; _x += 120) { + draw_rectangle(_x, 300, _x + 60, 340, false); +} +draw_set_colour(c_white); + +light_draw(night); diff --git a/prefabs/lighting/objects/obj_demo/Draw_GUI.gml b/prefabs/lighting/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..b07c1249 --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,2 @@ +draw_text(40, 30, "lighting demo — the mouse carries a cone torch, SPACE dims the night"); +draw_text(40, 50, "first lamp lit: " + (lit_lamp ? "yes" : "no")); diff --git a/prefabs/lighting/objects/obj_demo/Step.gml b/prefabs/lighting/objects/obj_demo/Step.gml new file mode 100644 index 00000000..510f87fc --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/Step.gml @@ -0,0 +1,12 @@ +torch_dir = point_direction(mouse_x, mouse_y, room_width / 2, room_height / 2); +if (keyboard_check_pressed(vk_space)) night.dark = night.dark > 0.5 ? 0.35 : 0.92; + +// Lights are declared every frame by whoever owns them — in Step, so the +// query below is per-frame logic rather than per-draw. +for (var i = 0; i < array_length(lamps); i++) { + light_add(night, lamps[i][0], lamps[i][1], 130, 0.9); +} +light_add_cone(night, mouse_x, mouse_y, 260, torch_dir, 40, 1); + +// Ask before drawing — `light_draw` clears the queue. +lit_lamp = light_lit(night, lamps[0][0], lamps[0][1]); diff --git a/prefabs/lighting/objects/obj_demo/object.toml b/prefabs/lighting/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/lighting/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/lighting/prefab.toml b/prefabs/lighting/prefab.toml new file mode 100644 index 00000000..1e3765e0 --- /dev/null +++ b/prefabs/lighting/prefab.toml @@ -0,0 +1,17 @@ +schema_version = 1 +name = "Darkness and lights" + +# A library: installed under `prefabs//` in the target project and +# called as `::lighting::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "A surface filled with night and lights subtracted from it, with a gradient falloff so a light reads as light rather than as a hole in a sheet of paper. Lights are declared every frame by whatever owns them, so one that is destroyed simply stops adding itself. Every call re-checks the surface: a surface is volatile, and the frame after the OS frees one draws nothing — a game that looks perfect until it does not, on a machine you do not have." +category = "library" +tags = ["capability:lighting", "capability:rendering", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "Draw the lighting after the world and before the HUD — lighting the score display is rarely what anyone wants.", + "Call `light_free` in a Cleanup event. A surface left behind on every room change is a leak the game only notices an hour in.", +] diff --git a/prefabs/lighting/project.toml b/prefabs/lighting/project.toml new file mode 100644 index 00000000..8159a4bc --- /dev/null +++ b/prefabs/lighting/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "lighting" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/lighting/rooms/rm_demo/layers/Background.toml b/prefabs/lighting/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/lighting/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/lighting/rooms/rm_demo/layers/Instances.toml b/prefabs/lighting/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/lighting/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/lighting/rooms/rm_demo/room.toml b/prefabs/lighting/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/lighting/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/lighting/scripts/scr_lighting/scr_lighting.gml b/prefabs/lighting/scripts/scr_lighting/scr_lighting.gml new file mode 100644 index 00000000..42fd0038 --- /dev/null +++ b/prefabs/lighting/scripts/scr_lighting/scr_lighting.gml @@ -0,0 +1,54 @@ +/// Darkness with holes cut in it. +/// +/// A surface filled with night, then lights subtracted from it. The whole +/// technique rests on one fact that bites everyone once: **a surface is +/// volatile**. The OS can free it whenever the window resizes, the display +/// sleeps, or memory gets tight, and the next frame that draws to a freed +/// surface draws nothing — a game that looks perfect until it does not, in +/// a way no test catches because it depends on the machine. +/// +/// So every function here re-checks `surface_exists` and rebuilds. Never +/// hold a surface across frames without that check. + +/// @function light_make(darkness, colour) +/// @description Lighting state. `darkness` is 0..1 — how black the unlit +/// parts are — and `colour` tints the night (a deep blue reads better than +/// pure black almost everywhere). +function light_make(darkness, colour) { + return { surf: -1, dark: clamp(darkness, 0, 1), tint: colour, lights: [] }; +} + +/// @function light_add(state, x, y, radius, strength) +/// @description Queue a light for this frame. Cleared by `light_draw`, so +/// lights are declared every frame by whatever owns them — a torch adds its +/// own, and stops adding it the moment it is destroyed. +function light_add(state, x, y, radius, strength) { + array_push(state.lights, { + x: x, y: y, r: max(1, radius), s: clamp(strength, 0, 1), + }); +} + +/// @function light_add_cone(state, x, y, radius, direction, spread, strength) +/// @description A light that only reaches one way — a torch, a headlamp. +/// Approximated as overlapping circles along the facing, which is cheaper +/// than a real cone and indistinguishable once it is blurred by the falloff. +function light_add_cone(state, x, y, radius, direction, spread, strength) { + var _steps = 4; + for (var i = 0; i < _steps; i++) { + var _t = (i + 1) / _steps; + var _d = radius * _t; + light_add(state, + x + lengthdir_x(_d * 0.6, direction), + y + lengthdir_y(_d * 0.6, direction), + radius * (0.35 + spread / 180 * _t), strength); + } +} + +/// @function light_free(state) +/// @description Release the surface. Call in Cleanup — a surface left +/// behind on every room change is a leak the game only notices an hour in. +function light_free(state) { + if (surface_exists(state.surf)) surface_free(state.surf); + state.surf = -1; +} + diff --git a/prefabs/lighting/scripts/scr_lighting_draw/scr_lighting_draw.gml b/prefabs/lighting/scripts/scr_lighting_draw/scr_lighting_draw.gml new file mode 100644 index 00000000..073f74a7 --- /dev/null +++ b/prefabs/lighting/scripts/scr_lighting_draw/scr_lighting_draw.gml @@ -0,0 +1,67 @@ +/// @function light_draw(state) +/// @description Draw the darkness with this frame's lights cut out of it, +/// then clear the queue. Call once, in a Draw event after the world and +/// before the HUD — lighting the score display is rarely what anyone wants. +function light_draw(state) { + var _w = room_width; + var _h = room_height; + + // Volatile: rebuild whenever it has gone, which is not rare. + if (!surface_exists(state.surf)) { + state.surf = surface_create(_w, _h); + } + + surface_set_target(state.surf); + draw_clear_alpha(state.tint, state.dark); + + // Subtractive blending punches the lights out of the night rather than + // painting light on top of it, which is what keeps the unlit parts fully + // dark instead of washing everything to grey. + gpu_set_blendmode(bm_subtract); + for (var i = 0; i < array_length(state.lights); i++) { + var _l = state.lights[i]; + light_falloff(_l.x, _l.y, _l.r, _l.s); + } + gpu_set_blendmode(bm_normal); + surface_reset_target(); + + draw_surface(state.surf, 0, 0); + state.lights = []; +} + +/// How many rings approximate one light's gradient. More is smoother and +/// costs a draw call each; ten is indistinguishable from twenty at the sizes +/// a light is usually drawn. +#macro LIGHT_FALLOFF_RINGS 10 +/// Per-ring alpha. The rings overlap, so this is well below 1 — raising it +/// makes the light's centre blow out to a hard white disc. +#macro LIGHT_FALLOFF_STEP 0.35 + +/// @function light_falloff(x, y, radius, strength) +/// @description One light's gradient, as rings from the centre out. A hard +/// circle reads as a hole in a sheet of paper; the gradient is what makes it +/// read as light. Shaped by `LIGHT_FALLOFF_RINGS` and `LIGHT_FALLOFF_STEP`. +function light_falloff(x, y, radius, strength) { + var _rings = LIGHT_FALLOFF_RINGS; + for (var i = _rings; i >= 1; i--) { + var _t = i / _rings; + var _a = strength * (1 - _t) * LIGHT_FALLOFF_STEP; + draw_set_alpha(_a); + draw_circle_colour(x, y, radius * _t, c_white, c_white, false); + } + draw_set_alpha(1); +} + +/// @function light_lit(state, x, y) +/// @description Whether a point is inside any light queued this frame. For +/// gameplay that depends on it — a monster that only moves in the dark — +/// without reading pixels back off the surface, which is far slower. +/// +/// Ask before `light_draw`, which clears the queue. +function light_lit(state, x, y) { + for (var i = 0; i < array_length(state.lights); i++) { + var _l = state.lights[i]; + if (point_distance(x, y, _l.x, _l.y) <= _l.r) return true; + } + return false; +} diff --git a/prefabs/lighting/thumbnail.png b/prefabs/lighting/thumbnail.png new file mode 100644 index 00000000..0ded3565 Binary files /dev/null and b/prefabs/lighting/thumbnail.png differ diff --git a/prefabs/main_menu/LICENSE b/prefabs/main_menu/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/main_menu/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/main_menu/README.md b/prefabs/main_menu/README.md new file mode 100644 index 00000000..b6be4542 --- /dev/null +++ b/prefabs/main_menu/README.md @@ -0,0 +1,23 @@ +# main_menu + +Keyboard and mouse menu — arrows or hover to select, Space or click to confirm. + +## The one line worth not deleting + +Hover only takes the selection when the mouse **moved this frame**: + +```gml +if (_p.x != last_mouse.x || _p.y != last_mouse.y) { ... } +``` + +Without that gate, arrow-key navigation snaps back to whatever item the +resting cursor happens to sit over, on every single step. It looks like a +bug in the keyboard handling and it is not. Anyone simplifying this file +will be tempted to remove it. + +## What comes from where + +Hit-testing, hover state and layout are `::ui_board::` — a prefab, so you +call it rather than reading it. Selection, wrapping and what each item +does are in the object this prefab gave you, because those are the parts +you change. diff --git a/prefabs/main_menu/objects/obj_demo/Create.gml b/prefabs/main_menu/objects/obj_demo/Create.gml new file mode 100644 index 00000000..3586ec06 --- /dev/null +++ b/prefabs/main_menu/objects/obj_demo/Create.gml @@ -0,0 +1,4 @@ +// The kernel owns the run state, and `kernel_playing()` reads it. Without a +// boot it is simply not set, and the first object to ask dies with a bare +// "not set before reading it" naming a kernel script rather than the caller. +::kernel::kernel_boot(); diff --git a/prefabs/main_menu/objects/obj_demo/object.toml b/prefabs/main_menu/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/main_menu/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/main_menu/objects/obj_menu/Create.gml b/prefabs/main_menu/objects/obj_menu/Create.gml new file mode 100644 index 00000000..d2557b2e --- /dev/null +++ b/prefabs/main_menu/objects/obj_menu/Create.gml @@ -0,0 +1,34 @@ +var _cfg = main_menu_tuning(); +items = main_menu_data("items"); + +board = ::ui_board::ui_board_make(); +for (var i = 0; i < array_length(items); i++) { + ::ui_board::ui_add(board, items[i].id, 0, 0, 0, 0, items[i].label, i); +} +// One column: a vertical list is a grid one wide, so the layout is the +// same call the quiz uses for a 2x2 answer block. +::ui_board::ui_layout_grid(board, 1, + (::kernel::kernel_gui_width() - _cfg.item_w) / 2, 200, + _cfg.item_w, _cfg.item_h, _cfg.gap); + +selected = 0; +last_mouse = ::kernel::kernel_pointer(); + +/// Act on a menu id. This is the branch you edit. +// Named `on_choose`, not `choose`: `choose` is a GML built-in, and a bare +// assignment to one does not create an instance variable — reading it back +// gives the function's index rather than what you stored. +on_choose = function(id) { + ::feel::feel_hitstop(0.05); + switch (id) { + case "play": room_goto_next(); break; + case "how": show_debug_message("wire up your how-to-play room"); break; + case "quit": game_end(); break; + } +}; + +/// Move the keyboard selection, wrapping at both ends. +move_by = function(dir) { + var _n = array_length(items); + selected = (selected + dir + _n) mod _n; +}; diff --git a/prefabs/main_menu/objects/obj_menu/Draw_GUI.gml b/prefabs/main_menu/objects/obj_menu/Draw_GUI.gml new file mode 100644 index 00000000..4cd9b294 --- /dev/null +++ b/prefabs/main_menu/objects/obj_menu/Draw_GUI.gml @@ -0,0 +1,6 @@ +// Tint tracks the keyboard selection so both input paths show the same +// highlight; ui_board owns hover, this owns "selected". +for (var i = 0; i < array_length(board.spots); i++) { + ::ui_board::ui_set_tint(board, board.spots[i].id, i == selected ? c_aqua : c_white); +} +::ui_board::ui_draw(board); diff --git a/prefabs/main_menu/objects/obj_menu/Step.gml b/prefabs/main_menu/objects/obj_menu/Step.gml new file mode 100644 index 00000000..b0570a78 --- /dev/null +++ b/prefabs/main_menu/objects/obj_menu/Step.gml @@ -0,0 +1,21 @@ +if (::kernel::kernel_action_pressed("up")) move_by(-1); +if (::kernel::kernel_action_pressed("down")) move_by(1); + +// Hover only steals the selection when the mouse actually MOVED this +// frame. Without this gate, arrow-key navigation snaps back to whatever +// item the resting cursor happens to sit over — the single most annoying +// bug in a menu that supports both. +var _p = ::kernel::kernel_pointer(); +if (_p.x != last_mouse.x || _p.y != last_mouse.y) { + for (var i = 0; i < array_length(board.spots); i++) { + if (::ui_board::ui_contains(board.spots[i], _p.x, _p.y)) selected = i; + } +} +last_mouse = _p; + +var _hit = ::ui_board::ui_update(board); +if (_hit != "") { + on_choose(_hit); +} else if (::kernel::kernel_action_pressed("confirm")) { + on_choose(items[selected].id); +} diff --git a/prefabs/main_menu/objects/obj_menu/object.toml b/prefabs/main_menu/objects/obj_menu/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/main_menu/objects/obj_menu/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/main_menu/prefab.toml b/prefabs/main_menu/prefab.toml new file mode 100644 index 00000000..58c5027c --- /dev/null +++ b/prefabs/main_menu/prefab.toml @@ -0,0 +1,44 @@ +schema_version = 1 +name = "Main menu" +description = "Keyboard and mouse menu — arrows or hover to select, Space or click to confirm." +category = "component" +tags = ["capability:ui", "role:template"] + +install = "copy" +requires = ["kernel", "ui_board", "feel"] + +seam = "objects/obj_menu/Step.gml" + +steps = [ + "Place obj_menu on an instance layer in your menu room, alongside ::kernel::obj_kernel.", + "Edit the `items` table in the generated data script, and the matching branch in obj_menu's `choose` method.", +] + +traps = [ + "Hover only takes the selection on a frame where the mouse actually moved. Without that gate, arrow-key navigation snaps back to whatever item the resting cursor happens to sit over — it looks like the keyboard is being ignored, and only on machines where the pointer is parked over the menu.", +] + +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +[roles] +obj_menu = "the menu: builds the ui_board spots from `[[content.items]]`, and reconciles keyboard selection with mouse hover" + +[tuning] +item_w = 320 +item_h = 64 +gap = 14 + +# `id` is what `on_choose` switches on, and what `ui_add` stores as the +# spot's key. Without it every item reads back `undefined` and obj_menu +# dies in its own Create the first time it is placed in a room. +[[content.items]] +id = "play" +label = "Play" + +[[content.items]] +id = "how" +label = "How to play" + +[[content.items]] +id = "quit" +label = "Quit" diff --git a/prefabs/main_menu/project.toml b/prefabs/main_menu/project.toml new file mode 100644 index 00000000..ddcd2971 --- /dev/null +++ b/prefabs/main_menu/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "main-menu" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/main_menu/rooms/rm_demo/layers/Background.toml b/prefabs/main_menu/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/main_menu/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/main_menu/rooms/rm_demo/layers/Instances.toml b/prefabs/main_menu/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2ac9e6e7 --- /dev/null +++ b/prefabs/main_menu/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_menu" +object = "obj_menu" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/main_menu/rooms/rm_demo/room.toml b/prefabs/main_menu/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/main_menu/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/main_menu/thumbnail.png b/prefabs/main_menu/thumbnail.png new file mode 100644 index 00000000..00f6d9e6 Binary files /dev/null and b/prefabs/main_menu/thumbnail.png differ diff --git a/prefabs/maze_chase/LICENSE b/prefabs/maze_chase/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/maze_chase/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/maze_chase/README.md b/prefabs/maze_chase/README.md new file mode 100644 index 00000000..7a228956 --- /dev/null +++ b/prefabs/maze_chase/README.md @@ -0,0 +1,74 @@ +# maze_chase + +Eat every dot before the ghosts corner you — they route round the walls. + +## Ghosts route, they do not home + +A chaser that moves straight at the player presses into the wall between you +and it, forever. `::pathgrid::` runs A* over the maze so a wall genuinely +hides you and the ghost comes round. + +Recomputing every frame is wasted work on a maze this size; recomputing too +rarely means walking towards a wall you already left. `repath_seconds` +controls that, and 0.35 is a reasonable middle. + +An empty route is a normal state, not an error — it means the target cell is +itself a wall. The ghost simply does not move that tick. + +## Speed is a design constraint, not a difficulty knob + +Raising `ghost_speed` past `hero_speed` makes the game *unwinnable* rather +than hard: a router that is strictly faster than you closes every distance +eventually, with no counterplay. Difficulty belongs in the number of ghosts +and the maze shape. + +## The maze is a tile layer; the actors are instances + +The walls live in `rooms/rm_play/layers/Tiles.toml`, and that layer is the only +copy of the maze. Read the shape back out of it, and edit it there: + +```console +$ gmx autotile shape rm_play +15 rows ↓ × 28 cols → — ops address row,col, zero-based + 0 10 20 + 0123456789012345678901234567 + 0 ############################ + 1 #............##............# + 2 #.####.#####.##.#####.####.# +``` + +``` +gmx autotile shape rm_play --op 'rect 7,12 7,15 clear' +``` + +opens a passage and redraws the tiles in the same run. The tile index is never +yours to pick: each filled cell gets the tile that fits its neighbours from +`ts_maze`'s `[autotile]` table, so a corridor's corners and dead ends are drawn +correctly without the shape saying anything about them. + +The dots, hero and ghosts are real entries in +`rooms/rm_play/layers/Instances.toml`, so any of them can be dragged in a +visual editor. + +**Which of the two a thing belongs in** is decided by whether the game ever +counts or destroys it. A dot is an instance because `instance_number(obj_dot)` +is how a cleared board is detected. A wall is a tile because nothing counts, +destroys or moves one — it only ever answers "is this solid", and 222 instances +answering that were 222 entries carrying no other information. + +Both halves are read rather than created: `obj_maze` counts the dots and blocks +the pathfinding grid from the tile layer in **Room Start**, not Create. Create +runs as each instance is made, so a controller placed first in the layer would +survey a half-built room. + +## The i-frame window + +Contact with a ghost costs a life, but only once per `iframe_seconds`. Without +it a ghost that stays overlapping takes all three lives in three frames, and +the game validates, builds and boots perfectly while being unplayable. + +## What it does not do + +No power pellets, no ghost states, no tunnels. Ghost behaviour is one mode — +always route to the player — and giving each a personality is the first thing +to add. diff --git a/prefabs/maze_chase/demo.gametest.json b/prefabs/maze_chase/demo.gametest.json new file mode 100644 index 00000000..d8952051 --- /dev/null +++ b/prefabs/maze_chase/demo.gametest.json @@ -0,0 +1,121 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 140, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_x0=undefined; global.gt_moved=false; global.gt_dots=0; global.gt_g0=undefined; global.gt_ghost=false; global.gt_hp=1;" + }, + "watch": { + "gml": "if (instance_exists(obj_hero)) { with (obj_hero) { global.gt_seen = true; global.gt_hp = min(global.gt_hp, (vitals.hp / vitals.max)); if (global.gt_x0 == undefined) global.gt_x0 = x; if (abs(x - global.gt_x0) > 8) global.gt_moved = true; } } if (instance_exists(obj_ghost)) { var _g = instance_find(obj_ghost, 0); if (global.gt_g0 == undefined) global.gt_g0 = { x: _g.x, y: _g.y }; if (point_distance(_g.x, _g.y, global.gt_g0.x, global.gt_g0.y) > 8) global.gt_ghost = true; } global.gt_dots = max(global.gt_dots, instance_number(obj_dot));" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "moved": { + "gml": "return global.gt_moved;" + }, + "dots": { + "gml": "return global.gt_dots > 0;" + }, + "ghost": { + "gml": "return global.gt_ghost;" + }, + "hurt": { + "gml": "return global.gt_hp < 1;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 90, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 130, + "assert": { + "id": "seen", + "name": "the hero is in the maze", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "dots", + "name": "the maze is laid out with dots to eat", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "moved", + "name": "holding a direction walks the hero", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "ghost", + "name": "a ghost routes toward the hero rather than standing still", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "hurt", + "name": "a ghost that reaches the hero costs health", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 128, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/maze_chase/objects/obj_dot/object.toml b/prefabs/maze_chase/objects/obj_dot/object.toml new file mode 100644 index 00000000..3fbbc90b --- /dev/null +++ b/prefabs/maze_chase/objects/obj_dot/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_dot" diff --git a/prefabs/maze_chase/objects/obj_ghost/Create.gml b/prefabs/maze_chase/objects/obj_ghost/Create.gml new file mode 100644 index 00000000..476c2fcf --- /dev/null +++ b/prefabs/maze_chase/objects/obj_ghost/Create.gml @@ -0,0 +1,7 @@ +move_speed = ::kernel::kernel_tuning("ghost_speed", 2); +route = []; +repath = 0; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +repath_seconds = ::kernel::kernel_tuning("repath_seconds", 0.35); diff --git a/prefabs/maze_chase/objects/obj_ghost/Step.gml b/prefabs/maze_chase/objects/obj_ghost/Step.gml new file mode 100644 index 00000000..24b9a2fc --- /dev/null +++ b/prefabs/maze_chase/objects/obj_ghost/Step.gml @@ -0,0 +1,25 @@ +if (!::kernel::kernel_playing()) exit; + +var _hero = instance_find(obj_hero, 0); +var _ctl = instance_find(obj_maze, 0); +if (_hero == noone || _ctl == noone) exit; + +// Recomputing every frame is wasted work; too rarely and it walks into a +// wall you have already left. +repath -= delta_time / 1000000; +if (repath <= 0) { + repath = repath_seconds; + var _from = ::pathgrid::pathgrid_cell_of(_ctl.nav, x, y); + var _to = ::pathgrid::pathgrid_cell_of(_ctl.nav, _hero.x, _hero.y); + route = ::pathgrid::pathgrid_find(_ctl.nav, _from.col, _from.row, _to.col, _to.row); +} + +// An empty route is normal — the player is standing in a wall cell, or +// there is genuinely no way round. +var _next = ::pathgrid::pathgrid_next_point(_ctl.nav, route); +if (_next == undefined) exit; + +var _dir = point_direction(x, y, _next.x, _next.y); +x += lengthdir_x(move_speed, _dir); +y += lengthdir_y(move_speed, _dir); +if (point_distance(x, y, _next.x, _next.y) < move_speed) array_delete(route, 0, 1); diff --git a/prefabs/maze_chase/objects/obj_ghost/object.toml b/prefabs/maze_chase/objects/obj_ghost/object.toml new file mode 100644 index 00000000..62bb848d --- /dev/null +++ b/prefabs/maze_chase/objects/obj_ghost/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_ghost" diff --git a/prefabs/maze_chase/objects/obj_hero/Create.gml b/prefabs/maze_chase/objects/obj_hero/Create.gml new file mode 100644 index 00000000..568925fd --- /dev/null +++ b/prefabs/maze_chase/objects/obj_hero/Create.gml @@ -0,0 +1,13 @@ +move_speed = ::kernel::kernel_tuning("hero_speed", 3); +vitals = ::health::health_make( + ::kernel::kernel_tuning("hero_lives", 3), + ::kernel::kernel_tuning("iframe_seconds", 1.5)); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +dot_score = ::kernel::kernel_tuning("dot_score", 10); + +// The maze walls are the room's `Tiles` layer, not instances — resolved once +// here because `layer_get_id` per axis per frame is a lookup for an answer +// that cannot change while the room is loaded. +walls = ::motion::motion_tilemap("Tiles"); diff --git a/prefabs/maze_chase/objects/obj_hero/Draw.gml b/prefabs/maze_chase/objects/obj_hero/Draw.gml new file mode 100644 index 00000000..07f87b73 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_hero/Draw.gml @@ -0,0 +1,2 @@ +if (::health::health_invulnerable(vitals) && (current_time div 90) mod 2 == 0) exit; +draw_self(); diff --git a/prefabs/maze_chase/objects/obj_hero/Step.gml b/prefabs/maze_chase/objects/obj_hero/Step.gml new file mode 100644 index 00000000..29ade36b --- /dev/null +++ b/prefabs/maze_chase/objects/obj_hero/Step.gml @@ -0,0 +1,23 @@ +if (!::kernel::kernel_playing()) exit; +::health::health_step(vitals); + +// Axis at a time against the walls, so a corner does not stick. +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +var _dy = keyboard_check(vk_down) - keyboard_check(vk_up); +if (_dx != 0 && !::motion::motion_blocked(noone, walls, x + _dx * move_speed, y)) x += _dx * move_speed; +if (_dy != 0 && !::motion::motion_blocked(noone, walls, x, y + _dy * move_speed)) y += _dy * move_speed; + +var _dot = instance_place(x, y, obj_dot); +if (_dot != noone) { + ::kernel::kernel_score_add(dot_score); + instance_destroy(_dot); +} + +// The i-frame window is what stops a ghost that stays overlapping taking +// every life in three frames. +if (place_meeting(x, y, obj_ghost)) { + if (::health::health_hurt(vitals, 1)) { + ::feel::feel_shake(0.25, 8); + ::feel::feel_hitstop(0.08); + } +} diff --git a/prefabs/maze_chase/objects/obj_hero/object.toml b/prefabs/maze_chase/objects/obj_hero/object.toml new file mode 100644 index 00000000..62159d85 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_hero/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_hero" diff --git a/prefabs/maze_chase/objects/obj_maze/Create.gml b/prefabs/maze_chase/objects/obj_maze/Create.gml new file mode 100644 index 00000000..f1abffe0 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_maze/Create.gml @@ -0,0 +1,6 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +cell = ::kernel::kernel_tuning("cell", 32); +total_dots = 0; +nav = ::pathgrid::pathgrid_make(1, 1, cell); diff --git a/prefabs/maze_chase/objects/obj_maze/Draw_GUI.gml b/prefabs/maze_chase/objects/obj_maze/Draw_GUI.gml new file mode 100644 index 00000000..02810d80 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_maze/Draw_GUI.gml @@ -0,0 +1,8 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, + "dots " + string(instance_number(obj_dot)) + " / " + string(total_dots)); +var _hero = instance_find(obj_hero, 0); +if (_hero != noone) { + ::kernel::kernel_draw_text(16, 64, + "lives " + string(_hero.vitals.hp)); +} diff --git a/prefabs/maze_chase/objects/obj_maze/Other_RoomStart.gml b/prefabs/maze_chase/objects/obj_maze/Other_RoomStart.gml new file mode 100644 index 00000000..e1e62aab --- /dev/null +++ b/prefabs/maze_chase/objects/obj_maze/Other_RoomStart.gml @@ -0,0 +1,14 @@ +// The walls are the room's `Tiles` layer; the dots, hero and ghosts are placed +// instances in the same room. Edit either in the room editor — the tile layer +// through `gmx autotile shape rm_play`. This only reads what is there. +// +// Room Start rather than Create: Create runs as each instance is made, so a +// controller placed first in the layer would survey a half-built room. +total_dots = instance_number(obj_dot); + +nav = ::pathgrid::pathgrid_make( + ceil(room_width / cell), ceil(room_height / cell), cell); +// Walls are terrain, so the nav grid is blocked from the tile layer. The +// grid's cell matches the tileset's tile size, which is what lets a cell be +// read as one tile. +::pathgrid::pathgrid_block_tilemap(nav, ::pathgrid::pathgrid_tilemap("Tiles")); diff --git a/prefabs/maze_chase/objects/obj_maze/Step.gml b/prefabs/maze_chase/objects/obj_maze/Step.gml new file mode 100644 index 00000000..9f3b751f --- /dev/null +++ b/prefabs/maze_chase/objects/obj_maze/Step.gml @@ -0,0 +1,16 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); + +if (instance_number(obj_dot) == 0) { + ::kernel::kernel_game_over("cleared"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); + exit; +} + +var _hero = instance_find(obj_hero, 0); +if (_hero != noone && ::health::health_dead(_hero.vitals)) { + ::kernel::kernel_game_over("caught"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); +} diff --git a/prefabs/maze_chase/objects/obj_maze/object.toml b/prefabs/maze_chase/objects/obj_maze/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/maze_chase/objects/obj_maze/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/maze_chase/objects/obj_results/Draw_GUI.gml b/prefabs/maze_chase/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/maze_chase/objects/obj_results/Step.gml b/prefabs/maze_chase/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/maze_chase/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/maze_chase/objects/obj_results/object.toml b/prefabs/maze_chase/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/maze_chase/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/maze_chase/objects/obj_title/Create.gml b/prefabs/maze_chase/objects/obj_title/Create.gml new file mode 100644 index 00000000..e490fea0 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(maze_chase_data, maze_chase_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/maze_chase/objects/obj_title/Draw_GUI.gml b/prefabs/maze_chase/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..9ab3283e --- /dev/null +++ b/prefabs/maze_chase/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "MAZE CHASE", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "eat the dots, avoid the ghosts", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/maze_chase/objects/obj_title/Step.gml b/prefabs/maze_chase/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/maze_chase/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/maze_chase/objects/obj_title/object.toml b/prefabs/maze_chase/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/maze_chase/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/maze_chase/prefab.toml b/prefabs/maze_chase/prefab.toml new file mode 100644 index 00000000..3b72a8a8 --- /dev/null +++ b/prefabs/maze_chase/prefab.toml @@ -0,0 +1,37 @@ +schema_version = 1 +name = "Maze chase" +description = "Eat every dot before the ghosts corner you — they route round the walls." +category = "game" +tags = ["genre:arcade", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "pathgrid", "health", "motion"] + +seam = "rooms/rm_play/layers/Tiles.toml" + +steps = [ + "`maze_chase.gametest.json` came with the prefab and already passes: the hero is in the maze, the maze is laid out with dots, a direction key walks the hero, a ghost routes toward it rather than standing still, and a ghost that reaches the hero costs health. Run it with `gmx test maze_chase.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The walls are the `Tiles` layer of rm_play and are the only copy of the maze. `gmx autotile shape rm_play` prints it back as `#` and `.` with the axes numbered, and `gmx autotile shape rm_play --op 'rect 7,12 7,15 clear'` opens a passage and redraws the tiles in the same run. You never pick a tile index: each filled cell gets the tile that fits its neighbours from `ts_maze`'s `[autotile]` table, so a corridor's corners and ends are drawn without the shape saying so.", + "The dots, hero and ghosts are placed instances in the same room — drag them in the editor, or edit `rooms/rm_play/layers/Instances.toml`. A dot is an instance rather than a tile because the game counts it: `instance_number(obj_dot) == 0` is how a cleared board is detected. A wall is a tile because nothing ever counts or destroys one. That is the line between the two, and it is worth holding: 222 walls as instances were 222 entries that only ever answered \"is this solid\".", + "Ghosts route with A* rather than moving straight at you, so a wall actually hides you. Raising `ghost_speed` past the player's makes the game unwinnable, not hard.", +] + +[roles] +obj_maze = "builds the maze, counts the dots, and ends the round" +obj_hero = "moves cell to cell on the arrow keys; loses a life on contact" +obj_ghost = "routes to the player around the walls" +obj_dot = "one pellet; scores and vanishes" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "cleared or caught, and the best score" + +[tuning] +cell = 32 +hero_speed = 3 +ghost_speed = 2 +hero_lives = 3 +iframe_seconds = 1.5 +dot_score = 10 +# How often a ghost recomputes its route, in seconds. Every frame is wasted +# work on a maze this size; too rare and it walks into a wall you just left. +repath_seconds = 0.35 diff --git a/prefabs/maze_chase/project.toml b/prefabs/maze_chase/project.toml new file mode 100644 index 00000000..50bbf0cd --- /dev/null +++ b/prefabs/maze_chase/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "maze_chase" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/maze_chase/rooms/rm_play/layers/Background.toml b/prefabs/maze_chase/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/maze_chase/rooms/rm_play/layers/Instances.toml b/prefabs/maze_chase/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..986f625b --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,1198 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_maze" +object = "obj_maze" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_rm_play_obj_dot_0" +object = "obj_dot" +x = 48.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_1" +object = "obj_dot" +x = 80.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_2" +object = "obj_dot" +x = 112.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_3" +object = "obj_dot" +x = 144.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_4" +object = "obj_dot" +x = 176.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_5" +object = "obj_dot" +x = 208.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_6" +object = "obj_dot" +x = 240.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_7" +object = "obj_dot" +x = 272.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_8" +object = "obj_dot" +x = 304.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_9" +object = "obj_dot" +x = 336.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_10" +object = "obj_dot" +x = 368.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_11" +object = "obj_dot" +x = 400.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_12" +object = "obj_dot" +x = 496.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_13" +object = "obj_dot" +x = 528.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_14" +object = "obj_dot" +x = 560.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_15" +object = "obj_dot" +x = 592.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_16" +object = "obj_dot" +x = 624.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_17" +object = "obj_dot" +x = 656.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_18" +object = "obj_dot" +x = 688.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_19" +object = "obj_dot" +x = 720.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_20" +object = "obj_dot" +x = 752.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_21" +object = "obj_dot" +x = 784.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_22" +object = "obj_dot" +x = 816.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_23" +object = "obj_dot" +x = 848.0 +y = 48.0 + +[[entries]] +id = "inst_rm_play_obj_dot_24" +object = "obj_dot" +x = 48.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_25" +object = "obj_dot" +x = 208.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_26" +object = "obj_dot" +x = 400.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_27" +object = "obj_dot" +x = 496.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_28" +object = "obj_dot" +x = 688.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_29" +object = "obj_dot" +x = 848.0 +y = 80.0 + +[[entries]] +id = "inst_rm_play_obj_dot_30" +object = "obj_dot" +x = 48.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_31" +object = "obj_dot" +x = 80.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_32" +object = "obj_dot" +x = 112.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_33" +object = "obj_dot" +x = 144.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_34" +object = "obj_dot" +x = 176.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_35" +object = "obj_dot" +x = 208.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_36" +object = "obj_dot" +x = 240.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_37" +object = "obj_dot" +x = 272.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_38" +object = "obj_dot" +x = 304.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_39" +object = "obj_dot" +x = 336.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_40" +object = "obj_dot" +x = 368.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_41" +object = "obj_dot" +x = 400.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_42" +object = "obj_dot" +x = 432.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_43" +object = "obj_dot" +x = 464.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_44" +object = "obj_dot" +x = 496.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_45" +object = "obj_dot" +x = 528.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_46" +object = "obj_dot" +x = 560.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_47" +object = "obj_dot" +x = 592.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_48" +object = "obj_dot" +x = 624.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_49" +object = "obj_dot" +x = 656.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_50" +object = "obj_dot" +x = 688.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_51" +object = "obj_dot" +x = 720.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_52" +object = "obj_dot" +x = 752.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_53" +object = "obj_dot" +x = 784.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_54" +object = "obj_dot" +x = 816.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_55" +object = "obj_dot" +x = 848.0 +y = 112.0 + +[[entries]] +id = "inst_rm_play_obj_dot_56" +object = "obj_dot" +x = 48.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_57" +object = "obj_dot" +x = 208.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_58" +object = "obj_dot" +x = 304.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_59" +object = "obj_dot" +x = 592.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_60" +object = "obj_dot" +x = 688.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_61" +object = "obj_dot" +x = 848.0 +y = 144.0 + +[[entries]] +id = "inst_rm_play_obj_dot_62" +object = "obj_dot" +x = 48.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_63" +object = "obj_dot" +x = 80.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_64" +object = "obj_dot" +x = 112.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_65" +object = "obj_dot" +x = 144.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_66" +object = "obj_dot" +x = 176.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_67" +object = "obj_dot" +x = 208.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_68" +object = "obj_dot" +x = 304.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_69" +object = "obj_dot" +x = 336.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_70" +object = "obj_dot" +x = 368.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_71" +object = "obj_dot" +x = 400.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_72" +object = "obj_dot" +x = 496.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_73" +object = "obj_dot" +x = 528.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_74" +object = "obj_dot" +x = 560.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_75" +object = "obj_dot" +x = 592.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_76" +object = "obj_dot" +x = 688.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_77" +object = "obj_dot" +x = 720.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_78" +object = "obj_dot" +x = 752.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_79" +object = "obj_dot" +x = 784.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_80" +object = "obj_dot" +x = 816.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_81" +object = "obj_dot" +x = 848.0 +y = 176.0 + +[[entries]] +id = "inst_rm_play_obj_dot_82" +object = "obj_dot" +x = 208.0 +y = 208.0 + +[[entries]] +id = "inst_rm_play_obj_dot_83" +object = "obj_dot" +x = 400.0 +y = 208.0 + +[[entries]] +id = "inst_rm_play_obj_dot_84" +object = "obj_dot" +x = 496.0 +y = 208.0 + +[[entries]] +id = "inst_rm_play_obj_dot_85" +object = "obj_dot" +x = 688.0 +y = 208.0 + +[[entries]] +id = "inst_rm_play_obj_dot_86" +object = "obj_dot" +x = 48.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_87" +object = "obj_dot" +x = 80.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_88" +object = "obj_dot" +x = 112.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_89" +object = "obj_dot" +x = 144.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_90" +object = "obj_dot" +x = 176.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_91" +object = "obj_dot" +x = 208.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_92" +object = "obj_dot" +x = 240.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_93" +object = "obj_dot" +x = 272.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_ghost_0" +object = "obj_ghost" +x = 304.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_94" +object = "obj_dot" +x = 336.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_95" +object = "obj_dot" +x = 368.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_96" +object = "obj_dot" +x = 400.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_97" +object = "obj_dot" +x = 432.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_hero_0" +object = "obj_hero" +x = 464.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_98" +object = "obj_dot" +x = 496.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_99" +object = "obj_dot" +x = 528.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_ghost_1" +object = "obj_ghost" +x = 560.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_100" +object = "obj_dot" +x = 592.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_101" +object = "obj_dot" +x = 624.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_102" +object = "obj_dot" +x = 656.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_103" +object = "obj_dot" +x = 688.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_104" +object = "obj_dot" +x = 720.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_105" +object = "obj_dot" +x = 752.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_106" +object = "obj_dot" +x = 784.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_107" +object = "obj_dot" +x = 816.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_108" +object = "obj_dot" +x = 848.0 +y = 240.0 + +[[entries]] +id = "inst_rm_play_obj_dot_109" +object = "obj_dot" +x = 208.0 +y = 272.0 + +[[entries]] +id = "inst_rm_play_obj_dot_110" +object = "obj_dot" +x = 400.0 +y = 272.0 + +[[entries]] +id = "inst_rm_play_obj_dot_111" +object = "obj_dot" +x = 496.0 +y = 272.0 + +[[entries]] +id = "inst_rm_play_obj_dot_112" +object = "obj_dot" +x = 688.0 +y = 272.0 + +[[entries]] +id = "inst_rm_play_obj_dot_113" +object = "obj_dot" +x = 48.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_114" +object = "obj_dot" +x = 80.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_115" +object = "obj_dot" +x = 112.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_116" +object = "obj_dot" +x = 144.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_117" +object = "obj_dot" +x = 176.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_118" +object = "obj_dot" +x = 208.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_119" +object = "obj_dot" +x = 304.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_120" +object = "obj_dot" +x = 336.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_121" +object = "obj_dot" +x = 368.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_122" +object = "obj_dot" +x = 400.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_123" +object = "obj_dot" +x = 496.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_124" +object = "obj_dot" +x = 528.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_125" +object = "obj_dot" +x = 560.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_126" +object = "obj_dot" +x = 592.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_127" +object = "obj_dot" +x = 688.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_128" +object = "obj_dot" +x = 720.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_129" +object = "obj_dot" +x = 752.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_130" +object = "obj_dot" +x = 784.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_131" +object = "obj_dot" +x = 816.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_132" +object = "obj_dot" +x = 848.0 +y = 304.0 + +[[entries]] +id = "inst_rm_play_obj_dot_133" +object = "obj_dot" +x = 48.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_134" +object = "obj_dot" +x = 208.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_135" +object = "obj_dot" +x = 304.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_136" +object = "obj_dot" +x = 592.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_137" +object = "obj_dot" +x = 688.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_138" +object = "obj_dot" +x = 848.0 +y = 336.0 + +[[entries]] +id = "inst_rm_play_obj_dot_139" +object = "obj_dot" +x = 48.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_140" +object = "obj_dot" +x = 80.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_141" +object = "obj_dot" +x = 112.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_142" +object = "obj_dot" +x = 144.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_143" +object = "obj_dot" +x = 176.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_144" +object = "obj_dot" +x = 208.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_145" +object = "obj_dot" +x = 240.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_146" +object = "obj_dot" +x = 272.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_147" +object = "obj_dot" +x = 304.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_148" +object = "obj_dot" +x = 336.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_149" +object = "obj_dot" +x = 368.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_150" +object = "obj_dot" +x = 400.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_151" +object = "obj_dot" +x = 432.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_152" +object = "obj_dot" +x = 464.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_153" +object = "obj_dot" +x = 496.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_154" +object = "obj_dot" +x = 528.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_155" +object = "obj_dot" +x = 560.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_156" +object = "obj_dot" +x = 592.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_157" +object = "obj_dot" +x = 624.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_158" +object = "obj_dot" +x = 656.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_159" +object = "obj_dot" +x = 688.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_160" +object = "obj_dot" +x = 720.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_161" +object = "obj_dot" +x = 752.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_162" +object = "obj_dot" +x = 784.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_163" +object = "obj_dot" +x = 816.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_164" +object = "obj_dot" +x = 848.0 +y = 368.0 + +[[entries]] +id = "inst_rm_play_obj_dot_165" +object = "obj_dot" +x = 48.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_166" +object = "obj_dot" +x = 208.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_167" +object = "obj_dot" +x = 400.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_168" +object = "obj_dot" +x = 496.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_169" +object = "obj_dot" +x = 688.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_170" +object = "obj_dot" +x = 848.0 +y = 400.0 + +[[entries]] +id = "inst_rm_play_obj_dot_171" +object = "obj_dot" +x = 48.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_172" +object = "obj_dot" +x = 80.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_173" +object = "obj_dot" +x = 112.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_174" +object = "obj_dot" +x = 144.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_175" +object = "obj_dot" +x = 176.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_176" +object = "obj_dot" +x = 208.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_177" +object = "obj_dot" +x = 240.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_178" +object = "obj_dot" +x = 272.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_179" +object = "obj_dot" +x = 304.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_180" +object = "obj_dot" +x = 336.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_181" +object = "obj_dot" +x = 368.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_182" +object = "obj_dot" +x = 400.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_183" +object = "obj_dot" +x = 496.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_184" +object = "obj_dot" +x = 528.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_185" +object = "obj_dot" +x = 560.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_186" +object = "obj_dot" +x = 592.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_187" +object = "obj_dot" +x = 624.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_188" +object = "obj_dot" +x = 656.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_189" +object = "obj_dot" +x = 688.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_190" +object = "obj_dot" +x = 720.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_191" +object = "obj_dot" +x = 752.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_192" +object = "obj_dot" +x = 784.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_193" +object = "obj_dot" +x = 816.0 +y = 432.0 + +[[entries]] +id = "inst_rm_play_obj_dot_194" +object = "obj_dot" +x = 848.0 +y = 432.0 + diff --git a/prefabs/maze_chase/rooms/rm_play/layers/Tiles.toml b/prefabs/maze_chase/rooms/rm_play/layers/Tiles.toml new file mode 100644 index 00000000..e7ef9703 --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_play/layers/Tiles.toml @@ -0,0 +1,54 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "tiles" +depth = 50 +tileset = "ts_maze" + +[autotile] +seed = 0 + +# Generated by `gmx autotile apply` from this layer's terrain. +# 28×15 tiles (47-tile set "default"). +# Redraw it with `gmx autotile shape` or the room editor; edits here +# are overwritten. +[legend] +"." = { tile = 0 } # empty +"2" = { tile = 51 } # E| +"3" = { tile = 49 } # NE|NE +"4" = { tile = 48 } # NE| +"6" = { tile = 40 } # NS| +"7" = { tile = 43 } # ES|SE +"8" = { tile = 42 } # ES| +"9" = { tile = 27 } # NES|NE,SE +"c" = { tile = 24 } # NES| +"d" = { tile = 53 } # W| +"e" = { tile = 47 } # NW|NW +"f" = { tile = 46 } # NW| +"g" = { tile = 41 } # EW| +"i" = { tile = 37 } # NEW|NW +"j" = { tile = 38 } # NEW|NE +"l" = { tile = 45 } # SW|SW +"m" = { tile = 44 } # SW| +"n" = { tile = 35 } # NSW|NW,SW +"q" = { tile = 32 } # NSW| +"s" = { tile = 30 } # ESW|SW +"t" = { tile = 29 } # ESW|SE + +[map] +rows = """ +7ggggggggggggstggggggggggggl +6............cq............6 +6.2ggd.2gggd.4f.2gggd.2ggd.6 +6..........................6 +6.2ggd.8m.2ggstggd.8m.2ggd.6 +6......cq....cq....cq......6 +9ggggd.4jggd.4f.2ggif.2ggggn +6..........................6 +9ggggd.8tggd.8m.2ggsm.2ggggn +6......cq....cq....cq......6 +6.2ggd.4f.2ggijggd.4f.2ggd.6 +6..........................6 +6.2ggd.2gggd.8m.2gggd.2ggd.6 +6............cq............6 +3ggggggggggggijgggggggggggge +""" diff --git a/prefabs/maze_chase/rooms/rm_play/room.toml b/prefabs/maze_chase/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/maze_chase/rooms/rm_results/layers/Background.toml b/prefabs/maze_chase/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/maze_chase/rooms/rm_results/layers/Instances.toml b/prefabs/maze_chase/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/maze_chase/rooms/rm_results/room.toml b/prefabs/maze_chase/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/maze_chase/rooms/rm_title/layers/Background.toml b/prefabs/maze_chase/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/maze_chase/rooms/rm_title/layers/Instances.toml b/prefabs/maze_chase/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/maze_chase/rooms/rm_title/room.toml b/prefabs/maze_chase/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/maze_chase/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/maze_chase/sprites/spr_dot/frame_000.png b/prefabs/maze_chase/sprites/spr_dot/frame_000.png new file mode 100644 index 00000000..a6f65611 Binary files /dev/null and b/prefabs/maze_chase/sprites/spr_dot/frame_000.png differ diff --git a/prefabs/maze_chase/sprites/spr_dot/sprite.toml b/prefabs/maze_chase/sprites/spr_dot/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/maze_chase/sprites/spr_dot/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/maze_chase/sprites/spr_ghost/frame_000.png b/prefabs/maze_chase/sprites/spr_ghost/frame_000.png new file mode 100644 index 00000000..8a0e8424 Binary files /dev/null and b/prefabs/maze_chase/sprites/spr_ghost/frame_000.png differ diff --git a/prefabs/maze_chase/sprites/spr_ghost/sprite.toml b/prefabs/maze_chase/sprites/spr_ghost/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/maze_chase/sprites/spr_ghost/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/maze_chase/sprites/spr_hero/frame_000.png b/prefabs/maze_chase/sprites/spr_hero/frame_000.png new file mode 100644 index 00000000..1a0f0f50 Binary files /dev/null and b/prefabs/maze_chase/sprites/spr_hero/frame_000.png differ diff --git a/prefabs/maze_chase/sprites/spr_hero/sprite.toml b/prefabs/maze_chase/sprites/spr_hero/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/maze_chase/sprites/spr_hero/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/maze_chase/sprites/spr_maze_sheet/frame_000.png b/prefabs/maze_chase/sprites/spr_maze_sheet/frame_000.png new file mode 100644 index 00000000..7cff61df Binary files /dev/null and b/prefabs/maze_chase/sprites/spr_maze_sheet/frame_000.png differ diff --git a/prefabs/maze_chase/sprites/spr_maze_sheet/sprite.toml b/prefabs/maze_chase/sprites/spr_maze_sheet/sprite.toml new file mode 100644 index 00000000..265fed33 --- /dev/null +++ b/prefabs/maze_chase/sprites/spr_maze_sheet/sprite.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#sprite diff --git a/prefabs/maze_chase/thumbnail.png b/prefabs/maze_chase/thumbnail.png new file mode 100644 index 00000000..a0da609c Binary files /dev/null and b/prefabs/maze_chase/thumbnail.png differ diff --git a/prefabs/maze_chase/tilesets/ts_maze/tileset.toml b/prefabs/maze_chase/tilesets/ts_maze/tileset.toml new file mode 100644 index 00000000..a30d6080 --- /dev/null +++ b/prefabs/maze_chase/tilesets/ts_maze/tileset.toml @@ -0,0 +1,62 @@ +#:schema ../../.gm-schema/v1.json#tileset + +# The sheet's first row is blank on purpose: tile index 0 is GameMaker's empty +# tile, so the 47-tile template starts on row 1 (`derive --origin 1,0`) and no +# art lands in a cell nothing can name. +sprite = "spr_maze_sheet" + +[tile_size] +width = 32 +height = 32 + +[autotile.default] +template = "sides_47" + +[autotile.default.tiles] +"ESW|" = 28 +"ESW|SE" = 29 +"ESW|SE,SW" = 31 +"ESW|SW" = 30 +"ES|" = 42 +"ES|SE" = 43 +"EW|" = 41 +"E|" = 51 +"NESW|" = 8 +"NESW|NE" = 10 +"NESW|NE,NW" = 11 +"NESW|NE,NW,SE" = 15 +"NESW|NE,NW,SE,SW" = 23 +"NESW|NE,NW,SW" = 19 +"NESW|NE,SE" = 14 +"NESW|NE,SE,SW" = 22 +"NESW|NE,SW" = 18 +"NESW|NW" = 9 +"NESW|NW,SE" = 13 +"NESW|NW,SE,SW" = 21 +"NESW|NW,SW" = 17 +"NESW|SE" = 12 +"NESW|SE,SW" = 20 +"NESW|SW" = 16 +"NES|" = 24 +"NES|NE" = 25 +"NES|NE,SE" = 27 +"NES|SE" = 26 +"NEW|" = 36 +"NEW|NE" = 38 +"NEW|NE,NW" = 39 +"NEW|NW" = 37 +"NE|" = 48 +"NE|NE" = 49 +"NSW|" = 32 +"NSW|NW" = 34 +"NSW|NW,SW" = 35 +"NSW|SW" = 33 +"NS|" = 40 +"NW|" = 46 +"NW|NW" = 47 +"N|" = 52 +"SW|" = 44 +"SW|SW" = 45 +"S|" = 50 +"W|" = 53 +"|" = 54 diff --git a/prefabs/motion/LICENSE b/prefabs/motion/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/motion/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/motion/README.md b/prefabs/motion/README.md new file mode 100644 index 00000000..ffca20b6 --- /dev/null +++ b/prefabs/motion/README.md @@ -0,0 +1,3 @@ +# motion + +Walk, run, jump and stop at walls — the movement every game needs. diff --git a/prefabs/motion/demo.gametest.json b/prefabs/motion/demo.gametest.json new file mode 100644 index 00000000..05255f55 --- /dev/null +++ b/prefabs/motion/demo.gametest.json @@ -0,0 +1,117 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 220, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "global.gt_landings = 0; global.gt_leaves = 0; global.gt_prev = obj_demo.jump.on_ground;" + }, + "watch": { + "gml": "if (instance_exists(obj_demo)) { var _g = obj_demo.jump.on_ground; if (_g && !global.gt_prev) global.gt_landings += 1; if (!_g && global.gt_prev) global.gt_leaves += 1; global.gt_prev = _g; }" + }, + "landings": { + "gml": "return global.gt_landings;" + }, + "leaves": { + "gml": "return global.gt_leaves;" + }, + "grounded_at_start": { + "gml": "return global.gt_prev;" + } + }, + "timeline": [ + { + "frame": 40, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 52, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 100, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 112, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 160, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 172, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 216, + "assert": { + "id": "grounded_at_start", + "name": "the demo has landed before the run starts", + "expect": true + } + }, + { + "frame": 216, + "assert": { + "id": "leaves", + "name": "three jumps leave the ground three times", + "expect": 3 + } + }, + { + "frame": 216, + "assert": { + "id": "landings", + "name": "and land exactly three times \u2014 a landing that reports twice is the sub-pixel bug", + "expect": 3 + } + }, + { + "frame": 30, + "code": [ + "init" + ] + }, + { + "frame": 31, + "every": 1, + "until": 215, + "code": [ + "watch" + ] + } + ] +} diff --git a/prefabs/motion/objects/obj_demo/Create.gml b/prefabs/motion/objects/obj_demo/Create.gml new file mode 100644 index 00000000..5461e96b --- /dev/null +++ b/prefabs/motion/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +// A jumping box in a room with three ledges — the shortest thing that +// shows the library working, including coyote time off a ledge edge. +jump = motion_platformer_make({ run_speed: 5, jump_strength: 11 }); diff --git a/prefabs/motion/objects/obj_demo/Draw_GUI.gml b/prefabs/motion/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..b9e897bc --- /dev/null +++ b/prefabs/motion/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1 @@ +draw_text(16, 16, "arrows to run, space to jump"); diff --git a/prefabs/motion/objects/obj_demo/Step.gml b/prefabs/motion/objects/obj_demo/Step.gml new file mode 100644 index 00000000..23867a72 --- /dev/null +++ b/prefabs/motion/objects/obj_demo/Step.gml @@ -0,0 +1,6 @@ +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +motion_platformer_step( + jump, obj_demo_wall, _dx, + keyboard_check(vk_space), keyboard_check_pressed(vk_space) +); +motion_clamp_to_room(); diff --git a/prefabs/motion/objects/obj_demo/object.toml b/prefabs/motion/objects/obj_demo/object.toml new file mode 100644 index 00000000..d8556182 --- /dev/null +++ b/prefabs/motion/objects/obj_demo/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_body" diff --git a/prefabs/motion/objects/obj_demo_wall/object.toml b/prefabs/motion/objects/obj_demo_wall/object.toml new file mode 100644 index 00000000..23710dd7 --- /dev/null +++ b/prefabs/motion/objects/obj_demo_wall/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_block" diff --git a/prefabs/motion/prefab.toml b/prefabs/motion/prefab.toml new file mode 100644 index 00000000..898c2e57 --- /dev/null +++ b/prefabs/motion/prefab.toml @@ -0,0 +1,18 @@ +schema_version = 1 +name = "Movement and collision" + +# A library: installed under `prefabs//` in the target project and +# called as `::motion::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +steps = [ + "A moving platform is `motion_carry(obj_player, dx, dy, obj_solid)` from the platform's Step, called *before* the rider takes its own step so the rider resolves gravity against where the platform now is. Riders are found by their feet — standing on something means one pixel above it, not overlapping it — and are moved through `motion_move_x`/`motion_move_y`, so a platform that shoves one into a wall leaves it flush rather than inside. Without this a moving platform is a wall that slides out from under you.", + "`obj_solid` is whatever the character must not pass through — pass `noone` when nothing blocks it yet.", + "Double jump, wall jump and bounce pads all go through `motion_platformer_jump(state, strength)` — the step owns `vy`, so assigning it yourself is overwritten on the next frame.", +] + +description = "Top-down and platformer movement against solid instances, resolved axis by axis so a wall stops you without sticking. Gravity, a jump with coyote time and a buffered press, and clamping to the room. The part of a game that is identical everywhere and easy to get subtly wrong." +category = "library" +tags = ["capability:movement", "capability:collision", "genre:platformer", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_wall", "rooms/rm_demo", "sprites/spr_demo_body", "sprites/spr_demo_block"] diff --git a/prefabs/motion/project.toml b/prefabs/motion/project.toml new file mode 100644 index 00000000..de8cb2fd --- /dev/null +++ b/prefabs/motion/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "motion" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/motion/rooms/rm_demo/layers/Background.toml b/prefabs/motion/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/motion/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/motion/rooms/rm_demo/layers/Instances.toml b/prefabs/motion/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..29751f70 --- /dev/null +++ b/prefabs/motion/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,249 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo_player" +object = "obj_demo" +x = 120.0 +y = 300.0 + +[[entries]] +id = "inst_demo_block_0" +object = "obj_demo_wall" +x = 0.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_1" +object = "obj_demo_wall" +x = 32.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_2" +object = "obj_demo_wall" +x = 64.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_3" +object = "obj_demo_wall" +x = 96.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_4" +object = "obj_demo_wall" +x = 128.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_5" +object = "obj_demo_wall" +x = 160.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_6" +object = "obj_demo_wall" +x = 192.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_7" +object = "obj_demo_wall" +x = 224.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_8" +object = "obj_demo_wall" +x = 256.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_9" +object = "obj_demo_wall" +x = 288.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_10" +object = "obj_demo_wall" +x = 320.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_11" +object = "obj_demo_wall" +x = 352.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_12" +object = "obj_demo_wall" +x = 384.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_13" +object = "obj_demo_wall" +x = 416.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_14" +object = "obj_demo_wall" +x = 448.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_15" +object = "obj_demo_wall" +x = 480.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_16" +object = "obj_demo_wall" +x = 512.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_17" +object = "obj_demo_wall" +x = 544.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_18" +object = "obj_demo_wall" +x = 576.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_19" +object = "obj_demo_wall" +x = 608.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_20" +object = "obj_demo_wall" +x = 640.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_21" +object = "obj_demo_wall" +x = 672.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_22" +object = "obj_demo_wall" +x = 704.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_23" +object = "obj_demo_wall" +x = 736.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_24" +object = "obj_demo_wall" +x = 768.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_25" +object = "obj_demo_wall" +x = 800.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_26" +object = "obj_demo_wall" +x = 832.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_27" +object = "obj_demo_wall" +x = 864.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_28" +object = "obj_demo_wall" +x = 896.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_29" +object = "obj_demo_wall" +x = 928.0 +y = 480.0 + +[[entries]] +id = "inst_demo_block_30" +object = "obj_demo_wall" +x = 320.0 +y = 380.0 + +[[entries]] +id = "inst_demo_block_31" +object = "obj_demo_wall" +x = 352.0 +y = 380.0 + +[[entries]] +id = "inst_demo_block_32" +object = "obj_demo_wall" +x = 384.0 +y = 380.0 + +[[entries]] +id = "inst_demo_block_33" +object = "obj_demo_wall" +x = 416.0 +y = 380.0 + +[[entries]] +id = "inst_demo_block_34" +object = "obj_demo_wall" +x = 448.0 +y = 380.0 + +[[entries]] +id = "inst_demo_block_35" +object = "obj_demo_wall" +x = 600.0 +y = 290.0 + +[[entries]] +id = "inst_demo_block_36" +object = "obj_demo_wall" +x = 632.0 +y = 290.0 + +[[entries]] +id = "inst_demo_block_37" +object = "obj_demo_wall" +x = 664.0 +y = 290.0 + +[[entries]] +id = "inst_demo_block_38" +object = "obj_demo_wall" +x = 696.0 +y = 290.0 + +[[entries]] +id = "inst_demo_block_39" +object = "obj_demo_wall" +x = 728.0 +y = 290.0 diff --git a/prefabs/motion/rooms/rm_demo/room.toml b/prefabs/motion/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/motion/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/motion/scripts/scr_motion/scr_motion.gml b/prefabs/motion/scripts/scr_motion/scr_motion.gml new file mode 100644 index 00000000..efb45f4b --- /dev/null +++ b/prefabs/motion/scripts/scr_motion/scr_motion.gml @@ -0,0 +1,156 @@ +/// Movement that stops at walls. +/// +/// Every function here resolves one axis at a time. Moving both at once +/// and then backing out of a wall is the classic way to get a player who +/// sticks to ceilings and slides along floors he should stand on — the +/// axes have to be independent so a blocked X never cancels a legal Y. +/// +/// Walls can be instances, tiles, or both. An instance works the moment you +/// place one and needs no tileset; a tile layer is one layer the editor can +/// paint instead of one instance per cell, and is what terrain usually wants +/// once there is art. Pass `noone` for whichever you are not using. + +/// @function motion_tilemap(layer_name) +/// @description The tilemap id of a layer by name, or `noone` if the room has +/// no such layer. Saves every caller the `layer_tilemap_get_id(layer_get_id(…))` +/// dance, and returns something safe in a room that lacks the layer rather +/// than a -1 that fails much later. +function motion_tilemap(layer_name) { + if (!layer_exists(layer_get_id(layer_name))) return noone; + var _tm = layer_tilemap_get_id(layer_get_id(layer_name)); + return (_tm == -1) ? noone : _tm; +} + +/// @function motion_tile_solid(tile_data) +/// @description Whether a cell read out of a tilemap is terrain. +/// +/// Two different values mean "not terrain" and only one of them is obvious. +/// Tile index 0 is GameMaker's empty tile, but a read outside the layer's +/// extent returns -1, and `-1 != 0` — so testing the raw value walls off +/// everything past the layer's edge. A tile layer smaller than its room +/// then becomes an invisible box around the level: a pit the player is +/// meant to fall out of quietly holds him up, and a patrol probing for the +/// floor ahead finds one where the layer merely stops. The high bits of +/// tile data also carry the mirror/flip flags, which is why the index has +/// to be masked out rather than compared whole. +function motion_tile_solid(tile_data) { + if (tile_data == -1) return false; + return tile_get_index(tile_data) != 0; +} + +/// @function motion_blocked(obj_solid, tilemap, px, py) +/// @scope instance +/// @description Whether this instance's mask would overlap something solid +/// with its origin at `(px, py)`. The one place that answers it, so the +/// instance and tile cases cannot drift apart — every move and the ground +/// test below go through here. +function motion_blocked(obj_solid, tilemap, px, py) { + if (obj_solid != noone && place_meeting(px, py, obj_solid)) return true; + if (tilemap == noone) return false; + + // The mask as it would sit at the candidate position: `bbox_*` describe + // it where the instance is now, so the offset moves it without moving + // the instance. + var _dx = px - x; + var _dy = py - y; + var _tw = max(1, tilemap_get_tile_width(tilemap)); + var _th = max(1, tilemap_get_tile_height(tilemap)); + + // Every cell the mask touches, not just its corners: a mover taller than + // one tile would otherwise straddle a wall with a corner either side of + // it and pass straight through. + // `bbox_right`/`bbox_bottom` sit one past the last pixel the mask covers, + // so dividing them straight reads the row or column beyond it: a body + // resting flush on a tile top reports itself blocked and cannot move. + var _cx = (bbox_left + _dx) div _tw; + var _cy = (bbox_top + _dy) div _th; + var _cx2 = (bbox_right + _dx - 1) div _tw; + var _cy2 = (bbox_bottom + _dy - 1) div _th; + for (var _iy = _cy; _iy <= _cy2; _iy++) { + for (var _ix = _cx; _ix <= _cx2; _ix++) { + if (motion_tile_solid(tilemap_get(tilemap, _ix, _iy))) return true; + } + } + return false; +} + +/// @function motion_solid_at(obj_solid, tilemap, px, py) +/// @scope instance +/// @description Whether the single point `(px, py)` is solid. +/// +/// The point form of [motion_blocked]. Use it for probes — "is there floor +/// just ahead of my feet" — where asking about the whole mask answers a +/// different question, and answers it wrong: a mask two cells wide finds +/// floor a mask-width away and a patrol walks off the ledge anyway. +function motion_solid_at(obj_solid, tilemap, px, py) { + if (obj_solid != noone && position_meeting(px, py, obj_solid)) return true; + if (tilemap == noone) return false; + return motion_tile_solid(tilemap_get_at_pixel(tilemap, px, py)); +} + +/// @function motion_move_x(obj_solid, amount, tilemap) +/// @scope instance +/// @description Move this instance `amount` pixels horizontally, stopping +/// flush against the first solid thing in the way. Returns true if something +/// blocked it. +function motion_move_x(obj_solid, amount, tilemap = noone) { + if (amount == 0) return false; + if (!motion_blocked(obj_solid, tilemap, x + amount, y)) { + x += amount; + return false; + } + // Step in one pixel at a time only once contact is known, so the + // common (unblocked) case stays a single check however fast you move. + var _step = sign(amount); + while (!motion_blocked(obj_solid, tilemap, x + _step, y)) { + x += _step; + } + return true; +} + +/// @function motion_move_y(obj_solid, amount, tilemap) +/// @scope instance +/// @description The vertical half of the same. Returns true if blocked. +function motion_move_y(obj_solid, amount, tilemap = noone) { + if (amount == 0) return false; + if (!motion_blocked(obj_solid, tilemap, x, y + amount)) { + y += amount; + return false; + } + var _step = sign(amount); + while (!motion_blocked(obj_solid, tilemap, x, y + _step)) { + y += _step; + } + return true; +} + +/// @function motion_clamp_to_room() +/// @scope instance +/// @description Keep this instance inside the room, measured from its +/// sprite's bounding box rather than its origin — an origin-centred +/// sprite otherwise leaves half of itself outside. +function motion_clamp_to_room() { + if (sprite_index == -1) { + x = clamp(x, 0, room_width); + y = clamp(y, 0, room_height); + return; + } + x = clamp(x, x - bbox_left, x + (room_width - bbox_right)); + y = clamp(y, y - bbox_top, y + (room_height - bbox_bottom)); +} + +/// @function motion_topdown(obj_solid, dx, dy, speed) +/// @scope instance +/// @description Eight-way movement. `dx`/`dy` are -1..1 direction, not +/// pixels. Diagonals are normalised, so holding two keys is not faster +/// than holding one — the bug you notice only after playtesting. +function motion_topdown(obj_solid, dx, dy, speed, tilemap = noone) { + var _len = point_distance(0, 0, dx, dy); + if (_len > 0) { + dx = dx / _len; + dy = dy / _len; + } + motion_move_x(obj_solid, dx * speed, tilemap); + motion_move_y(obj_solid, dy * speed, tilemap); +} + diff --git a/prefabs/motion/scripts/scr_motion_carry/scr_motion_carry.gml b/prefabs/motion/scripts/scr_motion_carry/scr_motion_carry.gml new file mode 100644 index 00000000..2bf6ed32 --- /dev/null +++ b/prefabs/motion/scripts/scr_motion_carry/scr_motion_carry.gml @@ -0,0 +1,58 @@ +/// Carrying a rider, which is the whole of what makes a moving platform +/// different from a wall that happens to move. + +/// @function motion_carry(obj_rider, dx, dy, obj_solid, tilemap) +/// @scope instance +/// @description Move this instance by (`dx`, `dy`) and take whatever is +/// standing on it along. Call it from the platform's Step *before* the +/// rider takes its own step, so the rider resolves its gravity against +/// where the platform now is rather than where it was. +/// +/// Riders are found by their feet, not by overlap: something standing on a +/// platform is not touching it, it is one pixel above. Each rider is moved +/// through `motion_move_x`/`motion_move_y`, so a platform that shoves a +/// rider into a wall leaves it flush against the wall instead of inside it. +/// +/// Which of the two moves first depends on the sign of `dy`, and getting it +/// wrong fails silently in one direction only: +/// +/// - Going **down or sideways** the platform leads. A rider stepped down +/// first is stopped by the platform still sitting under its feet, and +/// the platform then slides out from under it. +/// - Going **up** the rider leads. Move the platform first and it is now +/// overlapping the rider by `|dy|` pixels; if the rider's own move is +/// then blocked — a ceiling, another platform — the step-in loop finds +/// it already overlapping at one pixel and gives up, leaving it embedded +/// and pushed further in on every frame after. +/// +/// Returns the number of riders carried. +function motion_carry(obj_rider, dx, dy, obj_solid, tilemap = noone) { + // Collected before moving: once this instance has moved, the riders are + // no longer over it and the same test finds nobody. + var _riders = []; + with (obj_rider) { + if (place_meeting(x, y + 1, other.id)) array_push(_riders, id); + } + + var _count = array_length(_riders); + + // Rising: the riders go first, so the platform never moves into them. + if (dy < 0) { + for (var i = 0; i < _count; i++) { + with (_riders[i]) motion_move_y(obj_solid, dy, tilemap); + } + } + + x += dx; + y += dy; + + // Falling or level: the platform has moved out of the way, so the + // riders can follow it down without being blocked by it. + for (var i = 0; i < _count; i++) { + with (_riders[i]) { + motion_move_x(obj_solid, dx, tilemap); + if (dy > 0) motion_move_y(obj_solid, dy, tilemap); + } + } + return _count; +} diff --git a/prefabs/motion/scripts/scr_motion_platformer/scr_motion_platformer.gml b/prefabs/motion/scripts/scr_motion_platformer/scr_motion_platformer.gml new file mode 100644 index 00000000..d11eb6a7 --- /dev/null +++ b/prefabs/motion/scripts/scr_motion_platformer/scr_motion_platformer.gml @@ -0,0 +1,164 @@ +/// @function motion_platformer_make(tuning) +/// @description State a jumping character needs between frames. Pass a +/// struct to override any of the defaults; the numbers are pixels and +/// frames, at the room's speed. +function motion_platformer_make(tuning = {}) { + var _d = { + run_speed: 4, + gravity_per_frame: 0.5, + fall_max: 12, + jump_strength: 10, + // Frames after walking off a ledge where a jump still counts. + // Without it, players who press jump on the exact frame they + // leave the ground get nothing and blame the controls. + coyote_frames: 6, + // Frames a jump press is remembered while still airborne, so + // pressing slightly early lands as a jump on touchdown. + buffer_frames: 6, + // Extra launches allowed before touching ground again. 0 is a + // plain jump, 1 is a double jump. Refilled on landing. + air_jumps: 0, + // Strength of those extra launches. 0 means "same as the first", + // which is the usual choice; a smaller number makes the second + // jump a top-up rather than a second full arc. + air_jump_strength: 0, + // A tile layer to collide against, alongside whatever `obj_solid` + // the step is given. `motion_tilemap("Tiles")` produces one. + // `noone` means instances are the only walls. + tilemap: noone + }; + var _keys = variable_struct_get_names(tuning); + for (var i = 0; i < array_length(_keys); i++) { + _d[$ _keys[i]] = tuning[$ _keys[i]]; + } + _d.vy = 0; + _d.on_ground = false; + _d.coyote = 0; + _d.buffer = 0; + _d.spent_press = false; + _d.air_left = _d.air_jumps; + return _d; +} + +/// @function motion_platformer_jump(state, strength) +/// @description Launch the body upward right now, whatever it was doing. +/// +/// The hook for jumps the ground rules do not cover — wall jumps, a bounce +/// pad, a spring enemy. `motion_platformer_step` owns `vy`, so setting it +/// from outside fights the integrator on the next frame; this sets it the +/// way the step itself would. +/// +/// **Not needed for a double jump.** That is `air_jumps: 1` on the tuning +/// struct — the counter, the strength and refilling on landing are already +/// in the step. Hand-rolling it here is the slower road to the same place, +/// and it has to re-solve the press-buffer interaction below. +/// +/// `strength` is in the same units as `jump_strength` and is taken as a +/// magnitude, so passing a negative by mistake still goes up rather than +/// slamming the player into the floor. +function motion_platformer_jump(state, strength) { + state.vy = -abs(strength); + // Tell the step to ignore this frame's press. It re-arms `buffer` from the + // same `jump_pressed` that triggered this call, so a double jump within + // `buffer_frames` of the ground left a live buffer that spent itself the + // moment `on_ground` refreshed `coyote` on landing — a third jump nobody + // asked for. + state.spent_press = true; + // A jump that has just started is not standing on anything. Without + // this the same press can be spent twice — once here and once by the + // step's own coyote rule on the very next frame. + state.on_ground = false; + state.coyote = 0; + state.buffer = 0; + return state; +} + +/// @function motion_platformer_step(state, obj_solid, dx, jump_held, jump_pressed) +/// @scope instance +/// @description One frame of run-and-jump against `obj_solid`. `dx` is +/// -1..1. Mutates and returns `state`. +/// +/// Variable jump height comes from cutting upward velocity when the key +/// is released, not from a longer hold — that way a tap is always a small +/// hop and holding is always the full arc, at any frame rate. +/// +/// Double jump is `air_jumps: 1` on the tuning struct, not code you write +/// here: the extra launches, their strength, and refilling them on landing +/// are all handled below. +function motion_platformer_step(state, obj_solid, dx, jump_held, jump_pressed) { + motion_move_x(obj_solid, dx * state.run_speed, state.tilemap); + + state.vy = min(state.vy + state.gravity_per_frame, state.fall_max); + + if (jump_pressed && !state.spent_press) state.buffer = state.buffer_frames; + else if (state.buffer > 0) state.buffer -= 1; + state.spent_press = false; + + if (state.on_ground) state.coyote = state.coyote_frames; + else if (state.coyote > 0) state.coyote -= 1; + + if (state.buffer > 0 && state.coyote > 0) { + state.vy = -state.jump_strength; + state.buffer = 0; + state.coyote = 0; + } else if (jump_pressed && state.air_left > 0) { + // On the press itself, never out of the buffer: a buffered press is + // there to become a ground jump a frame later, and spending it in + // the air takes that away from a player who pressed slightly early. + var _air = (state.air_jump_strength > 0) ? state.air_jump_strength : state.jump_strength; + state.vy = -_air; + state.buffer = 0; + state.air_left -= 1; + } + if (state.vy < 0 && !jump_held) state.vy *= 0.5; + + var _hit = motion_move_y(obj_solid, state.vy, state.tilemap); + // Ground is what is under the mover, not whether this frame's move was + // blocked: the pixel step can stop a fraction short of a surface, and + // falling that fraction later would read as a second landing. + state.on_ground = state.vy >= 0 && motion_blocked(obj_solid, state.tilemap, x, y + 1); + if (_hit) state.vy = 0; + // Refill here rather than at the top, so the frame you land on is + // already a frame you can jump from again. + if (state.on_ground) state.air_left = state.air_jumps; + + return state; +} + +/// @function motion_platformer_stomp(state, obj_target, bounce) +/// @scope instance +/// @description Land on something from above: returns the instance stomped, +/// or `noone`. Bounces off it and gives the air jump back, so a chain of +/// enemies reads as one move rather than several. +/// +/// Only counts while falling and only from above, so walking into an enemy +/// is still walking into an enemy — which is the half that decides whether +/// this feels fair. `bounce` of 0 uses a little over half a jump. +/// +/// **Call it before whatever kills the player on contact.** Landing on an +/// enemy is a frame where both are true, and the first test wins — put this +/// second and a stomp reads as a death every time. +/// +/// The bounce is a launch like any other, so the variable-height rule applies +/// to it: `motion_platformer_step` halves upward velocity on every frame +/// `jump_held` is false, and a player who is not holding jump at the moment +/// of the stomp sees the arc cut to nothing. Count a few frames down after a +/// stomp and pass `jump_held` as true across them, and the bounce is one +/// fixed height rather than a coin flip on where the player's thumb was. +function motion_platformer_stomp(state, obj_target, bounce = 0) { + if (state.vy <= 0) return noone; + + var _hit = instance_place(x, y, obj_target); + if (_hit == noone) return noone; + + // Came down onto it rather than into its side: the feet have to be in + // the target's upper band at the moment of contact. + if (bbox_bottom > _hit.bbox_top + (_hit.bbox_bottom - _hit.bbox_top) * 0.5) { + return noone; + } + + state.vy = -((bounce > 0) ? bounce : state.jump_strength * 0.6); + state.air_left = state.air_jumps; + state.on_ground = false; + return _hit; +} diff --git a/prefabs/motion/sprites/spr_demo_block/frame_000.png b/prefabs/motion/sprites/spr_demo_block/frame_000.png new file mode 100644 index 00000000..27c19991 Binary files /dev/null and b/prefabs/motion/sprites/spr_demo_block/frame_000.png differ diff --git a/prefabs/motion/sprites/spr_demo_block/sprite.toml b/prefabs/motion/sprites/spr_demo_block/sprite.toml new file mode 100644 index 00000000..265fed33 --- /dev/null +++ b/prefabs/motion/sprites/spr_demo_block/sprite.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#sprite diff --git a/prefabs/motion/sprites/spr_demo_body/frame_000.png b/prefabs/motion/sprites/spr_demo_body/frame_000.png new file mode 100644 index 00000000..84d06327 Binary files /dev/null and b/prefabs/motion/sprites/spr_demo_body/frame_000.png differ diff --git a/prefabs/motion/sprites/spr_demo_body/sprite.toml b/prefabs/motion/sprites/spr_demo_body/sprite.toml new file mode 100644 index 00000000..3d0a099a --- /dev/null +++ b/prefabs/motion/sprites/spr_demo_body/sprite.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#sprite + +# Bottom-centre: a platformer character stands ON its y, so ledges and +# floors line up without per-draw half-height maths. +origin = "bottom" diff --git a/prefabs/motion/thumbnail.png b/prefabs/motion/thumbnail.png new file mode 100644 index 00000000..af6d7fac Binary files /dev/null and b/prefabs/motion/thumbnail.png differ diff --git a/prefabs/music_and_sfx/LICENSE b/prefabs/music_and_sfx/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/music_and_sfx/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/music_and_sfx/README.md b/prefabs/music_and_sfx/README.md new file mode 100644 index 00000000..88124dd0 --- /dev/null +++ b/prefabs/music_and_sfx/README.md @@ -0,0 +1,25 @@ +# music_and_sfx + +Looping music that survives room changes, plus one-shot effects. + +## The decision this makes for you + +**Compression mode, per sound.** This is the choice that actually matters +and the generator cannot make it, because it depends on the asset: + +- A multi-minute music track wants `compression = "streamed"`. Left + uncompressed it sits in RAM in full — tens of megabytes for one song. +- A short effect wants uncompressed. Streamed, it decodes on play, and the + latency is audible on something that has to land on a hit frame. + +Set it in the sound's `sound.toml` after adding it. + +## Adding a track + +``` +gmx prefab add track_and_street_racer/sounds/OGG_Racing_Track1 +``` + +Prefab resources are namespaced, so it is +`::track_and_street_racer::OGG_Racing_Track1` in GML, not the bare name. +Pass it to `music_play`. diff --git a/prefabs/music_and_sfx/objects/obj_audio/Create.gml b/prefabs/music_and_sfx/objects/obj_audio/Create.gml new file mode 100644 index 00000000..9ca6704b --- /dev/null +++ b/prefabs/music_and_sfx/objects/obj_audio/Create.gml @@ -0,0 +1,27 @@ +// A persistent object's Create runs again if a second one is ever placed; +// guarding on the global keeps the track playing instead of restarting it +// on every room change. +if (!variable_global_exists("music_handle")) global.music_handle = -1; + +/// @function music_play(snd) +/// @description Loop `snd`, replacing whatever is playing. +music_play = function(snd) { + if (global.music_handle != -1) audio_stop_sound(global.music_handle); + global.music_handle = audio_play_sound(snd, 100, true); + audio_sound_gain(global.music_handle, music_and_sfx_tuning().music_gain, 0); +}; + +/// @function music_stop(fade_ms) +music_stop = function(fade_ms = 400) { + if (global.music_handle == -1) return; + audio_sound_gain(global.music_handle, 0, fade_ms); + global.music_handle = -1; +}; + +/// @function sfx(snd) +/// @description One-shot. Not looped, not tracked — fire and forget. +sfx = function(snd) { + var _h = audio_play_sound(snd, 50, false); + audio_sound_gain(_h, music_and_sfx_tuning().sfx_gain, 0); + return _h; +}; diff --git a/prefabs/music_and_sfx/objects/obj_audio/object.toml b/prefabs/music_and_sfx/objects/obj_audio/object.toml new file mode 100644 index 00000000..d5d72d15 --- /dev/null +++ b/prefabs/music_and_sfx/objects/obj_audio/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +persistent = true diff --git a/prefabs/music_and_sfx/objects/obj_demo/Draw_GUI.gml b/prefabs/music_and_sfx/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..2a9460d5 --- /dev/null +++ b/prefabs/music_and_sfx/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,14 @@ +draw_text(40, 30, "music_and_sfx demo - every sound goes through cue_sfx / cue_music"); + +draw_text(40, 90, "controller: " + (instance_exists(obj_audio) + ? "obj_audio (persistent, survives room changes)" : "absent")); + +// A headless run has no audio device, so `audio_is_playing` reports false +// for everything. The cue counters are the only thing a test can read. +draw_text(40, 140, "cues fired: " + string(variable_global_exists("cue_count") + ? global.cue_count : 0)); +draw_text(40, 180, "last cue: " + (variable_global_exists("last_cue") + ? string(global.last_cue) : "none yet")); + +draw_text(40, 260, "music_gain " + string(music_and_sfx_tuning().music_gain)); +draw_text(40, 300, "sfx_gain " + string(music_and_sfx_tuning().sfx_gain)); diff --git a/prefabs/music_and_sfx/objects/obj_demo/object.toml b/prefabs/music_and_sfx/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/music_and_sfx/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/music_and_sfx/prefab.toml b/prefabs/music_and_sfx/prefab.toml new file mode 100644 index 00000000..8cade503 --- /dev/null +++ b/prefabs/music_and_sfx/prefab.toml @@ -0,0 +1,26 @@ +schema_version = 1 +name = "Music and sound effects" +description = "Looping music that survives room changes, plus one-shot effects." +category = "component" +tags = ["capability:audio", "role:template"] + +install = "copy" +requires = ["kernel"] + +seam = "objects/obj_audio/Create.gml" + +steps = [ + "Place obj_audio on an instance layer in your first room. It is persistent, so one placement covers the game.", + "Play sounds through `cue_sfx(snd)` / `cue_music(snd)` rather than `with (obj_audio)`. They work from any object, do nothing in a room with no controller, and record `global.last_cue` / `global.cue_count` — which is the only thing a headless gametest can assert on, since a run with no audio device reports every sound as not playing.", + "Add a track: `gmx prefab add track_and_street_racer/sounds/OGG_Racing_Track1`, then set `music` in the generated tuning to its namespaced name and call music_play(::track_and_street_racer::OGG_Racing_Track1).", + "Set the track's sound.toml to `compression = \"streamed\"` — a multi-minute track uncompressed sits in RAM in full. Leave short effects uncompressed so they fire with no decode latency.", +] + +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +[roles] +obj_audio = "persistent audio owner — holds the looping track across room changes and exposes music_play/sfx_play" + +[tuning] +music_gain = 0.4 +sfx_gain = 1.0 diff --git a/prefabs/music_and_sfx/project.toml b/prefabs/music_and_sfx/project.toml new file mode 100644 index 00000000..b3570309 --- /dev/null +++ b/prefabs/music_and_sfx/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "music-and-sfx" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/music_and_sfx/rooms/rm_demo/layers/Background.toml b/prefabs/music_and_sfx/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/music_and_sfx/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/music_and_sfx/rooms/rm_demo/layers/Instances.toml b/prefabs/music_and_sfx/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..dc22c4ff --- /dev/null +++ b/prefabs/music_and_sfx/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_audio" +object = "obj_audio" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/music_and_sfx/rooms/rm_demo/room.toml b/prefabs/music_and_sfx/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/music_and_sfx/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/music_and_sfx/scripts/scr_audio_cues/scr_audio_cues.gml b/prefabs/music_and_sfx/scripts/scr_audio_cues/scr_audio_cues.gml new file mode 100644 index 00000000..71070a4e --- /dev/null +++ b/prefabs/music_and_sfx/scripts/scr_audio_cues/scr_audio_cues.gml @@ -0,0 +1,38 @@ +/// Firing a sound from anywhere, and being able to test that you did. +/// +/// The controller holds the gains and the music handle, so playing a sound +/// means reaching through it. These wrap that reach: callable from any object +/// without naming `obj_audio`, and safe in a room that has not got one yet — +/// a missing controller is silence, not a crashed frame. + +/// @function cue_sfx(snd) +/// @description Fire a one-shot. +function cue_sfx(snd) { + // Recorded before playing, because the sound itself is unobservable in a + // headless run: with no audio device `audio_play_sound` returns -1 and + // `audio_is_playing` is never true, so a test can only check that the + // right cue was *asked for*. That is what these globals are. + if (!variable_global_exists("cue_count")) global.cue_count = 0; + global.last_cue = snd; + global.cue_count += 1; + + if (!instance_exists(obj_audio)) return; + with (obj_audio) sfx(snd); +} + +/// @function cue_music(snd) +/// @description Start the looping track, replacing whatever is playing. +function cue_music(snd) { + global.music_wanted = snd; + if (!instance_exists(obj_audio)) return; + with (obj_audio) music_play(snd); +} + +/// @function cue_music_stop(fade_ms) +/// @description Fade the track out — before a win sting, so it plays over +/// silence rather than over the loop. +function cue_music_stop(fade_ms = 400) { + global.music_wanted = noone; + if (!instance_exists(obj_audio)) return; + with (obj_audio) music_stop(fade_ms); +} diff --git a/prefabs/music_and_sfx/thumbnail.png b/prefabs/music_and_sfx/thumbnail.png new file mode 100644 index 00000000..d908dc92 Binary files /dev/null and b/prefabs/music_and_sfx/thumbnail.png differ diff --git a/prefabs/online_leaderboard/CLAUDE.md b/prefabs/online_leaderboard/CLAUDE.md deleted file mode 100644 index de818dd2..00000000 --- a/prefabs/online_leaderboard/CLAUDE.md +++ /dev/null @@ -1,878 +0,0 @@ -# CLAUDE.md - -> **Generated with gmx version 0.3.27.** If you're running a newer -> `gmx` (check `gmx --version`), ask the user whether to update this file by -> running `gmx refresh`. `gmx refresh` regenerates everything **above** the -> `` marker in the "Notes" section near the end of -> this file — everything from that marker to the end of the file is your -> project's own and is never touched. Put per-project notes there, not in a -> separate file. - -This is a GameMaker project in TOML form. `gmx` is the CLI; Source -of truth is the `*.toml` and `*.gml` files in this tree — never -write `.yy` or `.yyp`. - -## gmx commands - -The most useful subset, with the flags you'll reach for first: - -``` -gmx build produce .wad -gmx build --no-cache bypass the incremental _BLD cache -gmx run build + launch the host runner. Quiet by - default: prints a one-line verdict, not the - runner's ~20KB boot log. -v for the full log - (a failed run shows the log tail regardless). -gmx run --for 8 watch mode: boot, scan for fatal - errors, exit non-zero on crash or - zero after 8s clean run. Use this - for any non-interactive workflow — - the runner's modal error dialog - would otherwise hang the session. -gmx run --headless run the native runner windowless (renders - offscreen, background-only process). Pair - with --for/--timeout for CI/agents — no - on-screen window to absorb stray clicks/keys. -gmx run --screenshot s.png boot headless, grab one PNG at frame 2, exit — - the quickest "see the result". Deterministic - (same pixels every run); no window, no daemon. -gmx run --screenshot s.png --at-frame 2,60,120 - capture several frames (→ s-002.png, s-060.png, - s-120.png) to watch a transition settle. Bump - the frame past a splash/fade. Reaching a game - *state* (menus, input) needs gametest instead. -gmx run --wasm run the gx.games (wasm) build locally in a - browser instead of the host runner; add - --headless --timeout 10 for CI/agents -gmx gametest run --game - deterministic headless test: bake inline-GML - chunks, build, run, assert; exits 0 iff passed. - For CI/agents — see "Gametests" below -gmx gametest bake [-o ] compile a logical gametest to baked JSON only -gmx validate schema + load + compile checks -gmx validate --no-compile loader-only validation (faster) -gmx rename rename a resource and propagate -gmx rename --dry-run -gmx import ingest a GameMaker IDE project -gmx new scaffold a resource (object, room, - sprite, sound, script, font, shader, - tileset) with the correct #:schema path; - shader scaffolds a GLSL ES 3.00 - pass-through. run - from the project dir or pass - --project-dir . sprite/sound take - --source , tileset --source . - set common fields at scaffold time instead - of hand-editing afterward: object --parent - --persistent --solid; room - --persistent; sound --audio-group - --volume ; font --mode ttf --source - (bundles source.ttf so the font - validates clean immediately — default mode - is "system", no file needed) -gmx new instance append a placed instance to the room's - instances layer (--x/--y, --layer); - the unique id is generated for you -``` - -Reference for GML built-ins **and** the gmx-authored toolchain -pages (events, variables, layers, scripts, prefab-spec, -vk_constants, getting-started, common-gml-patterns): - -``` -gmx docs lookup single-page reference -gmx docs search fuzzy search names + summaries -gmx docs list --category gmx all first-party gmx pages -gmx docs list --kind recipe end-to-end recipes for common patterns -gmx docs list --kind function GML reference, by kind -gmx docs categories every category in the index -``` - -`vk_left`, `vk_space`, etc. resolve to the `vk_constants` page -via keyword fallback. `gmx docs lookup` works on a fresh binary -with no setup — the full manual is embedded. - -**Recipes are the canonical "how do I build X" implementations.** -Before generating GML or scaffolding objects from scratch for a -common feature (title screen, main menu, pause menu, audio, room -transitions, state-machine flow, custom cursor, …), check -`gmx docs list --kind recipe` first. When a recipe matches what -you're about to build, follow it verbatim — recipes encode -hard-won wisdom (compression modes, persistence patterns, input -event choices, frame-rate independence) that's easy to get wrong -from first principles. `gmx docs lookup anatomy-of-a-complete-game` -sketches how the recipes compose into a complete game. - -## Gametests (automated testing) - -A **gametest** (distinct from the docs "recipes" above — those are -how-to guides; a gametest is an executable test) is a JSON file that runs -the game **deterministically and headlessly** for a fixed number of frames, -plays scripted input, runs inline GML at chosen frames, and asserts on game -state — emitting a pass/fail `results.json` and a `0`/non-zero exit code. -Use it to verify behaviour without a human at the keyboard (CI, agent -loops, regression checks). - -``` -gmx gametest run mytest.gametest.json --game . # bake + build + run + assert -``` - -Author a logical gametest (inline GML in `chunks`, a frame `timeline` of -`inputs`/`code`/`assert`/`screenshot`), then `gmx gametest run` bakes it, -builds the WAD, runs the runner headless, and prints `results.json`. Full -schema, field rules, and determinism notes: `gmx docs lookup gametest`. - -## Prefabs (reusable assets from a catalog) - -A **catalog** is a git repo whose `prefabs//` directories each hold a -prefab (sprites, objects, rooms, scripts, …), discovered via git tags -(`@`). Commands default to the `gh:opera-emoller/prefabs` -catalog; override any command with `--catalog gh:/` (the only -catalog form — always a GitHub repo). - -``` -gmx prefab list every prefab in the catalog -gmx prefab list --tag --category filter (flags combine) -gmx prefab search free-text over resources - (name, kind, tags, description) -gmx prefab search --kind sprite restrict to one resource kind -gmx prefab show manifest + resource list -gmx prefab tags every tag in use, with counts -gmx prefab versions published versions, newest first -gmx prefab add --into add a prefab or one resource -gmx prefab sync --into re-resolve linked prefabs' caches -``` - -**A ref is either a whole prefab or a single resource inside one:** - -``` - a whole prefab (e.g. iconic-animals) -// one resource (e.g. iconic-animals/sprites/spr_cat) -gh://… full form, names the catalog explicitly -…@ pin a version (omit for the latest) -``` - -- `gmx prefab add --into .` writes `prefabs//link.toml` - (a pinned source); an implicit `sync` resolves it into a gitignored - `.cache/`. Add `--vendor` to materialise the full resolved tree in place - instead of linking. -- `gmx prefab add // --into .` vendors just that one - resource into `prefabs///` and writes a minimal - `prefabs//prefab.toml` so the resource is **namespaced** (that - manifest is what makes `::::` resolve — see below). - Add `--centre-origin` to set `origin = "centre"` on the vendored sprite(s) - (gameplay usually wants this; catalog sprites often ship `top_left`). - -**Worked example — grab a background + a player sprite and wire them up:** - -``` -gmx prefab add space-rocks-hd/sprites/spr_sky_nebula1 --into . -gmx prefab add iconic-animals/sprites/spr_cartoon_cat_ginger --into . --centre-origin -gmx new room rm_main --background "::space_rocks_hd::spr_sky_nebula1" -gmx new object obj_player --sprite "::iconic_animals::spr_cartoon_cat_ginger" -gmx new instance rm_main obj_player --x 960 --y 540 -# then author objects/obj_player/Step.gml for arrow-key movement -gmx validate . && gmx run . --screenshot boot.png -``` - -(Catalogs change — run `gmx prefab list` / `search` to see what yours actually -has, and `gmx prefab show ` to list a prefab's resource names. `add` -prints the exact `::ns::resource` ref to paste into TOML/GML.) - -**Discovering ready-made behaviour.** Some prefabs are effectively -libraries — drop-in GML for common effects (screen shake, flashes, room -transitions). Prefer them over hand-rolling. There are no guaranteed prefab -names, so **discover what the catalog actually has** with `gmx prefab list` -and `gmx prefab search `, then `gmx prefab show ` for its -resources and API before writing your own. - -**Prefab sprites usually need scale (and sometimes rotation) -adjustments per use case.** Catalog assets are original artwork — -they have no canonical "in-game size" or "facing direction": - -- **Scale.** Catalog sprites often ship at the original artwork - resolution (commonly 256–1024 px on a side), which is rarely the - size you want at gameplay scale. Multiply at draw time with - `image_xscale` / `image_yscale`. The swap-resilient idiom is to - derive the scale from the sprite's actual width so swapping - assets keeps the same on-screen size: - `image_xscale = 48 / sprite_get_width(spr_X);` -- **Rotation.** There's no library-wide convention for which way a - sprite "faces" — top-down characters / vehicles / projectiles - in different packs may point up, right, or whatever the original - artist chose. View the first frame after import and set - `image_angle` in `Create.gml` if it doesn't already match your - gameplay axis. -- **Origin.** Most gameplay code assumes a centred origin so - `draw_self()` pivots around the centre and collisions feel right. - Some packs ship sprites with `origin = "centre"` already set in - `sprite.toml`; others default to `top_left`. Check the imported - `sprite.toml`, or just pass `--centre-origin` to `gmx prefab add` to set - it on vendored sprites at add time. - -**Making changes to a linked prefab (patching)** - -A linked prefab keeps its pinned `source` in `prefabs//link.toml` and -resolves the full tree into a gitignored `prefabs//.cache/`. Never edit -`.cache/` (it's regenerated on every sync) — instead **overlay** from the -prefab folder: a file you place under `prefabs//` at a resolved file's -relative path wins over the source. - -- **Patch a file:** copy it out of `.cache/` to the matching path in - `prefabs//` and edit the copy in full — the copy replaces the - source file wholesale, `*.toml` included, so it must carry every field you - want kept, not just the ones you're changing. -- **Remove/reset:** add the resource's path to `unset` in `link.toml`: - - ```toml - source = "gh:opera-emoller/prefabs/cam-shake@1.2.0" - unset = ["sprites/spr_legacy"] - ``` - -Sync runs automatically before validate/build/run, or re-resolve manually with -`gmx prefab sync --into .`. Track the overlay files and `link.toml` in git -(`.cache/` is gitignored); or re-add with `--vendor` to take full local -ownership (no `link.toml`). - -## Referencing prefab resources: `::prefab::resource` - -A prefab added under `prefabs//` is namespaced: refer to any -resource inside it with the `::::` notation. The -leading `::`, the prefab folder name, `::`, then the resource name -(the prefab folder's `-`/`.` are normalised to `_`, so -`prefabs/camera-shake/` is the namespace `camera_shake`). - -**The same spelling works in both TOML and GML** — one rule: - -```toml -# objects/obj_x/object.toml — a prefab sprite as a resource ref -sprite = "::camera_shake::spr_asteroid" -``` - -`gmx new object --sprite "::camera_shake::spr_asteroid"` writes that -`sprite =` field for you at scaffold time, so you don't hand-edit the TOML. - -```gml -// Create.gml / Draw.gml — same reference in code -draw_sprite(::camera_shake::spr_asteroid, 0, x, y); -show_debug_message(sprite_get_name(::camera_shake::spr_asteroid)); -::camera_shake::camera_shake(0.5, 0.1, 0.1, 0.1); // call a prefab function -``` - -It lowers to the internal mangled name (`camera_shake__spr_asteroid`); -you always write the `::prefab::resource` surface form. Full rules: -`gmx docs lookup prefab-spec --category gmx`. - -### A prefab sprite as a room background - -The same `::prefab::resource` ref goes in a background layer's `[background]` -table, and `gmx new room --background` accepts it directly (auto-sizing the -room to the sprite — so pick a sprite whose aspect ratio matches the game you -want; a tall/portrait backdrop yields a tall/portrait room): - -``` -gmx new room rm_main --background "::scifi_space_shooter::sprBackground5" -``` - -```toml -# rooms/rm_main/layers/Background.toml -#:schema ../../../.gm-schema/v1.json#layer -kind = "background" -depth = 100 - -[background] -sprite = "::scifi_space_shooter::sprBackground5" -``` - -**Background layers don't auto-scale the sprite** — it's drawn at native size -from the top-left. Either size the room to the sprite (what `--background` -does, using `gmx sprite info ` for the dimensions), or set -`tiled_x`/`tiled_y` on the `[background]` table to repeat it across a larger -room. A background sprite with alpha also needs an opaque fill behind it (see -"Common runtime traps"). - -## Project layout - -``` -.gm-schema/v1.json JSON schema for every TOML file -project.toml schema_version, name, [room_order] -sprites// - sprite.toml optional — #:schema …#sprite; omit for an all-default sprite - frame_000.png first frame (PNG or JPEG: .png/.jpg/.jpeg), - or a single vector frame_000.svg - frame_001.png (more frames as the sprite needs) -objects// - object.toml #:schema ../../.gm-schema/v1.json#object - Create.gml event scripts (one per event) - Step.gml - Draw.gml -rooms// - room.toml #:schema ../../.gm-schema/v1.json#room - layers/ - Instances.toml #:schema ../../../.gm-schema/v1.json#layer - Background.toml - Tiles.toml - Assets.toml -sounds// - sound.toml optional — #:schema …#sound; omit for an all-default sound - source. -scripts// - .gml the whole asset — there is no script.toml -shaders// - vertex.vsh vertex source (required) - fragment.fsh fragment source (required) - shader.toml optional — no configuration; just a schema anchor -fonts// - font.toml #:schema ../../.gm-schema/v1.json#font - # mode = "frozen": needs sibling - # atlas.png + glyphs.toml. - # mode = "ttf": needs sibling - # source.ttf (literal name); - # rasterised at compile time. - # Required: size. Optional: - # bold, italic, anti_alias (bool), - # texture_group, charset_ranges, sdf. -``` - -## Editing rules - -- Every TOML's first line is - `#:schema /.gm-schema/v1.json#kind`. Preserve it — - that's how editors and `gmx validate` know the schema. -- `frame_NNN.png` (or `frame_NNN.jpg` / `frame_NNN.jpeg`) filenames - are positional (zero-padded). Don't rename them. To reorder frames, - change which file holds which bytes. A sprite may instead be a single - vector `frame_000.svg` (tessellated at build). JPEG frames decode to opaque - RGBA (handy for backgrounds) and may be mixed with PNG frames. -- For room layers: the `depth = N` field decides rendering - z-order at runtime AND the on-disk order — layers are sorted - by depth-ascending at load. The layer's name in - `instance_create_layer(...)` etc. is the file stem - (`Instances.toml` → `"Instances"`). Background layers stash - their body under a `[background]` table inside a - `kind = "background"` layer file. Full reference: - `gmx docs lookup layers --category gmx`. -- Room order: `[room_order].order` in `project.toml` lists the - rooms in play order — the first entry is the room the game - opens with. It's optional: with a single room the order is - implicit, so omit `[room_order]` entirely. The **first room's - `size`** (`room.toml`'s `size = { width, height }`) sets the - game's display/window dimensions — make the starting room the - size you want the window to be. `window_set_fullscreen()` / - `window_set_size()` calls have **no reliable effect under - `gmx run`**'s dev runner — don't chase workarounds for this; - see `gmx docs lookup window-fullscreen --category gmx`. -- Object event filenames are a closed grammar. - `gmx init` ships the **native** form (`Create.gml`), - `gmx import` the **imported** form (`Create_0.gml`). Match - whichever the project already uses; full grammar at - `gmx docs lookup events --category gmx`. -- Shaders are GLSL ES. They default to **GLSL ES 1.00** - (`attribute`/`varying`/`gl_FragColor`); to write a **GLSL ES - 3.00** shader make `#version 300 es` the first line of the - source (`in`/`out` + your own fragment output). `shader.toml` - carries no configuration and may be omitted. **Match the - project's existing shaders** — if they're ES 3.00, author new - ones ES3 too; don't mix an ES1 shader into an ES3 project - (it forfeits ES3 features and is inconsistent). Prefer ES 3.00 - when the project/runner supports it. Full reference: - `gmx docs lookup shaders --category gmx`. -- **Never draw text with the default font.** The built-in font - (`draw_set_font(-1)`) is a tiny bitmap; scaling it up for - headings/labels is blurry and ugly. Create a sized font asset - (`gmx new font `, `fonts//font.toml` at the intended - size), `draw_set_font()`, and draw at scale 1. -- Object-scope `[[variables]]` and the always-string - room-instance override gotcha: - `gmx docs lookup variables --category gmx`. -- An invisible "controller" object (game manager, spawner) can - have an `object.toml` with only the `#:schema` line — no - `sprite =` field needed. -- `gmx docs lookup ` covers GML built-ins **and** the - first-party gmx pages above. `vk_left`, `vk_space`, … resolve - to `vk_constants` via keyword fallback. `gmx docs list ---category gmx` enumerates the toolchain set. - -## Physics shapes (declarative) - -`[physics.body.shape]` in `object.toml` handles every Box2D fixture -case — no `bind_*_fixture` / `physics_fixture_set_*` calls needed in -`Create.gml`. Variants: - -- `circle`, `aabb`, `polygon`, `edge`, `wire` (passthrough escape - hatch). -- `polygon` accepts **any simple polygon** — convex or concave. The - compiler decomposes inputs to fit the 32-vertex convex Box2D limit. -- `edge` defines a single line segment via `from = [x, y]` and - `to = [x, y]`. Used for invisible barriers (one-way gates). -- Every variant supports `scale = "instance"` — when set, the runner - multiplies vertices (and circle radius) by `image_xscale` / - `image_yscale` at fixture-create time and reverses polygon winding - for negative-on-one-axis scale (mirroring). - -Points are in sprite-top-left coordinates; the runner subtracts the -sprite's origin (from `sprite.toml`'s `origin` / `custom_origin`) -before handing vertices to Box2D, so polygon authoring matches what a -tracing tool sees on the sprite image. Spriteless objects (e.g. -`edge` barriers) use body-local coords directly. - -`gmx docs lookup physics-shapes --category gmx` has the full reference -with worked examples. - -## Build-time TOML references in GML (`::path`) - -GML can reference TOML values as compile-time literals via the `::` -sigil. The build resolves these to literal expressions before the GML -compiler sees the source. - -```gml -// In obj_foo/Create.gml — resolves against the current object's TOML: -drag_hull = ::physics.body.shape.points; -my_sprite = ::sprite; - -// Cross-resource form names another resource: -ball_origin = spr_ball::custom_origin; -game_name = project::name; -``` - -Project-wide constants live in a `[constants]` table in `project.toml` and -are read with `project::constants.` — the idiomatic way to avoid -duplicating a value (server URL, tuning number, flag) across objects: - -```toml -# project.toml -[constants] -server_url = "https://api.example.com" -``` - -```gml -url = project::constants.server_url; -``` - -Constants resolve to literals at build time, so they're baked into the -compiled game and are **not** hot-reloadable: editing `[constants]` and -hot-reloading through the debugger won't change a running game (rebuild to -pick it up). Use `[constants]` for build-fixed values. For something you -want to tweak live while the game runs, put it on `global.` or a game -object's instance variable instead — those are runtime state hot reload can -change. - -`gmx validate` flags broken references with a span pointing at the -failing token. `gmx-project-lsp` surfaces the same as a red squiggle. -`gmx rename ` rewrites the resource prefix in -`::path` references for free (the lexer treats the prefix as -a plain identifier). - -`gmx docs lookup tomlref --category gmx` for the syntax + scoping + -error cases. - -## Runner version pin - -When a project depends on runner features that are newer than vanilla -GameMaker (declarative concave polygons, edge shapes, `scale = -"instance"`), declare it in `project.toml`: - -```toml -[runner] -min_version = "0.1.0.24" -``` - -The value is the four-part gmx runner version (e.g. `0.1.0.24`). -`gmx run` enforces it: if your installed runner is older it downloads a -new-enough one, and it errors if no available runner satisfies the -requirement. Omit `[runner]` for projects that run on any runner build. - -## Publishing a prefab - -A catalog is just a git repository, so publishing is just: - -1. Place your prefab's source tree at `prefabs//` in a catalog repo you - control (it must carry a `prefab.toml` at its root). -2. Run `gmx prefab reindex` at the repo root to (re)generate the committed - `prefab-index.json`. Consumers fetch that one file instead of walking the - whole tree, so regenerate it whenever prefabs change — before you tag. -3. Commit both (`prefabs//` and `prefab-index.json`) and push. -4. Tag the release following the `@` convention (e.g. - `git tag iconic-animals@1.0.0 && git push --tags`). `gmx prefab versions` - and `add …@` discover versions from these tags; an untagged - catalog resolves to its newest tag overall. The index is fetched _at_ the - tag, so tag only after committing a fresh `prefab-index.json`. - -Consumers then add it with `gmx prefab add gh://` -(or `` with `--catalog gh:/`). - -## Verify before claiming done - -``` -gmx validate # schema + loader + GML compile pass -gmx run # launches the host runner interactively -gmx run --for 8 # watch mode, exits cleanly after 8s -``` - -A clean `gmx validate` is the strongest signal that an edit is -correct — it runs the full compile pipeline and surfaces any GML -lex/parse/codegen errors. It also emits GML lint **warnings** -(e.g. `draw-event-mutation`); when a flagged pattern is genuinely -intentional — like building a surface in a Draw event — silence -that one line with a trailing `//gmx-lint-ignore ` (or bare -`//gmx-lint-ignore` for all codes on the line / the next line). -For any non-interactive runtime check -(CI, agent, smoke test) use `gmx run --for ` — it spawns -the runner with debug-log capture, scans for fatal patterns -("ERROR in action", "Unable to find function", "Failed to load", -premature exit), force-quits on detection, and exits non-zero -with the captured log. On a clean run it boots, lets the game -tick for ``, then quits zero. Without `--for`, a runtime -crash pops a modal dialog that will hang any non-interactive -workflow. - -The Mac runner prints unconditional `NSLog` lines before any -game code runs (`Nil context detected`, `!!!!######## rendersize=…`). -They're cosmetic, not warnings — ignore them when scanning for -real `Warning!` / `ERROR` lines. - -## Debugging and driving a running game - -`gmx debug` runs a live runner and lets you inspect and _modify_ a -running game from the CLI — use it to verify behaviour, not just -`gmx run`. Verbs target the current directory's project by default; pass -`--project ` to target another. - -**Prefer headless verification.** Default to a windowless run unless you -expect the _user_ to drive the window: `gmx debug start --headless`, -`gmx run --headless`, `gmx gametest` (always headless), or -`gmx debug start --wasm --headless`. A windowed runner left on screen -absorbs the user's clicks/keys into the debug-input channel -(`keyboard_lastkey`, mouse state) and corrupts your readings — you will -chase phantom bugs that came from the user's stray input, not the game. -`--headless` renders offscreen as a background-only process, so -`screenshot`/`capture` and every other verb still work exactly the same. -Only run windowed when you specifically need the user to play the game -and report what they see. - -There are **two ways to drive it** — pick by how you need to react: - -**A. Discrete steps (most tasks): per-verb `--json`.** Each verb is one -command that returns one JSON envelope — issue it, read the result, -decide the next. Simplest and most common: - -``` -gmx debug start --headless # build --debug, launch windowless, attach - # (preferred default — see "Prefer - # headless verification" above) -gmx debug start # same, but windowed (only when the user - # drives the game) -gmx debug bp set objects/obj_player/Step.gml:14 -gmx debug continue -gmx debug wait --timeout 5 --json # block until the next stop -gmx debug describe --json # where am I: state, location, stacks, globals, log tail -gmx debug eval 'global.score' --json -gmx debug stop -``` - -**B. Event-driven (react to stops without polling): a backgrounded -session.** Run `gmx debug session --json` as a _background_ process and -write `ClientRequest` lines to its stdin. It streams newline-JSON events -on stdout — `stopped` (with `file`/`line`/`reason`), `log`, `running`, -`exited` — so you get woken when a breakpoint hits instead of burning a -turn on `wait`. Use this when you set a breakpoint, `continue`, and want -to be told when it fires. The session owns the runner and reaps it on -exit, so you don't leak a hung session. - -``` -# (run in background) gmx debug session --json -# stdin: {"verb":"bp_set","location":"objects/obj_player/Step.gml:14"} -# stdin: {"verb":"continue"} -# stdout: {"schema":"gmx-debug/1","type":"event","event":"stopped","reason":"breakpoint","file":"...","line":14} -``` - -**Read values — use the `json` field.** `eval ''` evaluates one GML -expression. Responses carry a `value` display string AND a structured -**`json`** field: scalars as native JSON, and arrays followed + recursed, -so `eval 'global.angles' --json` gives `[0,90,180]` instead of -`array @0x…`. Prefer `json`. (Expansion needs the value to persist — -introspect stored state, not a throwaway literal, which may be GC'd.) - -**Dump instances.** `gmx debug instances --json` returns every live -instance of an object with its built-in (x, y, image_angle, …) and user -variables as JSON — one call instead of N per-instance probes: - -``` -gmx debug instances obj_enemy --json -``` - -**Modify live state.** `runscript` runs full GML _statements_ for their -side effects — assignment, `var`, `with`, loops, new globals — and an -optional trailing `return ;` reads a value back: - -``` -gmx debug runscript 'global.score = 0;' -gmx debug runscript 'with (obj_enemy) hp = 50;' -gmx debug runscript 'repeat (5) instance_create_depth(64, 64, 0, obj_coin);' -gmx debug runscript 'global.n = instance_number(obj_coin); return global.n;' -``` - -A failed `eval`/`runscript` returns `kind:"error"` and a non-zero exit — -trust the exit code; a silent no-op won't read as success. - -**Hot-reload code without a rebuild.** Edit an object event's -`objects//.gml`, then push it into the _already-running_ game: - -``` -gmx debug reload obj_player Step # the edited Step runs next frame -gmx debug reload obj_player Step --revert # restore the original -``` - -No `gmx build`, no relaunch — iterate on behaviour in seconds against the -live world (the enemies are still on screen, the score is still 9000). -`runscript` mutates _state_; `reload` swaps _code_. Together they let you -test a fix without losing the game state that triggered the bug. - -**Hot-reload art without a rebuild.** `reload-sprite` swaps a sprite's -pixels live — every instance and asset-layer entry drawing it updates next -frame: - -``` -gmx debug reload-sprite spr_player # re-read sprites/spr_player/ and push it -gmx debug reload-sprite spr_player --from new.png # swap in an arbitrary image -``` - -Tweak a PNG in `sprites//` and see it in the running game without a -build. (Prefab-provided sprites aren't in the project `sprites/` tree — -use `--from` for those.) - -**See what's on screen.** `gmx debug screenshot -o shot.png` captures the -live frame — read the PNG back to _verify your hot-reload actually looks -right_, not just that the call succeeded. - -A typical live-debug loop: `bp set` → `continue` → inspect with -`describe`/`instances`/`eval` → fix with `runscript` (state) or `reload` -(code) → `screenshot` to confirm → `continue`. All without rebuilding. - -**Debug the gx.games (wasm) build in a browser.** `gmx debug start --wasm` -runs the game in a Chromium-family browser (the actual gx.games target) and -attaches the debugger over a WebSocket — **every verb above works exactly the -same** (`eval`, `runscript`, `instances`, breakpoints, …). Add `--headless` -for CI/agents. Use this to reproduce browser-only behaviour; the native -`gmx debug start --headless` stays the default for everyday work. - -**One-shot verbs** (also usable standalone against a `gmx debug start` -daemon): - -``` -gmx debug start --headless build --debug, launch windowless, attach (preferred default) -gmx debug start same, but windowed (only when the user drives the game) -gmx debug start --wasm [--headless] debug the gx.games (wasm) build in a browser; all verbs work the same -gmx debug session [--json] owned, event-streaming session (preferred) -gmx debug describe --json one-shot orientation snapshot (paused state, location, stacks, globals, watches, log tail) -gmx debug bp set [--cond ] set a (conditional) breakpoint -gmx debug continue / pause / step [in|over|out] -gmx debug wait [--timeout secs] block until the next stop event -gmx debug eval '' evaluate a GML expression; response has a structured `json` field -gmx debug runscript '' run GML statements (assignment, with, loops); trailing `return ;` reads a value back -gmx debug instances --json dump every live instance of an object with its vars as JSON -gmx debug reload [--revert] hot-reload an event's GML into the running game -gmx debug reload-sprite [--from x.png] live-swap a sprite's pixels (no rebuild) -gmx debug screenshot -o shot.png grab the whole game screen to a PNG -gmx debug stack / locals / selfvars / globals inspect the paused frame -gmx debug watch add '' persistent watch, re-evaluated on every stop -gmx debug input key synthesise a keypress -gmx debug input mouse --at X Y synthesise a click -gmx debug stop tear down daemon + runner -``` - -Drop into an unknown session with `gmx debug describe --json` for a -single-shot snapshot before issuing more verbs. - -## Common runtime traps - -These produce a successfully-validated, successfully-built game -that renders empty / wrong on launch. `gmx validate` won't catch -them — they're runtime semantics, not schema or compile errors. - -- **`enable_views = true` in `room.toml`** turns on GameMaker's - view-camera system. Without per-view fields configured - (`camera_*`, `viewport_*`) the runtime renders nothing visible. - Leave `enable_views` unset (= default `false`) unless you actually - need cameras — most small games don't, and the room's `view` - size already controls the rendered area. -- **Transparent sprite on a Background layer** lets whatever's in - the framebuffer show through the alpha pixels — usually garbage. - If your background sprite has an alpha channel (parallax / - overlay sprites — stars, fog, light shafts, weather — are - commonly authored to layer _on top of_ something else), add a - second `Background.toml` at a HIGHER `depth` (drawn first / - further back) with `[background] colour = "#0a0014"` (or any - opaque colour) to fill behind it. The transparent sprite then - composites correctly over the solid colour. - -## If stuck - -- Read sibling files of the same kind to see the expected shape. -- `gmx schema show ` prints the JSON-schema definition - for any TOML kind (`object`, `sprite`, `room`, `layer`, - `sound`, `script`, `font`, `tileset`, `sequence`, - `particle_system`, …). `gmx schema show` (no argument) lists - every kind. -- `gmx docs lookup ` covers GML built-ins **and** the - first-party gmx pages. Try `gmx docs list --category gmx` for - the toolchain set; `gmx docs lookup getting-started --category -gmx` for a first-five-minutes walkthrough. -- `gmx prefab show ` lists a catalog prefab's resources; - `gmx prefab search ` finds assets to reuse. - -## Prefab authoring (this is a prefab project) - -This project is a **prefab** — authored to be published to a gmx prefab -catalog and reused in other projects, not just run as a standalone game. - -**What makes it a prefab** is the `prefab.toml` at the project root. Its -presence is what namespaces the prefab's resources when a consumer adds it — -there is no export list and no public/private tier. Namespacing is uniform, so -every resource is referable as `::::` once added. - -**`prefab.toml` (discovery metadata only; all fields optional):** - -```toml -name = "Camera Shake" # falls back to the folder name -description = "Smooth camera trauma and shake." # one-line, for list/search -category = "effects" # optional single grouping -tags = ["camera:shake", "style:juice"] # discovery tags, `facet:value` -asset_bundle = false # true = individual assets can be imported independently; - # false = assets have dependencies and must be imported as a whole - -# Demo-only resources: present so `gmx run` works here, but kept out of the -# published index and out of what consumers pull. `/` keys. -exclude = ["objects/obj_demo", "rooms/rm_demo"] - -# Optional per-asset metadata (surfaced in search), keyed by "/": -[assets."scripts/scr_cam_shake"] -description = "camera_shake(time, mag) — trauma-based shake." -tags = ["role:api"] -``` - -Curate `description` / `category` / `tags` by hand — that's your job as the -agent; gmx doesn't generate them. - -**The demo is demo-only.** `rm_demo` and its driver object let you `gmx run` / -`gmx validate` the prefab standalone while authoring. The scaffold lists them -in `exclude` (above), so they stay in your tree but never reach the published -index or a consumer. Add any other author/test-only resources to `exclude` -too; drop the list once a resource becomes part of the real surface. - -## Deriving from a parent (`[parent-link]`) - -A variant prefab derives from a published (or sibling, still-unpublished) -base instead of duplicating it, by adding a `[parent-link]` table to its own -`prefab.toml`: - -```toml -name = "Camera Shake (Heavy)" -description = "A heavier variant of Camera Shake." - -[parent-link] -source = "cam-shake" # see the two forms below -unset = ["scripts/scr_cam_legacy"] # optional: drop inherited files/resources -``` - -At resolve time the parent's whole tree is fetched and your own files are -overlaid on top: any file you carry at a given path replaces the parent's -file there wholesale (`*.toml` included — there's no field-level merging, so -a file overriding an inherited one must carry every field it needs, not just -the ones that differ). `unset` works exactly like a consumer `link.toml`'s: -a path (`sprites/spr_legacy`) drops an inherited file/resource. - -**`source` has two forms:** - -```toml -source = "cam-shake" # implicit: same catalog as this prefab -source = "gh://cam-shake@1.0.0" # explicit: any catalog -``` - -The **implicit** form (no `gh:` prefix) is only legal inside `[parent-link]` -— it means "the parent lives in this same catalog", and is what makes -authoring and testing a derived prefab possible with **no catalog at all** -(see below). The **explicit** form names any catalog and always resolves over -the network; use it to derive from a prefab published somewhere else. - -### Testing a derivation — fully offline, no catalog required - -With the implicit form, `gmx run` / `gmx validate` / `gmx build` / -`gmx gametest` resolve `[parent-link]` **in place**, straight from your -working tree: they look for the parent as a **plain sibling directory** -(a folder of the same name next to your prefab dir, carrying its own -`prefab.toml`) and merge it in on disk — no `--catalog`, no `gh:` ref, no -network, no publishing. This is the easiest way to verify a derivation while -authoring, and it's what makes the implicit form worth reaching for even for -a prefab you never intend to publish standalone. - -```sh -# The base: a normal scaffolded prefab, so it runs standalone and has -# something to inherit from. -gmx init --prefab sandbox/cam-shake - -# The derived variant: just a prefab.toml, next to it. -mkdir sandbox/cam-shake-heavy -cat > sandbox/cam-shake-heavy/prefab.toml <<'EOF' -name = "Camera Shake (Heavy)" - -[parent-link] -source = "cam-shake" -EOF - -gmx validate sandbox/cam-shake-heavy # resolves cam-shake from the sibling dir, offline -``` - -`sandbox/cam-shake-heavy/.cache/` now holds the merged tree (gitignored, -regenerated on every run — never hand-edit it). This only stays offline while -every hop in the chain uses the implicit form and has a real sibling -directory; the moment a hop names an explicit `gh:...` source, resolution -falls through to the network for that hop (and every hop above it) — which -also means the same mechanism verifies a real multi-catalog chain once you -point `source` at a published parent. - -## Publishing - -A catalog is a git repo of `prefabs//` directories: - -1. Place this prefab's tree at `prefabs//` in your catalog repo. -2. Run `gmx prefab reindex` at the catalog root to regenerate - `prefab-index.json` — consumers fetch that one file instead of walking the - whole tree. -3. Commit both, then tag the release `@` (e.g. - `git tag cam-shake@1.0.0 && git push --tags`). The index is fetched *at* the - tag, so tag only after committing a fresh `prefab-index.json`. - -Consumers then add it from any project: - -```sh -gmx prefab add gh://cam-shake --into -``` - -This writes a pinned `prefabs/cam-shake/link.toml` and namespaces every -resource under the folder name — call it as `::cam_shake::` in TOML and -GML (folder `-`/`.` normalised to `_`). Add a single resource with -`gmx prefab add gh://cam-shake//`, or `--vendor` to -copy the resolved tree in instead of linking. A derived prefab's -`[parent-link]` chain resolves the same way for a consumer as it does for you -locally — the consumer never has to know the prefab is derived. - -Keep `README.md` and `LICENSE` present, and finalise them before publishing. - -## Version Control (Git) - -This project is structured for Git. To initialize your repository and track changes: - -```bash -git init -git add . -git commit -m "Initial commit from gmx scaffold" -``` - -Before committing large binary assets, ensure you configure a .gitignore file - -## Notes - - -Add project-specific notes below this line. `gmx refresh` regenerates everything above `` from the installed gmx version's template but never touches anything from that marker to the end of the file, so hand-written notes here are always safe. - diff --git a/prefabs/online_leaderboard/README.md b/prefabs/online_leaderboard/README.md index c0cf54a1..f3f2a659 100644 --- a/prefabs/online_leaderboard/README.md +++ b/prefabs/online_leaderboard/README.md @@ -129,7 +129,7 @@ enough for any URL-backed image. ## Local development vs. gx.games On gx.games, the API talks to the real backend over HTTP. Anywhere else -(the GameMaker/gmx editor, a desktop build, `gmx run`, a `gmx gametest`) it +(the GameMaker/gmx editor, a desktop build, `gmx run`, a `gmx test` run) it falls back to fake, hardcoded data - there's no real backend to talk to until the game is actually published and running embedded on gx.games. diff --git a/prefabs/parallax/LICENSE b/prefabs/parallax/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/parallax/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/parallax/README.md b/prefabs/parallax/README.md new file mode 100644 index 00000000..9da2267d --- /dev/null +++ b/prefabs/parallax/README.md @@ -0,0 +1,3 @@ +# parallax + +Layers that move at different speeds, and never run out. diff --git a/prefabs/parallax/objects/obj_demo/Create.gml b/prefabs/parallax/objects/obj_demo/Create.gml new file mode 100644 index 00000000..fb77b4b5 --- /dev/null +++ b/prefabs/parallax/objects/obj_demo/Create.gml @@ -0,0 +1,6 @@ +sky = parallax_make(); +// Back to front: the far band moves least, the near one most. +far = parallax_add(sky, spr_demo_far, 0.15, 200, 0.05); +near = parallax_add(sky, spr_demo_near, 0.45, 280, 0.1); +parallax_drift(sky, far, 6); + diff --git a/prefabs/parallax/objects/obj_demo/Draw.gml b/prefabs/parallax/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..68edec85 --- /dev/null +++ b/prefabs/parallax/objects/obj_demo/Draw.gml @@ -0,0 +1,2 @@ +var _cam = view_camera[0]; +parallax_draw(sky, camera_get_view_x(_cam), camera_get_view_y(_cam)); diff --git a/prefabs/parallax/objects/obj_demo/Draw_GUI.gml b/prefabs/parallax/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..45774096 --- /dev/null +++ b/prefabs/parallax/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,3 @@ +draw_text(40, 30, "parallax demo — arrow keys scroll, SPACE recentres"); +draw_text(40, 50, "camera at " + string(camera_get_view_x(view_camera[0])) + + " — the far band also drifts on its own"); diff --git a/prefabs/parallax/objects/obj_demo/Step.gml b/prefabs/parallax/objects/obj_demo/Step.gml new file mode 100644 index 00000000..b2e46d0d --- /dev/null +++ b/prefabs/parallax/objects/obj_demo/Step.gml @@ -0,0 +1,12 @@ +// A real camera, moved through the view rather than a variable of our own, +// so what the demo shows is what a game would get. +var _cam = view_camera[0]; +var _x = camera_get_view_x(_cam) + + (keyboard_check(vk_right) - keyboard_check(vk_left)) * 6; +camera_set_view_pos(_cam, clamp(_x, 0, room_width - camera_get_view_width(_cam)), 0); + +parallax_step(sky); +if (keyboard_check_pressed(vk_space)) { + camera_set_view_pos(_cam, 1200, 0); + parallax_reset(sky); +} diff --git a/prefabs/parallax/objects/obj_demo/object.toml b/prefabs/parallax/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/parallax/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/parallax/prefab.toml b/prefabs/parallax/prefab.toml new file mode 100644 index 00000000..e162a2df --- /dev/null +++ b/prefabs/parallax/prefab.toml @@ -0,0 +1,21 @@ +schema_version = 1 +name = "Parallax backgrounds" + +# A library: installed under `prefabs//` in the target project and +# called as `::parallax::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +steps = [ + "Reach for `parallax_layer(\"\", factor)` first. It offsets a *background layer* — art named in the room file, with `sprite` and `tiled_x = true` — so the editor, a room thumbnail and anyone reading the TOML all see the backdrop the game shows. A band drawn from a Draw event is invisible to every one of them, and the room ends up looking nothing like the game.", + "Seat each band with `parallax_layer(\"\", , parallax_seat_y(, ))` rather than working out `y` by hand. A background layer draws from its top-left with no origin, and catalog bands are padded, so subtracting `sprite_get_height` puts the art well above the ground; two bands seated by eye end up within a few pixels of each other and the nearer one is never seen.", + "Depth and factor have to agree: a band that moves less must draw further back. `parallax_order([\"Far\", \"Near\"])` sets the depths from the order you list them in, so the two cannot disagree. **Look at the art before deciding that order** — a name is not a depth. `spr_backtrees_1` is a mountain range and `spr_backtrees_2` a green treeline, both described in the catalog as distant tree bands, and ordering them by their suffix put the mountains in front and hid the treeline completely.", + "Two things a background layer *can* do that look like reasons to reach for the drawn stack, and are not. **It can drift on its own**: `[background] hspeed` scrolls the element in pixels per step, and it composes with `parallax_layer` — the layer moves against the camera while the element scrolls within it, so clouds get both. **It can sit in front of the action**: layer depth may be negative, and `parallax_layer` accepts `factor > 1`, so a foreground band is a background layer at depth -100. A band at any depth between two others is fine too; depths are arbitrary numbers.", + "`parallax_make` / `parallax_add` / `parallax_draw` are the genuine fallback, and it is narrower than it looks: a background layer cannot *scale* its sprite (`stretch` fills the room, which is a different thing), so it is for bands whose art you cannot resize to size — `parallax_fit` scales at draw time. Resize the asset with `gmx tools sprite-resize --fit-height` and the layer form covers it. Anything drawn this way is invisible to the editor, so reach for it last.", + "Catalog backdrops are drawn far larger than a view — `parallax_fit(stack, index, view_height)` sizes one to the camera, and the tiling follows the scale. A background layer scales with `stretch` instead, in the room file.", +] + +description = "A back-to-front stack of layers, each moving at a fraction of the camera, tiled to cover the view so no layer ever ends. Factors above 1 sit in front of the action for a foreground. Optional self-drift for clouds that move while the player stands still, with the tiling anchored to the sprite rather than to distance travelled, so a long level does not slowly desynchronise. Layers carry a scale, so art drawn at a different size than the view still fits and still tiles." +category = "library" +tags = ["capability:background", "capability:parallax", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo", "sprites/spr_demo_far", "sprites/spr_demo_near"] diff --git a/prefabs/parallax/project.toml b/prefabs/parallax/project.toml new file mode 100644 index 00000000..23007717 --- /dev/null +++ b/prefabs/parallax/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "parallax" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/parallax/rooms/rm_demo/layers/Background.toml b/prefabs/parallax/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/parallax/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/parallax/rooms/rm_demo/layers/Instances.toml b/prefabs/parallax/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/parallax/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/parallax/rooms/rm_demo/room.toml b/prefabs/parallax/rooms/rm_demo/room.toml new file mode 100644 index 00000000..9c65b718 --- /dev/null +++ b/prefabs/parallax/rooms/rm_demo/room.toml @@ -0,0 +1,16 @@ +#:schema ../../.gm-schema/v1.json#room + +# A real view that starts away from the origin, and a room wide enough to +# scroll in. The demo used a stand-in camera in a room with no view, which +# meant it drew correctly while `parallax_draw` was placing every layer at +# the room origin — the defect could not show up here. +size = { width = 3840, height = 540 } +enable_views = true + +[[views]] +visible = true +x_view = 1200 +w_view = 960 +h_view = 540 +w_port = 960 +h_port = 540 diff --git a/prefabs/parallax/scripts/scr_parallax/scr_parallax.gml b/prefabs/parallax/scripts/scr_parallax/scr_parallax.gml new file mode 100644 index 00000000..78437396 --- /dev/null +++ b/prefabs/parallax/scripts/scr_parallax/scr_parallax.gml @@ -0,0 +1,67 @@ +/// Depth from layers that move at different speeds. +/// +/// Two rules make it read as depth rather than as sliding wallpaper: +/// further layers move less, and every layer wraps rather than ending. A +/// layer that runs out is the one thing that destroys the effect outright. +/// +/// A band that can live in the room should: name it on a background layer and +/// move it with `parallax_layer`, so the room file says what the backdrop is. +/// The stack below draws in code and is the fallback for what cannot - a +/// foreground at `factor > 1`, self-drifting clouds, more bands than layers. + +/// @function parallax_make() +/// @description An empty stack of layers. Add to it back-to-front. +function parallax_make() { + return { layers: [] }; +} + +/// @function parallax_add(stack, sprite, factor, y, vertical_factor, scale) +/// @description Add a layer drawn at `y` that moves at `factor` of the +/// camera — 0 is painted on, 1 moves with the world. Anything above 1 sits +/// in front of the action, which is what a foreground layer wants. `scale` +/// sizes art that was not drawn for this view; the tiling follows it, so a +/// scaled layer still meets itself edge to edge. +/// +/// `vertical_factor` defaults to 0 — a band that does not move with the +/// camera's height, which is nearly every backdrop. Leaving it required cost +/// a caller a `FATAL ERROR: undefined value` raised inside `parallax_draw` +/// with no line number, for the omission of the argument they least needed. +function parallax_add(stack, sprite, factor, y, vertical_factor = 0, scale = 1) { + array_push(stack.layers, { + sprite: sprite, + factor: factor, + vfactor: vertical_factor, + y: y, + scale: scale, + drift: 0, + }); + return array_length(stack.layers) - 1; +} + +/// @function parallax_fit(stack, index, view_height) +/// @description Scale a layer to stand `view_height` tall. Catalog backdrops +/// are drawn far larger than a view, and a layer that overflows it is the +/// same mistake as one that runs out. +function parallax_fit(stack, index, view_height) { + if (index < 0 || index >= array_length(stack.layers)) return; + var _l = stack.layers[index]; + var _h = sprite_get_height(_l.sprite); + if (_h > 0) _l.scale = view_height / _h; +} + +/// @function parallax_drift(stack, index, pixels_per_second) +/// @description Make a layer scroll on its own as well — clouds that move +/// while the player stands still. Added to the camera-driven offset. +function parallax_drift(stack, index, pixels_per_second) { + if (index < 0 || index >= array_length(stack.layers)) return; + stack.layers[index].speed = pixels_per_second; +} + +/// @function parallax_reset(stack) +/// @description Clear accumulated drift. A new run should not start with +/// the clouds wherever the last one left them. +function parallax_reset(stack) { + for (var i = 0; i < array_length(stack.layers); i++) { + stack.layers[i].drift = 0; + } +} diff --git a/prefabs/parallax/scripts/scr_parallax_draw/scr_parallax_draw.gml b/prefabs/parallax/scripts/scr_parallax_draw/scr_parallax_draw.gml new file mode 100644 index 00000000..ac8b8939 --- /dev/null +++ b/prefabs/parallax/scripts/scr_parallax_draw/scr_parallax_draw.gml @@ -0,0 +1,118 @@ +/// @function parallax_step(stack) +/// @description Advance any self-drifting layers. Call once a frame. +function parallax_step(stack) { + var _dt = delta_time / 1000000; + for (var i = 0; i < array_length(stack.layers); i++) { + var _l = stack.layers[i]; + if (!variable_struct_exists(_l, "speed")) continue; + _l.drift += _l.speed * _dt; + } +} + +/// @function parallax_draw(stack, camera_x, camera_y) +/// @description Draw every layer, tiled to cover the view horizontally. +/// Each is repeated from one screen left to one screen right of the camera, +/// so a layer never runs out however far the camera travels. +/// +/// Call from a Draw event. The offsets below are worked out in screen space +/// and then placed back at `camera_x`/`camera_y`, because a Draw event draws +/// in *room* coordinates: without that last step every layer lands near the +/// room origin and a game whose camera has travelled sees nothing at all. +function parallax_draw(stack, camera_x, camera_y) { + // The view, not the GUI: they are the same only when no view is enabled, + // and the tiling has to cover what the camera can see. + var _vw = (view_enabled && view_visible[0]) + ? camera_get_view_width(view_camera[0]) + : display_get_gui_width(); + for (var i = 0; i < array_length(stack.layers); i++) { + var _l = stack.layers[i]; + if (!sprite_exists(_l.sprite)) continue; + var _w = sprite_get_width(_l.sprite) * _l.scale; + if (_w <= 0) continue; + + var _off = camera_x * _l.factor + _l.drift; + // Modulo *then* step, so the tiling is anchored to the sprite rather + // than to how far the camera has travelled — the drift that makes a + // long level slowly desynchronise. + var _start = -((_off mod _w) + _w) mod _w; + var _y = _l.y - camera_y * _l.vfactor; + for (var _x = _start - _w; _x < _vw + _w; _x += _w) { + draw_sprite_ext(_l.sprite, 0, camera_x + _x, camera_y + _y, + _l.scale, _l.scale, 0, c_white, 1); + } + } +} + + +/// @function parallax_layer(layer_name, factor, base_y) +/// @description Parallax a *background layer* — art that lives in the room +/// rather than in a Draw event — by offsetting it against the camera. Call +/// once a frame with the layer's name. +/// +/// Worth preferring for the furthest band, because a background layer is +/// room data: the editor draws it, a thumbnail of the room shows it, and a +/// reader of the TOML can see what the sky is. A backdrop drawn in code is +/// invisible to all three, so the room looks nothing like the game. +/// +/// Set the layer's `sprite` and `tiled_x = true` in the room file; this only +/// moves it. `factor` is the same 0..1 as `parallax_add` — 0 is painted on, +/// 1 rides with the world. +/// +/// `base_y` is where the band sits before the camera moves it, normally +/// `parallax_seat_y(sprite, ground_y)`. It is an argument rather than +/// something you set once because this function owns `layer_y`: anything that +/// sets it separately is overwritten on the next frame, and the band vanishes +/// with nothing to say why. +function parallax_layer(layer_name, factor, base_y = 0) { + var _id = layer_get_id(layer_name); + if (!layer_exists(_id)) return false; + // Against the camera, not accumulated per frame: an offset that adds up + // drifts out of step the moment anything skips a frame or the camera is + // moved by hand. + var _cx = camera_get_view_x(view_camera[0]); + var _cy = camera_get_view_y(view_camera[0]); + layer_x(_id, _cx * (1 - factor)); + layer_y(_id, base_y + _cy * (1 - factor)); + return true; +} + + +/// @function parallax_seat_y(sprite, ground_y) +/// @description The `base_y` that lands the *bottom of the art* on `ground_y`. +/// Work it out once in Create and pass it to `parallax_layer` each frame. +/// +/// A background layer draws from its top-left and ignores the sprite's origin, +/// so seating a band on a horizon is arithmetic over where the opaque pixels +/// actually are. Catalog bands are padded, often by a quarter of the canvas, +/// so `ground_y - sprite_get_height()` puts the trees well above the ground. +/// Two bands sized differently then need the sum done twice, and getting it +/// wrong by a few pixels stacks them on each other so the nearer one is never +/// seen. +function parallax_seat_y(sprite, ground_y) { + if (!sprite_exists(sprite)) return 0; + // The drawing's bottom edge within the canvas, measured from the top-left + // — `bbox_bottom` is relative to the origin, so add the origin back. + var _bottom = sprite_get_bbox_bottom(sprite) + sprite_get_yoffset(sprite); + return ground_y - _bottom; +} + +/// @function parallax_order(layer_names) +/// @description Give the named background layers depths that match their order +/// — first is furthest — and return how many were set. +/// +/// Depth and parallax factor have to agree: a band that moves less must also +/// draw further back. Set by hand in two places they drift apart, and the +/// failure is silent and total — the band you meant to be nearer draws behind +/// the far one and is never seen. Pass the same order you pass factors in. +function parallax_order(layer_names) { + var _n = array_length(layer_names); + var _set = 0; + for (var i = 0; i < _n; i++) { + var _id = layer_get_id(layer_names[i]); + if (!layer_exists(_id)) continue; + // Spread so anything placed between two bands still has room. + layer_depth(_id, 100 * (_n - i) + 100); + _set += 1; + } + return _set; +} diff --git a/prefabs/parallax/sprites/spr_demo_far/frame_000.png b/prefabs/parallax/sprites/spr_demo_far/frame_000.png new file mode 100644 index 00000000..417c21c3 Binary files /dev/null and b/prefabs/parallax/sprites/spr_demo_far/frame_000.png differ diff --git a/prefabs/parallax/sprites/spr_demo_far/sprite.toml b/prefabs/parallax/sprites/spr_demo_far/sprite.toml new file mode 100644 index 00000000..265fed33 --- /dev/null +++ b/prefabs/parallax/sprites/spr_demo_far/sprite.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#sprite diff --git a/prefabs/parallax/sprites/spr_demo_near/frame_000.png b/prefabs/parallax/sprites/spr_demo_near/frame_000.png new file mode 100644 index 00000000..e1d487b0 Binary files /dev/null and b/prefabs/parallax/sprites/spr_demo_near/frame_000.png differ diff --git a/prefabs/parallax/sprites/spr_demo_near/sprite.toml b/prefabs/parallax/sprites/spr_demo_near/sprite.toml new file mode 100644 index 00000000..265fed33 --- /dev/null +++ b/prefabs/parallax/sprites/spr_demo_near/sprite.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#sprite diff --git a/prefabs/parallax/thumbnail.png b/prefabs/parallax/thumbnail.png new file mode 100644 index 00000000..f84213e9 Binary files /dev/null and b/prefabs/parallax/thumbnail.png differ diff --git a/prefabs/pathgrid/LICENSE b/prefabs/pathgrid/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/pathgrid/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/pathgrid/README.md b/prefabs/pathgrid/README.md new file mode 100644 index 00000000..8e8005f3 --- /dev/null +++ b/prefabs/pathgrid/README.md @@ -0,0 +1,3 @@ +# pathgrid + +A route around the walls, instead of pressing into a corner. diff --git a/prefabs/pathgrid/objects/obj_demo/Create.gml b/prefabs/pathgrid/objects/obj_demo/Create.gml new file mode 100644 index 00000000..1b6ac531 --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo/Create.gml @@ -0,0 +1,10 @@ +cell = 40; +nav = pathgrid_make(24, 13, cell); + +// A wall with one gap, so the route has to go round rather than through. +for (var r = 2; r < 11; r++) { + if (r == 6) continue; + pathgrid_block(nav, 11, r, true); + instance_create_depth(11 * cell + cell / 2, r * cell + cell / 2, 0, obj_demo_wall); +} +route = []; diff --git a/prefabs/pathgrid/objects/obj_demo/Draw.gml b/prefabs/pathgrid/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..7aabd745 --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo/Draw.gml @@ -0,0 +1,10 @@ +draw_set_colour(c_lime); +draw_circle(80, 260, 10, false); + +// An empty route is a normal state — the target is inside a wall. +draw_set_colour(array_length(route) > 0 ? c_yellow : c_red); +for (var i = 0; i < array_length(route); i++) { + var _p = pathgrid_centre(nav, route[i].col, route[i].row); + draw_circle(_p.x, _p.y, 6, false); +} +draw_set_colour(c_white); diff --git a/prefabs/pathgrid/objects/obj_demo/Draw_GUI.gml b/prefabs/pathgrid/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..7d16c1ac --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,4 @@ +draw_text(40, 30, "pathgrid demo — move the mouse; the route goes round the wall"); +draw_text(40, 50, array_length(route) > 0 + ? string(array_length(route)) + " steps" + : "no route (target is inside a wall)"); diff --git a/prefabs/pathgrid/objects/obj_demo/Step.gml b/prefabs/pathgrid/objects/obj_demo/Step.gml new file mode 100644 index 00000000..56dc84e4 --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo/Step.gml @@ -0,0 +1,3 @@ +var _from = pathgrid_cell_of(nav, 80, 260); +var _to = pathgrid_cell_of(nav, mouse_x, mouse_y); +route = pathgrid_find(nav, _from.col, _from.row, _to.col, _to.row); diff --git a/prefabs/pathgrid/objects/obj_demo/object.toml b/prefabs/pathgrid/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/pathgrid/objects/obj_demo_wall/Draw.gml b/prefabs/pathgrid/objects/obj_demo_wall/Draw.gml new file mode 100644 index 00000000..3a8c9a1a --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo_wall/Draw.gml @@ -0,0 +1,3 @@ +draw_set_colour(c_teal); +draw_rectangle(x - 20, y - 20, x + 20, y + 20, false); +draw_set_colour(c_white); diff --git a/prefabs/pathgrid/objects/obj_demo_wall/object.toml b/prefabs/pathgrid/objects/obj_demo_wall/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/pathgrid/objects/obj_demo_wall/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/pathgrid/prefab.toml b/prefabs/pathgrid/prefab.toml new file mode 100644 index 00000000..590f3ceb --- /dev/null +++ b/prefabs/pathgrid/prefab.toml @@ -0,0 +1,21 @@ +schema_version = 1 +name = "Pathfinding on a grid" + +# A library: installed under `prefabs//` in the target project and +# called as `::pathgrid::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "A* over a grid of blocked cells, four-directional, returning the cells to walk. An empty array means no route exists, which callers must handle — a walled-off target is a normal state, not an error. Blocked cells can be rebuilt from the instances of an object, so a door that opens is a cell that clears." +category = "library" +tags = ["capability:pathfinding", "capability:ai", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_wall", "rooms/rm_demo"] + +steps = [ + "Rebuild the blocked cells after the world changes — a wall that was destroyed stays blocked until something says otherwise.", + "Block from whichever holds the walls: `pathgrid_block_instances(grid, obj_wall)` when they are objects, `pathgrid_block_tilemap(grid, pathgrid_tilemap(\"Tiles\"))` when they are terrain in a tile layer. Terrain nothing counts or destroys belongs in the tile layer, and then there are no instances to walk.", +] + +traps = [ + "`pathgrid_block_tilemap` reads the tile at each cell's *centre*, so it assumes the grid's cell and the tileset's tile size agree. A grid coarser than the tiles samples between them and silently misses walls — the path is then computed through terrain, and the mover walks into it with no error anywhere.", +] diff --git a/prefabs/pathgrid/project.toml b/prefabs/pathgrid/project.toml new file mode 100644 index 00000000..148df789 --- /dev/null +++ b/prefabs/pathgrid/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "pathgrid" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/pathgrid/rooms/rm_demo/layers/Background.toml b/prefabs/pathgrid/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/pathgrid/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/pathgrid/rooms/rm_demo/layers/Instances.toml b/prefabs/pathgrid/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/pathgrid/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/pathgrid/rooms/rm_demo/room.toml b/prefabs/pathgrid/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/pathgrid/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/pathgrid/scripts/scr_pathgrid/scr_pathgrid.gml b/prefabs/pathgrid/scripts/scr_pathgrid/scr_pathgrid.gml new file mode 100644 index 00000000..978304fb --- /dev/null +++ b/prefabs/pathgrid/scripts/scr_pathgrid/scr_pathgrid.gml @@ -0,0 +1,51 @@ +/// Getting from one cell to another without walking into the wall. +/// +/// A* over a grid of blocked cells. The alternative — moving straight at the +/// target and hoping — produces an enemy that presses into a corner forever, +/// which reads as broken rather than as difficult. + +/// @function pathgrid_make(cols, rows, cell) +/// @description A grid of `cols` x `rows` cells, each `cell` pixels square, +/// all walkable. +function pathgrid_make(cols, rows, cell) { + var _blocked = []; + for (var _r = 0; _r < rows; _r++) { + var _row = []; + for (var _c = 0; _c < cols; _c++) array_push(_row, false); + array_push(_blocked, _row); + } + return { cols: cols, rows: rows, cell: cell, blocked: _blocked }; +} + +/// @function pathgrid_block(grid, col, row, solid) +/// @description Mark one cell blocked or clear. +function pathgrid_block(grid, col, row, solid) { + if (!pathgrid_inside(grid, col, row)) return; + grid.blocked[row][col] = solid; +} + +/// @function pathgrid_block_instances(grid, obj) +/// @description Mark every cell holding an instance of `obj` as blocked. +/// Rebuild after the world changes — a door that opened is a cell that is +/// still blocked until something says otherwise. +function pathgrid_block_instances(grid, obj) { + // Held in a local first: inside `with`, `other` is the calling + // *instance*, and `grid` is an argument rather than one of its + // variables — so `other.grid` finds nothing and dies at runtime. + var _g = grid; + with (obj) { + pathgrid_block(_g, floor(x / _g.cell), floor(y / _g.cell), true); + } +} + +/// @function pathgrid_inside(grid, col, row) +function pathgrid_inside(grid, col, row) { + return col >= 0 && row >= 0 && col < grid.cols && row < grid.rows; +} + +/// @function pathgrid_walkable(grid, col, row) +function pathgrid_walkable(grid, col, row) { + if (!pathgrid_inside(grid, col, row)) return false; + return !grid.blocked[row][col]; +} + diff --git a/prefabs/pathgrid/scripts/scr_pathgrid_cells/scr_pathgrid_cells.gml b/prefabs/pathgrid/scripts/scr_pathgrid_cells/scr_pathgrid_cells.gml new file mode 100644 index 00000000..5f577ec3 --- /dev/null +++ b/prefabs/pathgrid/scripts/scr_pathgrid_cells/scr_pathgrid_cells.gml @@ -0,0 +1,13 @@ +/// @function pathgrid_cell_of(grid, px, py) +/// @description Which cell a room position is in, as `{col, row}`. +function pathgrid_cell_of(grid, px, py) { + return { col: floor(px / grid.cell), row: floor(py / grid.cell) }; +} + +/// @function pathgrid_centre(grid, col, row) +/// @description The room position at the middle of a cell, as `{x, y}` — +/// where a mover should aim so it does not clip the corner of a wall. +function pathgrid_centre(grid, col, row) { + return { x: col * grid.cell + grid.cell / 2, y: row * grid.cell + grid.cell / 2 }; +} + diff --git a/prefabs/pathgrid/scripts/scr_pathgrid_find/scr_pathgrid_find.gml b/prefabs/pathgrid/scripts/scr_pathgrid_find/scr_pathgrid_find.gml new file mode 100644 index 00000000..50576dc0 --- /dev/null +++ b/prefabs/pathgrid/scripts/scr_pathgrid_find/scr_pathgrid_find.gml @@ -0,0 +1,62 @@ +/// @function pathgrid_find(grid, from_col, from_row, to_col, to_row) +/// @description A* from one cell to another, four-directionally. Returns an +/// array of `{col, row}` starting at the first step and ending at the goal, +/// or an empty array when there is no route — which a caller must handle, +/// because a walled-off target is a normal state, not an error. +function pathgrid_find(grid, from_col, from_row, to_col, to_row) { + if (!pathgrid_walkable(grid, to_col, to_row)) return []; + if (from_col == to_col && from_row == to_row) return []; + + var _key = function(_c, _r) { return string(_c) + "," + string(_r); }; + var _open = [{ col: from_col, row: from_row, g: 0, + f: abs(from_col - to_col) + abs(from_row - to_row) }]; + var _came = {}; + var _seen = {}; + _seen[$ _key(from_col, from_row)] = 0; + + while (array_length(_open) > 0) { + // Linear scan for the cheapest. A binary heap would be faster; on a + // grid this size it is not the part that costs anything. + var _best = 0; + for (var i = 1; i < array_length(_open); i++) { + if (_open[i].f < _open[_best].f) _best = i; + } + var _cur = _open[_best]; + array_delete(_open, _best, 1); + + if (_cur.col == to_col && _cur.row == to_row) { + var _path = []; + var _at = _key(_cur.col, _cur.row); + var _node = { col: _cur.col, row: _cur.row }; + while (_at != _key(from_col, from_row)) { + array_insert(_path, 0, _node); + _node = _came[$ _at]; + _at = _key(_node.col, _node.row); + } + return _path; + } + + var _steps = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (var s = 0; s < 4; s++) { + var _nc = _cur.col + _steps[s][0]; + var _nr = _cur.row + _steps[s][1]; + if (!pathgrid_walkable(grid, _nc, _nr)) continue; + var _g = _cur.g + 1; + var _nk = _key(_nc, _nr); + if (variable_struct_exists(_seen, _nk) && _seen[$ _nk] <= _g) continue; + _seen[$ _nk] = _g; + _came[$ _nk] = { col: _cur.col, row: _cur.row }; + array_push(_open, { col: _nc, row: _nr, g: _g, + f: _g + abs(_nc - to_col) + abs(_nr - to_row) }); + } + } + return []; +} + +/// @function pathgrid_next_point(grid, path) +/// @description The room position of the first step of a path, or +/// `undefined` for an empty one. What a mover actually needs each frame. +function pathgrid_next_point(grid, path) { + if (array_length(path) == 0) return undefined; + return pathgrid_centre(grid, path[0].col, path[0].row); +} diff --git a/prefabs/pathgrid/scripts/scr_pathgrid_tiles/scr_pathgrid_tiles.gml b/prefabs/pathgrid/scripts/scr_pathgrid_tiles/scr_pathgrid_tiles.gml new file mode 100644 index 00000000..e71aa7b7 --- /dev/null +++ b/prefabs/pathgrid/scripts/scr_pathgrid_tiles/scr_pathgrid_tiles.gml @@ -0,0 +1,37 @@ +/// @function pathgrid_tilemap(layer_name) +/// @description The tilemap id of a layer by name, or `noone` if the room has +/// no such layer. Returns something safe in a room that lacks the layer rather +/// than a -1 that fails much later. +function pathgrid_tilemap(layer_name) { + if (!layer_exists(layer_get_id(layer_name))) return noone; + var _tm = layer_tilemap_get_id(layer_get_id(layer_name)); + return (_tm == -1) ? noone : _tm; +} + +/// @function pathgrid_block_tilemap(grid, tilemap) +/// @description Mark every cell holding a tile as blocked. The counterpart of +/// `pathgrid_block_instances` for terrain that lives in a tile layer rather +/// than as objects — a wall nothing destroys or counts belongs there, and then +/// there are no instances to walk. +/// +/// The grid's cell and the layer's tile size are assumed to agree: this reads +/// the tile at each cell's centre, so a grid coarser than the tiles samples +/// between them and misses walls. `noone` is a room with no such layer, and +/// blocks nothing rather than erroring. +function pathgrid_block_tilemap(grid, tilemap) { + if (tilemap == noone) return; + var _half = grid.cell / 2; + for (var _row = 0; _row < grid.rows; _row++) { + for (var _col = 0; _col < grid.cols; _col++) { + // Tile index 0 is GameMaker's empty tile, so a non-zero index is + // terrain. `tilemap_get_at_pixel` wants room coordinates, and + // returns -1 outside the layer's extent — which is not index 0 + // and would otherwise block every cell the layer does not cover. + var _t = tilemap_get_at_pixel(tilemap, + _col * grid.cell + _half, _row * grid.cell + _half); + if (_t != -1 && tile_get_index(_t) != 0) { + pathgrid_block(grid, _col, _row, true); + } + } + } +} diff --git a/prefabs/pathgrid/thumbnail.png b/prefabs/pathgrid/thumbnail.png new file mode 100644 index 00000000..25dbca61 Binary files /dev/null and b/prefabs/pathgrid/thumbnail.png differ diff --git a/prefabs/pause_menu/LICENSE b/prefabs/pause_menu/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/pause_menu/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/pause_menu/README.md b/prefabs/pause_menu/README.md new file mode 100644 index 00000000..a8b8f864 --- /dev/null +++ b/prefabs/pause_menu/README.md @@ -0,0 +1,45 @@ +# pause_menu + +Freeze the room, snapshot the screen, dim it, and resume on a key. + +## Two approaches, and you must pick one + +This is the whole content of this page. Running both means two pause +states that disagree. + +**Freeze** — what this prefab applies. `instance_deactivate_all` stops +every instance, so nothing in your game has to know pause exists. The +screen is captured to a surface first, because a deactivated room draws +nothing. Costs: deactivated instances do not run alarms or animations, and +anything that must keep going (a background shader, a music visualiser) +has to be excluded by hand. + +**Keep running** — what `::kernel::` already does. The room keeps +simulating and every system gates on `::kernel::kernel_playing()`. Costs: +every gameplay object must check it, and one that forgets keeps moving +behind the pause overlay. Benefit: animations continue, and you can pause +mid-transition without artefacts. + +Freeze suits an action game where the pause is a full stop. Keep-running +suits anything with live presentation behind the menu. + +## What the snapshot does and does not capture + +`application_surface` holds the **world** — everything drawn in `Draw`. +Anything drawn in `Draw_GUI` is composited afterwards and is *not* in it. + +So a game whose HUD, menus or debug text live in `Draw_GUI` will freeze +correctly and then show a snapshot missing all of them, because the +deactivated instances are no longer drawing their GUI either. If that +matters, draw the frozen HUD yourself in the pause overlay, or move the +parts that must persist into `Draw`. + +This is easy to mistake for a broken snapshot. Check by pausing a room +that draws something in `Draw` — if that survives and the GUI does not, +this is what you are seeing. + +## Blurring instead of dimming + +Blur the snapshot **once**, when the pause opens — not every frame. A +few passes at a small radius reads as depth; one pass at a large radius +reads as pixelation. diff --git a/prefabs/pause_menu/objects/obj_demo/Create.gml b/prefabs/pause_menu/objects/obj_demo/Create.gml new file mode 100644 index 00000000..f86b3bdf --- /dev/null +++ b/prefabs/pause_menu/objects/obj_demo/Create.gml @@ -0,0 +1,6 @@ +// The kernel owns the run state, and `kernel_playing()` reads it. Without a +// boot it is simply not set, and the first object to ask dies with a bare +// "not set before reading it" naming a kernel script rather than the caller. +::kernel::kernel_boot(); + +t = 0; diff --git a/prefabs/pause_menu/objects/obj_demo/Draw.gml b/prefabs/pause_menu/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..ad08f4c8 --- /dev/null +++ b/prefabs/pause_menu/objects/obj_demo/Draw.gml @@ -0,0 +1,11 @@ +// Something for the dim to sit over: a paused screenshot of an empty room +// shows the dim and nothing about what pausing is for. +for (var i = 0; i < 9; i++) { + var _x = 120 + (i mod 3) * 320; + var _y = 120 + (i div 3) * 150; + var _p = t / 60 + i; + draw_set_colour(make_colour_hsv((i * 26) mod 255, 150, 220)); + draw_rectangle(_x - 60, _y - 40 + sin(_p) * 18, + _x + 60, _y + 40 + sin(_p) * 18, false); +} +draw_set_colour(c_white); diff --git a/prefabs/pause_menu/objects/obj_demo/Draw_GUI.gml b/prefabs/pause_menu/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..c1250c6f --- /dev/null +++ b/prefabs/pause_menu/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1 @@ +draw_text(40, 30, "pause_menu demo - the room behind is deactivated, not just covered"); diff --git a/prefabs/pause_menu/objects/obj_demo/Step.gml b/prefabs/pause_menu/objects/obj_demo/Step.gml new file mode 100644 index 00000000..b6a3d54b --- /dev/null +++ b/prefabs/pause_menu/objects/obj_demo/Step.gml @@ -0,0 +1,20 @@ +t += 1; + +// Pausing for real needs a keypress, which a headless capture has no way to +// make. This drives the same three steps the keypress does, in the same +// order: snapshot first, because `instance_deactivate_all` empties the room +// and a surface copied after it captures nothing. +if (t == 60 && instance_exists(obj_pause)) { + with (obj_pause) { + if (surface_exists(application_surface)) { + var _w = surface_get_width(application_surface); + var _h = surface_get_height(application_surface); + if (snap != -1 && surface_exists(snap)) surface_free(snap); + snap = surface_create(_w, _h); + surface_copy(snap, 0, 0, application_surface); + } + instance_deactivate_all(true); + paused = true; + age = 0; + } +} diff --git a/prefabs/pause_menu/objects/obj_demo/object.toml b/prefabs/pause_menu/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/pause_menu/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/pause_menu/objects/obj_pause/Create.gml b/prefabs/pause_menu/objects/obj_pause/Create.gml new file mode 100644 index 00000000..bc8896c4 --- /dev/null +++ b/prefabs/pause_menu/objects/obj_pause/Create.gml @@ -0,0 +1,10 @@ +// This object owns pausing, so the kernel's own must stand down — +// otherwise one keypress drives two pause systems at once. +::kernel::kernel_pause_enabled(false); + +paused = false; +ramp = 0; +snap = -1; +// Guard against the keypress that opened the menu also closing it on the +// very next frame. +age = 0; diff --git a/prefabs/pause_menu/objects/obj_pause/Destroy.gml b/prefabs/pause_menu/objects/obj_pause/Destroy.gml new file mode 100644 index 00000000..71463e4f --- /dev/null +++ b/prefabs/pause_menu/objects/obj_pause/Destroy.gml @@ -0,0 +1 @@ +if (snap != -1 && surface_exists(snap)) surface_free(snap); diff --git a/prefabs/pause_menu/objects/obj_pause/Draw_GUI.gml b/prefabs/pause_menu/objects/obj_pause/Draw_GUI.gml new file mode 100644 index 00000000..f38630b6 --- /dev/null +++ b/prefabs/pause_menu/objects/obj_pause/Draw_GUI.gml @@ -0,0 +1,17 @@ +if (ramp <= 0.001) exit; + +var _cfg = pause_menu_tuning(); +var _w = ::kernel::kernel_gui_width(); +var _h = ::kernel::kernel_gui_height(); + +// The world is deactivated, so nothing is drawing it — the snapshot is +// the only thing keeping the game on screen behind the dim. +if (snap != -1 && surface_exists(snap)) { + draw_surface_stretched(snap, 0, 0, _w, _h); +} + +::kernel::kernel_draw_panel(0, 0, _w, _h, c_black, _cfg.dim * ramp); +draw_set_alpha(ramp); +::kernel::kernel_draw_text(_w / 2, _h / 2 - 12, "- paused -", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, _h / 2 + 16, "P to resume", c_ltgray, fa_center); +draw_set_alpha(1); diff --git a/prefabs/pause_menu/objects/obj_pause/Step.gml b/prefabs/pause_menu/objects/obj_pause/Step.gml new file mode 100644 index 00000000..c1dfc110 --- /dev/null +++ b/prefabs/pause_menu/objects/obj_pause/Step.gml @@ -0,0 +1,29 @@ +var _dt = delta_time / 1000000; +var _cfg = pause_menu_tuning(); +ramp += ((paused ? 1 : 0) - ramp) * min(1, _dt / max(_cfg.fade_seconds, 0.0001)); + +if (!paused) { + if (::kernel::kernel_action_pressed("pause")) { + // Snapshot BEFORE deactivating, or the surface captures an + // already-empty room. + if (surface_exists(application_surface)) { + var _w = surface_get_width(application_surface); + var _h = surface_get_height(application_surface); + if (snap != -1 && surface_exists(snap)) surface_free(snap); + snap = surface_create(_w, _h); + surface_copy(snap, 0, 0, application_surface); + } + instance_deactivate_all(true); + audio_pause_all(); + paused = true; + age = 0; + } + exit; +} + +age += _dt; +if (age > 0.15 && ::kernel::kernel_action_pressed("pause")) { + instance_activate_all(); + audio_resume_all(); + paused = false; +} diff --git a/prefabs/pause_menu/objects/obj_pause/object.toml b/prefabs/pause_menu/objects/obj_pause/object.toml new file mode 100644 index 00000000..3bda8a05 --- /dev/null +++ b/prefabs/pause_menu/objects/obj_pause/object.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#object + +# Not deactivated by its own instance_deactivate_all — an object that +# freezes the room has to keep running to unfreeze it. +persistent = true diff --git a/prefabs/pause_menu/prefab.toml b/prefabs/pause_menu/prefab.toml new file mode 100644 index 00000000..cc3c5f05 --- /dev/null +++ b/prefabs/pause_menu/prefab.toml @@ -0,0 +1,25 @@ +schema_version = 1 +name = "Pause menu (freeze)" +description = "Freeze the room, snapshot the screen, dim it, and resume on a key." +category = "component" +tags = ["capability:ui", "role:template"] + +install = "copy" +requires = ["kernel"] + +seam = "objects/obj_pause/Step.gml" + +steps = [ + "Place obj_pause on an instance layer in your game room.", + "The snapshot captures application_surface, which is world drawing only — anything in Draw_GUI is composited after it and will be missing while paused.", + "This is the FREEZE approach: instance_deactivate_all stops everything, so nothing has to check a flag. The kernel's own pause is the other approach — it keeps the room running and every pack gates on kernel_playing(). Pick one; running both means two pause states.", +] + +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +[roles] +obj_pause = "owns pausing: snapshots the screen, deactivates the room behind it, and dims on a ramp" + +[tuning] +dim = 0.62 +fade_seconds = 0.18 diff --git a/prefabs/pause_menu/project.toml b/prefabs/pause_menu/project.toml new file mode 100644 index 00000000..35a1179c --- /dev/null +++ b/prefabs/pause_menu/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "pause-menu" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/pause_menu/rooms/rm_demo/layers/Background.toml b/prefabs/pause_menu/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/pause_menu/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/pause_menu/rooms/rm_demo/layers/Instances.toml b/prefabs/pause_menu/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..b9b6881e --- /dev/null +++ b/prefabs/pause_menu/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_pause" +object = "obj_pause" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/pause_menu/rooms/rm_demo/room.toml b/prefabs/pause_menu/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/pause_menu/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/pause_menu/thumbnail.png b/prefabs/pause_menu/thumbnail.png new file mode 100644 index 00000000..76bf4a06 Binary files /dev/null and b/prefabs/pause_menu/thumbnail.png differ diff --git a/prefabs/physics_arena/LICENSE b/prefabs/physics_arena/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/physics_arena/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/physics_arena/README.md b/prefabs/physics_arena/README.md new file mode 100644 index 00000000..69b45298 --- /dev/null +++ b/prefabs/physics_arena/README.md @@ -0,0 +1,56 @@ +# physics_arena + +Roll a ball with real physics, knock out the targets, beat the clock. + +## `physics_world = true` or nothing happens + +The play room sets it. Without that flag the room looks entirely correct, +every body is declared correctly, and nothing simulates. It is the first +thing to check when a physics game does nothing. + +The second thing: **built-in `speed`/`direction`/`hspeed`/`vspeed` do nothing +to instances in a physics room.** An object relying on them sits still while +looking perfectly configured. Move bodies with `phy_*` or +`physics_apply_*`, and non-physics objects by writing `x` and `y` yourself. + +## Restitution is the feel, and the trap + +`restitution = 1.05` on the bumpers is why the arena has energy. Above about +1.1 the ball *gains* speed on every bounce, and the speed cap stops being a +safety net and becomes the only thing keeping it in the room. + +That is why `phys_clamp_speed` runs every frame even though `phy_bullet` is +also set. Continuous collision handles a fast body; it does not handle a body +that keeps getting faster. + +## Fixtures are in final pixels + +`[physics.body.shape]` gives the ball a 17px radius directly, and deliberately +does **not** use `scale = "instance"`. That option bakes the fixture at +instance-creation scale — which runs *before* `Create.gml` can set +`image_xscale` — so a sprite scaled in Create ends up with a hitbox from +before the scaling. The jam bug this comes from left a 74px fixture on a 16px +gem. + +## Impact is a velocity delta, not a collision event + +A target breaks on a hard enough hit, and "hard enough" is how much the +ball's velocity *changed* this frame. That gives one event per bounce with a +magnitude, rather than a collision event per frame of contact with none. + +`break_impact` and `speed_cap` are both **pixels per step**, the same unit as +every other speed in a GameMaker game. + +That is worth stating because GameMaker offers the velocity twice: +`phy_speed_*` per step and `phy_linear_velocity_*` per second, `room_speed` +apart. Measured on the runner, a linear velocity of 30 is a `phy_speed` of +0.499 and moves the body 14.7px in 30 frames. A physics library quoting +seconds inside a catalog that quotes steps is how a launch speed of 22 ends up +moving a ball a third of a pixel per frame — which is exactly what this prefab +did until the numbers were measured rather than assumed. + +## What it does not do + +No flippers, no launcher, no gravity — `physics_gravity_y` is 0, so this is a +top-down table. A pinball table is this plus gravity, two `revolute` joints +and a plunger. diff --git a/prefabs/physics_arena/demo.gametest.json b/prefabs/physics_arena/demo.gametest.json new file mode 100644 index 00000000..d3cbd4c2 --- /dev/null +++ b/prefabs/physics_arena/demo.gametest.json @@ -0,0 +1,110 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 140, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_p0=undefined; global.gt_ballmoved=false; global.gt_targets=0; global.gt_t0=undefined; global.gt_counts=false;" + }, + "watch": { + "gml": "if (instance_exists(obj_ball)) { with (obj_ball) { global.gt_seen = true; if (global.gt_p0 == undefined) global.gt_p0 = { x: x, y: y }; if (point_distance(x, y, global.gt_p0.x, global.gt_p0.y) > 8) global.gt_ballmoved = true; } } global.gt_targets = max(global.gt_targets, instance_number(obj_target)); if (instance_exists(obj_arena)) { with (obj_arena) { if (global.gt_t0 == undefined) global.gt_t0 = clock.left; if (clock.left < global.gt_t0 - 0.5) global.gt_counts = true; } }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "moves": { + "gml": "return global.gt_ballmoved;" + }, + "targets": { + "gml": "return global.gt_targets > 0;" + }, + "counts": { + "gml": "return global.gt_counts;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 90, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 130, + "assert": { + "id": "seen", + "name": "the ball is in the arena", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "targets", + "name": "the arena is set with targets", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "moves", + "name": "the ball rolls under the physics step", + "expect": true + } + }, + { + "frame": 130, + "assert": { + "id": "counts", + "name": "the round clock counts down", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 128, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/physics_arena/objects/obj_arena/Create.gml b/prefabs/physics_arena/objects/obj_arena/Create.gml new file mode 100644 index 00000000..ed677835 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_arena/Create.gml @@ -0,0 +1,5 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +clock = ::timer::timer_countdown(::kernel::kernel_tuning("round_seconds", 45)); +total_targets = 0; diff --git a/prefabs/physics_arena/objects/obj_arena/Draw_GUI.gml b/prefabs/physics_arena/objects/obj_arena/Draw_GUI.gml new file mode 100644 index 00000000..6142fff0 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_arena/Draw_GUI.gml @@ -0,0 +1,9 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, + "targets " + string(instance_number(obj_target)) + " / " + string(total_targets)); + +var _a = ::hud::hud_anchor("tr", 220, 16); +::kernel::kernel_draw_text(_a.x, _a.y, + "time " + ::timer::timer_format(::timer::timer_seconds(clock))); +::hud::hud_bar(_a.x, _a.y + 26, 200, 14, + ::timer::timer_fraction(clock), c_aqua); diff --git a/prefabs/physics_arena/objects/obj_arena/Other_RoomStart.gml b/prefabs/physics_arena/objects/obj_arena/Other_RoomStart.gml new file mode 100644 index 00000000..8ed4ec32 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_arena/Other_RoomStart.gml @@ -0,0 +1,4 @@ +// Counted here, not in Create: Create runs as each instance is made, so a +// controller placed first in the layer would count a room that is still +// half-built. Room Start runs once every instance exists. +total_targets = instance_number(obj_target); diff --git a/prefabs/physics_arena/objects/obj_arena/Step.gml b/prefabs/physics_arena/objects/obj_arena/Step.gml new file mode 100644 index 00000000..e222592f --- /dev/null +++ b/prefabs/physics_arena/objects/obj_arena/Step.gml @@ -0,0 +1,16 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); + +// True on exactly one frame, so the round ends once. +if (::timer::timer_step(clock)) { + ::kernel::kernel_game_over("time"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); + exit; +} + +if (instance_number(obj_target) == 0) { + ::kernel::kernel_game_over("cleared"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); +} diff --git a/prefabs/physics_arena/objects/obj_arena/object.toml b/prefabs/physics_arena/objects/obj_arena/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/physics_arena/objects/obj_arena/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/physics_arena/objects/obj_ball/Create.gml b/prefabs/physics_arena/objects/obj_ball/Create.gml new file mode 100644 index 00000000..65e6f7a0 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_ball/Create.gml @@ -0,0 +1,16 @@ +// Continuous collision first; the cap is the backstop behind it. +phy_bullet = true; +body = ::physics_body::phys_make(::kernel::kernel_tuning("speed_cap", 30)); +squash = 0; + +// The ball launches itself, so a second one dragged into the room in an +// editor works with no code change at all. +::physics_body::phys_launch(irandom(359), + ::kernel::kernel_tuning("launch_speed", 22)); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +break_impact = ::kernel::kernel_tuning("break_impact", 9); +target_score = ::kernel::kernel_tuning("target_score", 100); +nudge_force = ::kernel::kernel_tuning("nudge_force", 0.4); +launch_speed = ::kernel::kernel_tuning("launch_speed", 22); diff --git a/prefabs/physics_arena/objects/obj_ball/Draw.gml b/prefabs/physics_arena/objects/obj_ball/Draw.gml new file mode 100644 index 00000000..b6a7da97 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_ball/Draw.gml @@ -0,0 +1,4 @@ +var _s = 1 + squash * 0.45; +draw_sprite_ext(sprite_index, 0, x, y, _s, 2 - _s, + point_direction(0, 0, phy_speed_x, phy_speed_y), + c_white, 1); diff --git a/prefabs/physics_arena/objects/obj_ball/Step.gml b/prefabs/physics_arena/objects/obj_ball/Step.gml new file mode 100644 index 00000000..cddffcd0 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_ball/Step.gml @@ -0,0 +1,53 @@ +var _ctl = instance_find(obj_arena, 0); +if (_ctl == noone || !::kernel::kernel_playing()) exit; + +// Impact first, before anything else touches the velocity. +var _hit = ::physics_body::phys_impact(body, 2); +if (_hit > 0) { + squash = min(1, _hit / 30); + var _break = break_impact; + if (_hit >= _break) { + // What the ball is actually touching, not whatever target happens to + // be nearest. Targets sit at (480, 90) and (480, 450) and the wall + // faces are ~50px beyond them, so "nearest within 60" scored a kill + // for bouncing off the wall above a target without going near it. + var _t = instance_place(x, y, obj_target); + if (_t != noone) { + ::kernel::kernel_score_add(target_score); + ::feel::feel_pop(_t.x, _t.y, "+" + + string(target_score), c_yellow); + ::feel::feel_shake(0.2, 7); + instance_destroy(_t); + } + } +} +squash *= 0.86; + +::physics_body::phys_clamp_speed(body); + +// A ball at rest in a corner ends no round. +if (::physics_body::phys_stalled(body, 1.5, 1.0)) { + ::physics_body::phys_nudge(irandom(359), nudge_force); +} + +// Bodies do escape, through a seam or at a speed the solver could not +// resolve. One that has left is gone unless something notices. +if (::physics_body::phys_out_of_bounds(90)) { + // Not the room centre — a bumper is placed there. Pinning a dynamic body + // inside a static one with restitution above 1 makes Box2D eject it hard, + // and a `phy_bullet` ejected hard can leave the room again on the next + // step: a pin/eject loop rather than a recovery. + var _rx = room_width / 2; + var _ry = room_height / 2; + var _tries = 0; + while (_tries < 16 + && (position_meeting(_rx, _ry, obj_bumper) + || position_meeting(_rx, _ry, obj_target) + || position_meeting(_rx, _ry, obj_wall))) { + _rx = irandom_range(120, room_width - 120); + _ry = irandom_range(120, room_height - 120); + _tries += 1; + } + ::physics_body::phys_pin(_rx, _ry); + ::physics_body::phys_launch(irandom(359), launch_speed); +} diff --git a/prefabs/physics_arena/objects/obj_ball/object.toml b/prefabs/physics_arena/objects/obj_ball/object.toml new file mode 100644 index 00000000..68aa40ad --- /dev/null +++ b/prefabs/physics_arena/objects/obj_ball/object.toml @@ -0,0 +1,21 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_ball" + +[physics] +kind = "dynamic" + +[physics.body] +density = 0.6 +restitution = 0.7 +friction = 0.02 +linear_damping = 0.08 +angular_damping = 0.4 + +# Final pixels, deliberately not `scale = "instance"` — that bakes the +# fixture at instance-creation scale, which runs before Create.gml can set +# image_xscale, and the hitbox stops matching the art. +[physics.body.shape] +type = "circle" +centre = [17.0, 17.0] +radius = 17.0 diff --git a/prefabs/physics_arena/objects/obj_bumper/object.toml b/prefabs/physics_arena/objects/obj_bumper/object.toml new file mode 100644 index 00000000..4be21adf --- /dev/null +++ b/prefabs/physics_arena/objects/obj_bumper/object.toml @@ -0,0 +1,18 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_bumper" + +[physics] +kind = "static" + +[physics.body] +density = 0.0 +# Where most of the energy comes from. Past ~1.1 the ball gains speed on +# every bounce and the cap becomes the only thing keeping it in the room. +restitution = 1.05 +friction = 0.0 + +[physics.body.shape] +type = "circle" +centre = [26.0, 26.0] +radius = 26.0 diff --git a/prefabs/physics_arena/objects/obj_results/Draw_GUI.gml b/prefabs/physics_arena/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/physics_arena/objects/obj_results/Step.gml b/prefabs/physics_arena/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/physics_arena/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/physics_arena/objects/obj_results/object.toml b/prefabs/physics_arena/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/physics_arena/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/physics_arena/objects/obj_target/object.toml b/prefabs/physics_arena/objects/obj_target/object.toml new file mode 100644 index 00000000..e3465e1e --- /dev/null +++ b/prefabs/physics_arena/objects/obj_target/object.toml @@ -0,0 +1,16 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_target" + +[physics] +kind = "static" + +[physics.body] +density = 0.0 +restitution = 0.7 +friction = 0.1 + +[physics.body.shape] +type = "circle" +centre = [18.0, 18.0] +radius = 18.0 diff --git a/prefabs/physics_arena/objects/obj_title/Create.gml b/prefabs/physics_arena/objects/obj_title/Create.gml new file mode 100644 index 00000000..760ce6a7 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(physics_arena_data, physics_arena_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/physics_arena/objects/obj_title/Draw_GUI.gml b/prefabs/physics_arena/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..b7af2572 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "ROLL", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "roll the ball, hit the targets, beat the clock", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/physics_arena/objects/obj_title/Step.gml b/prefabs/physics_arena/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/physics_arena/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/physics_arena/objects/obj_title/object.toml b/prefabs/physics_arena/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/physics_arena/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/physics_arena/objects/obj_wall/object.toml b/prefabs/physics_arena/objects/obj_wall/object.toml new file mode 100644 index 00000000..c42cc40c --- /dev/null +++ b/prefabs/physics_arena/objects/obj_wall/object.toml @@ -0,0 +1,16 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_wall" + +[physics] +kind = "static" + +[physics.body] +density = 0.0 +restitution = 0.4 +friction = 0.2 + +[physics.body.shape] +type = "aabb" +origin = [0.0, 0.0] +size = [40.0, 40.0] diff --git a/prefabs/physics_arena/prefab.toml b/prefabs/physics_arena/prefab.toml new file mode 100644 index 00000000..db91e137 --- /dev/null +++ b/prefabs/physics_arena/prefab.toml @@ -0,0 +1,40 @@ +schema_version = 1 +name = "Physics arena" +description = "Roll a ball with real physics, knock out the targets, beat the clock." +category = "game" +tags = ["genre:physics", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "physics_body", "timer", "hud"] + +seam = "scripts/scr_physics_arena_data/scr_physics_arena_data.gml" + +steps = [ + "`physics_arena.gametest.json` came with the prefab and already passes: the ball is in the arena with targets set, it rolls under the physics step, and the round clock counts down. Run it with `gmx test physics_arena.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The play room needs `physics_world = true`, and it already has it — a physics room without that flag simulates nothing while looking entirely correct.", + "`restitution` on the bumpers is what makes the arena feel alive. Above about 1.1 the ball gains energy every bounce and the speed cap becomes the only thing keeping it in the room.", + "Built-in `speed`/`direction` motion does nothing to instances in a physics room. Move bodies with `phy_*` or `physics_apply_*`, and anything else by writing x and y yourself.", +] +traps = [ + "`obj_ball`, `obj_bumper` and `obj_target` have physics bodies. The solver owns their position, so assigning `x` or `y` is discarded and the instance simply never moves — no error, nothing on screen. Use `phy_position_x` / `phy_position_y` for a kinematic or static body, or apply a force or impulse to a dynamic one.", +] + +[roles] +obj_arena = "builds the arena, counts the targets, runs the clock" +obj_ball = "the physics body — capped, impact-aware, and put back if it escapes" +obj_target = "knocked out by a hard enough hit; scores" +obj_bumper = "bouncy, static, and where most of the energy comes from" +obj_wall = "the arena edge" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game, one physics room" +rm_results = "targets cleared and the best score" + +[tuning] +round_seconds = 45 +launch_speed = 9 +speed_cap = 14 +# How hard a hit has to be to knock a target out, in pixels per step — the +# same scale as `speed_cap`, so a threshold above the cap can never fire. +break_impact = 4 +target_score = 100 +nudge_force = 0.4 diff --git a/prefabs/physics_arena/project.toml b/prefabs/physics_arena/project.toml new file mode 100644 index 00000000..634d9b58 --- /dev/null +++ b/prefabs/physics_arena/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "physics_arena" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/physics_arena/rooms/rm_play/layers/Background.toml b/prefabs/physics_arena/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/physics_arena/rooms/rm_play/layers/Instances.toml b/prefabs/physics_arena/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..6248c57e --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,525 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_arena_0" +object = "obj_arena" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_rm_play_obj_wall_0" +object = "obj_wall" +x = 20.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_1" +object = "obj_wall" +x = 60.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_2" +object = "obj_wall" +x = 100.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_3" +object = "obj_wall" +x = 140.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_4" +object = "obj_wall" +x = 180.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_5" +object = "obj_wall" +x = 220.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_6" +object = "obj_wall" +x = 260.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_7" +object = "obj_wall" +x = 300.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_8" +object = "obj_wall" +x = 340.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_9" +object = "obj_wall" +x = 380.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_10" +object = "obj_wall" +x = 420.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_11" +object = "obj_wall" +x = 460.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_12" +object = "obj_wall" +x = 500.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_13" +object = "obj_wall" +x = 540.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_14" +object = "obj_wall" +x = 580.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_15" +object = "obj_wall" +x = 620.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_16" +object = "obj_wall" +x = 660.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_17" +object = "obj_wall" +x = 700.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_18" +object = "obj_wall" +x = 740.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_19" +object = "obj_wall" +x = 780.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_20" +object = "obj_wall" +x = 820.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_21" +object = "obj_wall" +x = 860.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_22" +object = "obj_wall" +x = 900.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_23" +object = "obj_wall" +x = 940.0 +y = 20.0 + +[[entries]] +id = "inst_rm_play_obj_wall_24" +object = "obj_wall" +x = 20.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_25" +object = "obj_wall" +x = 60.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_26" +object = "obj_wall" +x = 100.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_27" +object = "obj_wall" +x = 140.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_28" +object = "obj_wall" +x = 180.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_29" +object = "obj_wall" +x = 220.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_30" +object = "obj_wall" +x = 260.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_31" +object = "obj_wall" +x = 300.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_32" +object = "obj_wall" +x = 340.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_33" +object = "obj_wall" +x = 380.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_34" +object = "obj_wall" +x = 420.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_35" +object = "obj_wall" +x = 460.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_36" +object = "obj_wall" +x = 500.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_37" +object = "obj_wall" +x = 540.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_38" +object = "obj_wall" +x = 580.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_39" +object = "obj_wall" +x = 620.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_40" +object = "obj_wall" +x = 660.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_41" +object = "obj_wall" +x = 700.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_42" +object = "obj_wall" +x = 740.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_43" +object = "obj_wall" +x = 780.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_44" +object = "obj_wall" +x = 820.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_45" +object = "obj_wall" +x = 860.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_46" +object = "obj_wall" +x = 900.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_47" +object = "obj_wall" +x = 940.0 +y = 520.0 + +[[entries]] +id = "inst_rm_play_obj_wall_48" +object = "obj_wall" +x = 20.0 +y = 60.0 + +[[entries]] +id = "inst_rm_play_obj_wall_49" +object = "obj_wall" +x = 20.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_wall_50" +object = "obj_wall" +x = 20.0 +y = 140.0 + +[[entries]] +id = "inst_rm_play_obj_wall_51" +object = "obj_wall" +x = 20.0 +y = 180.0 + +[[entries]] +id = "inst_rm_play_obj_wall_52" +object = "obj_wall" +x = 20.0 +y = 220.0 + +[[entries]] +id = "inst_rm_play_obj_wall_53" +object = "obj_wall" +x = 20.0 +y = 260.0 + +[[entries]] +id = "inst_rm_play_obj_wall_54" +object = "obj_wall" +x = 20.0 +y = 300.0 + +[[entries]] +id = "inst_rm_play_obj_wall_55" +object = "obj_wall" +x = 20.0 +y = 340.0 + +[[entries]] +id = "inst_rm_play_obj_wall_56" +object = "obj_wall" +x = 20.0 +y = 380.0 + +[[entries]] +id = "inst_rm_play_obj_wall_57" +object = "obj_wall" +x = 20.0 +y = 420.0 + +[[entries]] +id = "inst_rm_play_obj_wall_58" +object = "obj_wall" +x = 20.0 +y = 460.0 + +[[entries]] +id = "inst_rm_play_obj_wall_59" +object = "obj_wall" +x = 20.0 +y = 500.0 + +[[entries]] +id = "inst_rm_play_obj_wall_60" +object = "obj_wall" +x = 940.0 +y = 60.0 + +[[entries]] +id = "inst_rm_play_obj_wall_61" +object = "obj_wall" +x = 940.0 +y = 100.0 + +[[entries]] +id = "inst_rm_play_obj_wall_62" +object = "obj_wall" +x = 940.0 +y = 140.0 + +[[entries]] +id = "inst_rm_play_obj_wall_63" +object = "obj_wall" +x = 940.0 +y = 180.0 + +[[entries]] +id = "inst_rm_play_obj_wall_64" +object = "obj_wall" +x = 940.0 +y = 220.0 + +[[entries]] +id = "inst_rm_play_obj_wall_65" +object = "obj_wall" +x = 940.0 +y = 260.0 + +[[entries]] +id = "inst_rm_play_obj_wall_66" +object = "obj_wall" +x = 940.0 +y = 300.0 + +[[entries]] +id = "inst_rm_play_obj_wall_67" +object = "obj_wall" +x = 940.0 +y = 340.0 + +[[entries]] +id = "inst_rm_play_obj_wall_68" +object = "obj_wall" +x = 940.0 +y = 380.0 + +[[entries]] +id = "inst_rm_play_obj_wall_69" +object = "obj_wall" +x = 940.0 +y = 420.0 + +[[entries]] +id = "inst_rm_play_obj_wall_70" +object = "obj_wall" +x = 940.0 +y = 460.0 + +[[entries]] +id = "inst_rm_play_obj_wall_71" +object = "obj_wall" +x = 940.0 +y = 500.0 + +[[entries]] +id = "inst_rm_play_obj_bumper_0" +object = "obj_bumper" +x = 480.0 +y = 270.0 + +[[entries]] +id = "inst_rm_play_obj_bumper_1" +object = "obj_bumper" +x = 300.0 +y = 180.0 + +[[entries]] +id = "inst_rm_play_obj_bumper_2" +object = "obj_bumper" +x = 660.0 +y = 180.0 + +[[entries]] +id = "inst_rm_play_obj_bumper_3" +object = "obj_bumper" +x = 300.0 +y = 380.0 + +[[entries]] +id = "inst_rm_play_obj_bumper_4" +object = "obj_bumper" +x = 660.0 +y = 380.0 + +[[entries]] +id = "inst_rm_play_obj_target_0" +object = "obj_target" +x = 140.0 +y = 110.0 + +[[entries]] +id = "inst_rm_play_obj_target_1" +object = "obj_target" +x = 820.0 +y = 110.0 + +[[entries]] +id = "inst_rm_play_obj_target_2" +object = "obj_target" +x = 140.0 +y = 430.0 + +[[entries]] +id = "inst_rm_play_obj_target_3" +object = "obj_target" +x = 820.0 +y = 430.0 + +[[entries]] +id = "inst_rm_play_obj_target_4" +object = "obj_target" +x = 480.0 +y = 90.0 + +[[entries]] +id = "inst_rm_play_obj_target_5" +object = "obj_target" +x = 480.0 +y = 450.0 + +[[entries]] +id = "inst_rm_play_obj_target_6" +object = "obj_target" +x = 140.0 +y = 270.0 + +[[entries]] +id = "inst_rm_play_obj_target_7" +object = "obj_target" +x = 820.0 +y = 270.0 + +[[entries]] +id = "inst_rm_play_obj_ball_0" +object = "obj_ball" +x = 480.0 +y = 130.0 diff --git a/prefabs/physics_arena/rooms/rm_play/room.toml b/prefabs/physics_arena/rooms/rm_play/room.toml new file mode 100644 index 00000000..41f8e595 --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_play/room.toml @@ -0,0 +1,10 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } + +# Without this the bodies do not simulate at all, while everything +# else about the room looks correct. +physics_world = true +physics_gravity_x = 0.0 +physics_gravity_y = 0.0 diff --git a/prefabs/physics_arena/rooms/rm_results/layers/Background.toml b/prefabs/physics_arena/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/physics_arena/rooms/rm_results/layers/Instances.toml b/prefabs/physics_arena/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/physics_arena/rooms/rm_results/room.toml b/prefabs/physics_arena/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/physics_arena/rooms/rm_title/layers/Background.toml b/prefabs/physics_arena/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/physics_arena/rooms/rm_title/layers/Instances.toml b/prefabs/physics_arena/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/physics_arena/rooms/rm_title/room.toml b/prefabs/physics_arena/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/physics_arena/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/physics_arena/sprites/spr_ball/frame_000.png b/prefabs/physics_arena/sprites/spr_ball/frame_000.png new file mode 100644 index 00000000..b1e0e572 Binary files /dev/null and b/prefabs/physics_arena/sprites/spr_ball/frame_000.png differ diff --git a/prefabs/physics_arena/sprites/spr_ball/sprite.toml b/prefabs/physics_arena/sprites/spr_ball/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_arena/sprites/spr_ball/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_arena/sprites/spr_bumper/frame_000.png b/prefabs/physics_arena/sprites/spr_bumper/frame_000.png new file mode 100644 index 00000000..40bd3a22 Binary files /dev/null and b/prefabs/physics_arena/sprites/spr_bumper/frame_000.png differ diff --git a/prefabs/physics_arena/sprites/spr_bumper/sprite.toml b/prefabs/physics_arena/sprites/spr_bumper/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_arena/sprites/spr_bumper/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_arena/sprites/spr_target/frame_000.png b/prefabs/physics_arena/sprites/spr_target/frame_000.png new file mode 100644 index 00000000..c0bfd5a2 Binary files /dev/null and b/prefabs/physics_arena/sprites/spr_target/frame_000.png differ diff --git a/prefabs/physics_arena/sprites/spr_target/sprite.toml b/prefabs/physics_arena/sprites/spr_target/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_arena/sprites/spr_target/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_arena/sprites/spr_wall/frame_000.png b/prefabs/physics_arena/sprites/spr_wall/frame_000.png new file mode 100644 index 00000000..a36af715 Binary files /dev/null and b/prefabs/physics_arena/sprites/spr_wall/frame_000.png differ diff --git a/prefabs/physics_arena/sprites/spr_wall/sprite.toml b/prefabs/physics_arena/sprites/spr_wall/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_arena/sprites/spr_wall/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_arena/thumbnail.png b/prefabs/physics_arena/thumbnail.png new file mode 100644 index 00000000..fd7eadfc Binary files /dev/null and b/prefabs/physics_arena/thumbnail.png differ diff --git a/prefabs/physics_body/LICENSE b/prefabs/physics_body/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/physics_body/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/physics_body/README.md b/prefabs/physics_body/README.md new file mode 100644 index 00000000..7286dc7a --- /dev/null +++ b/prefabs/physics_body/README.md @@ -0,0 +1,3 @@ +# physics_body + +Speed caps, impact detection, stall recovery and a pause that actually pauses. diff --git a/prefabs/physics_body/objects/obj_demo/Create.gml b/prefabs/physics_body/objects/obj_demo/Create.gml new file mode 100644 index 00000000..1cc3031a --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo/Create.gml @@ -0,0 +1,17 @@ +paused = false; + +// A ring of static walls plus a few obstacles, so bounces happen often. +for (var _x = 0; _x < room_width; _x += 40) { + instance_create_depth(_x, 0, 0, obj_demo_wall); + instance_create_depth(_x, room_height - 40, 0, obj_demo_wall); +} +for (var _y = 40; _y < room_height - 40; _y += 40) { + instance_create_depth(0, _y, 0, obj_demo_wall); + instance_create_depth(room_width - 40, _y, 0, obj_demo_wall); +} +for (var i = 0; i < 5; i++) { + instance_create_depth(220 + i * 130, 200 + (i mod 2) * 140, 0, obj_demo_wall); +} + +ball = instance_create_depth(480, 130, 0, obj_demo_ball); +with (ball) phys_launch(irandom(359), 9); diff --git a/prefabs/physics_body/objects/obj_demo/Draw_GUI.gml b/prefabs/physics_body/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..0be9a310 --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,7 @@ +draw_text(40, 30, "physics-body demo — SPACE relaunches, P pauses, ENTER pins"); +if (!instance_exists(ball)) exit; +draw_text(40, 60, "speed " + string_format(ball.body.cap, 2, 0) + + " cap, now " + string_format( + point_distance(0, 0, ball.phy_speed_x, ball.phy_speed_y), 2, 1)); +draw_text(40, 80, "bounces " + string(ball.hits)); +draw_text(40, 100, paused ? "PAUSED — the body is stopped, not just un-stepped" : "running"); diff --git a/prefabs/physics_body/objects/obj_demo/Step.gml b/prefabs/physics_body/objects/obj_demo/Step.gml new file mode 100644 index 00000000..67a837f9 --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo/Step.gml @@ -0,0 +1,14 @@ +// A real pause: the body is stopped, not merely un-stepped. Skipping your +// own Step leaves the physics world integrating underneath it. +if (keyboard_check_pressed(ord("P"))) { + paused = !paused; + with (ball) { + if (other.paused) phys_freeze(body); else phys_thaw(body); + } +} +if (keyboard_check_pressed(vk_space) && instance_exists(ball)) { + with (ball) phys_launch(irandom(359), 12); +} +if (keyboard_check_pressed(vk_enter) && instance_exists(ball)) { + with (ball) phys_pin(480, 130); +} diff --git a/prefabs/physics_body/objects/obj_demo/object.toml b/prefabs/physics_body/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/physics_body/objects/obj_demo_ball/Create.gml b/prefabs/physics_body/objects/obj_demo_ball/Create.gml new file mode 100644 index 00000000..accafada --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo_ball/Create.gml @@ -0,0 +1,5 @@ +// Continuous collision first; the speed cap is the backstop behind it. +phy_bullet = true; +body = phys_make(14); +squash = 0; +hits = 0; diff --git a/prefabs/physics_body/objects/obj_demo_ball/Draw.gml b/prefabs/physics_body/objects/obj_demo_ball/Draw.gml new file mode 100644 index 00000000..69487de2 --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo_ball/Draw.gml @@ -0,0 +1,5 @@ +// Squash along travel on impact — the visible half of `phys_impact`. +var _s = 1 + squash * 0.5; +draw_sprite_ext(sprite_index, 0, x, y, _s, 2 - _s, + point_direction(0, 0, phy_speed_x, phy_speed_y), + c_white, 1); diff --git a/prefabs/physics_body/objects/obj_demo_ball/Step.gml b/prefabs/physics_body/objects/obj_demo_ball/Step.gml new file mode 100644 index 00000000..bb74f73e --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo_ball/Step.gml @@ -0,0 +1,23 @@ +var _ctl = instance_find(obj_demo, 0); +if (_ctl != noone && _ctl.paused) exit; + +// Impact first, before anything else touches the velocity. +// Threshold well under the cap, or it can never fire — the two are +// the same scale on purpose. +var _hit = phys_impact(body, 3); +if (_hit > 0) { + squash = min(1, _hit / 14); + hits += 1; +} +squash *= 0.86; + +phys_clamp_speed(body); + +// A ball at rest somewhere unreachable ends no run, so nudge it. +if (phys_stalled(body, 1.5, 1.2)) phys_nudge(irandom(359), 0.35); + +// Escaping through a seam happens; put it back rather than losing it. +if (phys_out_of_bounds(80)) { + phys_pin(room_width / 2, room_height / 2); + phys_launch(irandom(359), 12); +} diff --git a/prefabs/physics_body/objects/obj_demo_ball/object.toml b/prefabs/physics_body/objects/obj_demo_ball/object.toml new file mode 100644 index 00000000..d2285c3e --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo_ball/object.toml @@ -0,0 +1,21 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_ball" + +[physics] +kind = "dynamic" + +[physics.body] +density = 0.5 +restitution = 0.85 +friction = 0.05 +linear_damping = 0.1 +angular_damping = 0.4 + +# Final pixels, deliberately not `scale = "instance"` — that bakes the +# fixture at instance-creation scale, which runs before Create.gml can set +# `image_xscale`, and the hitbox stops matching the art. +[physics.body.shape] +type = "circle" +centre = [16.0, 16.0] +radius = 16.0 diff --git a/prefabs/physics_body/objects/obj_demo_wall/object.toml b/prefabs/physics_body/objects/obj_demo_wall/object.toml new file mode 100644 index 00000000..c409345f --- /dev/null +++ b/prefabs/physics_body/objects/obj_demo_wall/object.toml @@ -0,0 +1,16 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_wall" + +[physics] +kind = "static" + +[physics.body] +density = 0.0 +restitution = 0.6 +friction = 0.2 + +[physics.body.shape] +type = "aabb" +origin = [0.0, 0.0] +size = [40.0, 40.0] diff --git a/prefabs/physics_body/prefab.toml b/prefabs/physics_body/prefab.toml new file mode 100644 index 00000000..0cf058e8 --- /dev/null +++ b/prefabs/physics_body/prefab.toml @@ -0,0 +1,18 @@ +schema_version = 1 +name = "Physics body hygiene" + +# A library: installed under `prefabs//` in the target project and +# called as `::physics_body::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "The things a Box2D body needs that an instance does not. A speed ceiling behind phy_bullet, because a body that gains energy off bouncy surfaces eventually outruns continuous collision and passes through a wall. Impact strength from a velocity reversal, which fires once per bounce with a magnitude rather than once per frame of contact with none. Stall detection, because a ball that comes to rest out of reach ends no run. Pinning, because writing x does nothing to a physics body. And a freeze that stops the body, since gating your own Step leaves the world integrating underneath it." +category = "library" +tags = ["capability:physics", "capability:box2d", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_ball", "objects/obj_demo_wall", "rooms/rm_demo", "sprites/spr_demo_ball", "sprites/spr_demo_wall"] + +steps = [ + "The room needs `physics_world = true` in its `room.toml`, or none of the bodies simulate at all.", + "Set `phy_bullet = true` in Create on anything fast. `phys_clamp_speed` is the backstop behind it, not a replacement for it.", + "Declare fixtures in `[physics.body.shape]` at final pixel size. `scale = \"instance\"` bakes the fixture at instance-creation scale, which runs before Create.gml sets `image_xscale` — that mismatch is how a 16px sprite ends up with a 74px hitbox.", +] diff --git a/prefabs/physics_body/project.toml b/prefabs/physics_body/project.toml new file mode 100644 index 00000000..0cbed426 --- /dev/null +++ b/prefabs/physics_body/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "physics-body" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/physics_body/rooms/rm_demo/layers/Background.toml b/prefabs/physics_body/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/physics_body/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/physics_body/rooms/rm_demo/layers/Instances.toml b/prefabs/physics_body/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/physics_body/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/physics_body/rooms/rm_demo/room.toml b/prefabs/physics_body/rooms/rm_demo/room.toml new file mode 100644 index 00000000..aa9802f1 --- /dev/null +++ b/prefabs/physics_body/rooms/rm_demo/room.toml @@ -0,0 +1,8 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } + +physics_world = true +physics_gravity_x = 0.0 +physics_gravity_y = 0.0 diff --git a/prefabs/physics_body/scripts/scr_physics_body/scr_physics_body.gml b/prefabs/physics_body/scripts/scr_physics_body/scr_physics_body.gml new file mode 100644 index 00000000..55a7ea1e --- /dev/null +++ b/prefabs/physics_body/scripts/scr_physics_body/scr_physics_body.gml @@ -0,0 +1,78 @@ +/// Keeping a Box2D body honest. +/// +/// Every one of these is a bug found in a shipped jam game rather than a +/// hypothetical. A physics body does not behave like an instance you move: +/// writing `x` does nothing, resting bodies fall asleep, fast ones pass +/// through walls, and a "pause" that only skips your Step leaves the world +/// simulating underneath it. + +/// @function phys_make(speed_cap) +/// @description Bookkeeping for one body: the speed ceiling, and the state +/// impact and stall detection need between frames. Call in Create. +function phys_make(speed_cap) { + return { + cap: speed_cap, + prev_vx: 0, + prev_vy: 0, + stalled: 0, + frozen: false, + held_vx: 0, + held_vy: 0, + }; +} + +/// @function phys_speed() +/// @scope instance +/// @description How fast the calling body is going, in **pixels per step**. +/// +/// GameMaker offers the same velocity twice: `phy_speed_*` in pixels per +/// step and `phy_linear_velocity_*` in pixels per second, a factor of +/// `room_speed` apart. Measured on the runner, setting a linear velocity of +/// 30 gives a `phy_speed` of 0.499 and moves the body 14.7px in 30 frames. +/// +/// Everything here is per step, because every other speed in a GameMaker +/// game is — `motion`'s run speed, a bullet's travel, an enemy's chase. +/// A physics library quoting seconds while the rest of the catalog quotes +/// steps is how a launch speed of 22 ends up moving a ball a third of a +/// pixel a frame. +function phys_speed() { + return point_distance(0, 0, phy_speed_x, phy_speed_y); +} + +/// @function phys_clamp_speed(state) +/// @scope instance +/// @description Hold the body under its cap, keeping direction. Returns the +/// resulting speed. +/// +/// `phy_bullet = true` is the first defence against a fast body passing +/// through a thin wall, and it is not sufficient on its own — a ball that +/// keeps gaining energy off bouncy surfaces eventually outruns it. This is +/// the backstop. +function phys_clamp_speed(state) { + var _spd = phys_speed(); + if (_spd <= state.cap || _spd <= 0) return _spd; + var _k = state.cap / _spd; + phy_speed_x *= _k; + phy_speed_y *= _k; + return state.cap; +} + +/// @function phys_nudge(direction, force) +/// @scope instance +/// @description Shove the body along a direction — what a stall recovery, +/// a bumper or a launcher does. +function phys_nudge(direction, force) { + physics_apply_impulse(x, y, + lengthdir_x(force, direction), lengthdir_y(force, direction)); +} + +/// @function phys_launch(direction, speed) +/// @scope instance +/// @description Set velocity outright rather than adding to it, so a serve +/// is the same however the body was moving beforehand. An impulse on a body +/// already travelling gives a different result each time. +function phys_launch(direction, speed) { + phy_speed_x = lengthdir_x(speed, direction); + phy_speed_y = lengthdir_y(speed, direction); +} + diff --git a/prefabs/physics_body/scripts/scr_physics_body_freeze/scr_physics_body_freeze.gml b/prefabs/physics_body/scripts/scr_physics_body_freeze/scr_physics_body_freeze.gml new file mode 100644 index 00000000..448bb5bd --- /dev/null +++ b/prefabs/physics_body/scripts/scr_physics_body_freeze/scr_physics_body_freeze.gml @@ -0,0 +1,40 @@ +/// @function phys_freeze(state) +/// @scope instance +/// @description Stop the body and remember its velocity. Gating your own +/// Step is not a pause: the physics world keeps integrating, so a "paused" +/// body keeps drifting and colliding. +function phys_freeze(state) { + if (state.frozen) return; + state.held_vx = phy_speed_x; + state.held_vy = phy_speed_y; + phy_speed_x = 0; + phy_speed_y = 0; + phy_angular_velocity = 0; + state.frozen = true; +} + +/// @function phys_thaw(state) +/// @scope instance +/// @description Give the velocity back, so a pause does not cost momentum. +function phys_thaw(state) { + if (!state.frozen) return; + phy_speed_x = state.held_vx; + phy_speed_y = state.held_vy; + state.frozen = false; +} + +/// @function phys_pin(px, py) +/// @scope instance +/// @description Hold the body at a point — carried, docked, held before a +/// serve. Writing `x` and `y` does nothing to a physics body: the world +/// overwrites them from its own position every step. Teleport the body and +/// zero the velocity, every frame, or it drifts away by whatever it +/// accumulated in between. +function phys_pin(px, py) { + phy_position_x = px; + phy_position_y = py; + phy_speed_x = 0; + phy_speed_y = 0; + phy_angular_velocity = 0; +} + diff --git a/prefabs/physics_body/scripts/scr_physics_body_impact/scr_physics_body_impact.gml b/prefabs/physics_body/scripts/scr_physics_body_impact/scr_physics_body_impact.gml new file mode 100644 index 00000000..f0dd1b9b --- /dev/null +++ b/prefabs/physics_body/scripts/scr_physics_body_impact/scr_physics_body_impact.gml @@ -0,0 +1,54 @@ +/// @function phys_impact(state, threshold) +/// @scope instance +/// @description How hard the body just hit something, or 0 for no impact. +/// +/// Measured as how much the velocity *changed* this frame. That beats a +/// collision event for feedback, because it fires once per bounce carrying +/// a magnitude rather than once per frame of contact carrying none. +/// +/// It is a delta rather than a direction reversal deliberately. A reversal +/// test — this frame's velocity pointing against last frame's — misses the +/// common case: a body travelling at 45° that bounces off one axis has +/// exactly reversed one component and kept the other, and the two cancel to +/// a dot product of zero. Only a near head-on hit registers, which is why a +/// game built that way feels like it only sometimes notices walls. +/// +/// Call once a frame, before anything else changes the velocity. +function phys_impact(state, threshold) { + var _vx = phy_speed_x; + var _vy = phy_speed_y; + var _change = point_distance(state.prev_vx, state.prev_vy, _vx, _vy); + state.prev_vx = _vx; + state.prev_vy = _vy; + return _change >= threshold ? _change : 0; +} + +/// @function phys_stalled(state, min_speed, seconds) +/// @scope instance +/// @description True once the body has been slower than `min_speed` for +/// `seconds` without interruption, and resets when it is. +/// +/// A physics game needs this because a ball can come to rest somewhere the +/// player cannot reach it, and the run then never ends — the one failure a +/// playtest finds and a test suite does not. +function phys_stalled(state, min_speed, seconds) { + if (phys_speed() >= min_speed) { + state.stalled = 0; + return false; + } + state.stalled += delta_time / 1000000; + if (state.stalled < seconds) return false; + state.stalled = 0; + return true; +} + +/// @function phys_out_of_bounds(margin) +/// @scope instance +/// @description Whether the body has left the room by `margin`. Bodies do +/// escape — through a seam between two walls, or at a speed the solver +/// could not resolve — and one that has left is gone for good unless +/// something notices. +function phys_out_of_bounds(margin) { + return x < -margin || y < -margin + || x > room_width + margin || y > room_height + margin; +} diff --git a/prefabs/physics_body/sprites/spr_demo_ball/frame_000.png b/prefabs/physics_body/sprites/spr_demo_ball/frame_000.png new file mode 100644 index 00000000..50fb7ee8 Binary files /dev/null and b/prefabs/physics_body/sprites/spr_demo_ball/frame_000.png differ diff --git a/prefabs/physics_body/sprites/spr_demo_ball/sprite.toml b/prefabs/physics_body/sprites/spr_demo_ball/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_body/sprites/spr_demo_ball/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_body/sprites/spr_demo_wall/frame_000.png b/prefabs/physics_body/sprites/spr_demo_wall/frame_000.png new file mode 100644 index 00000000..375bfdb7 Binary files /dev/null and b/prefabs/physics_body/sprites/spr_demo_wall/frame_000.png differ diff --git a/prefabs/physics_body/sprites/spr_demo_wall/sprite.toml b/prefabs/physics_body/sprites/spr_demo_wall/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/physics_body/sprites/spr_demo_wall/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/physics_body/thumbnail.png b/prefabs/physics_body/thumbnail.png new file mode 100644 index 00000000..ec4ed8fd Binary files /dev/null and b/prefabs/physics_body/thumbnail.png differ diff --git a/prefabs/pickup/LICENSE b/prefabs/pickup/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/pickup/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/pickup/README.md b/prefabs/pickup/README.md new file mode 100644 index 00000000..08bc8d82 --- /dev/null +++ b/prefabs/pickup/README.md @@ -0,0 +1,3 @@ +# pickup + +Coins on the floor, with a magnet so near misses still count. diff --git a/prefabs/pickup/objects/obj_demo/Create.gml b/prefabs/pickup/objects/obj_demo/Create.gml new file mode 100644 index 00000000..bf1d4f13 --- /dev/null +++ b/prefabs/pickup/objects/obj_demo/Create.gml @@ -0,0 +1,4 @@ +taken = 0; +pickup_scatter(obj_demo_coin, 24, 480, 270, 320); +x = 480; +y = 400; diff --git a/prefabs/pickup/objects/obj_demo/Draw.gml b/prefabs/pickup/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..d22003aa --- /dev/null +++ b/prefabs/pickup/objects/obj_demo/Draw.gml @@ -0,0 +1 @@ +draw_circle(x, y, 12, true); diff --git a/prefabs/pickup/objects/obj_demo/Draw_GUI.gml b/prefabs/pickup/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..3b45863c --- /dev/null +++ b/prefabs/pickup/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,3 @@ +draw_text(40, 30, "pickup demo — move the mouse to collect, SPACE to scatter again"); +draw_text(40, 50, "taken " + string(taken) + + " left " + string(pickup_remaining(obj_demo_coin))); diff --git a/prefabs/pickup/objects/obj_demo/Step.gml b/prefabs/pickup/objects/obj_demo/Step.gml new file mode 100644 index 00000000..85171524 --- /dev/null +++ b/prefabs/pickup/objects/obj_demo/Step.gml @@ -0,0 +1,9 @@ +// The collector follows the mouse, so the magnet is easy to feel: coins +// start moving before you reach them. +x += (mouse_x - x) * 0.2; +y += (mouse_y - y) * 0.2; + +if (keyboard_check_pressed(vk_space)) { + taken = 0; + pickup_scatter(obj_demo_coin, 24, 480, 270, 320); +} diff --git a/prefabs/pickup/objects/obj_demo/object.toml b/prefabs/pickup/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/pickup/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/pickup/objects/obj_demo_coin/Create.gml b/prefabs/pickup/objects/obj_demo_coin/Create.gml new file mode 100644 index 00000000..8d29c054 --- /dev/null +++ b/prefabs/pickup/objects/obj_demo_coin/Create.gml @@ -0,0 +1 @@ +coin = pickup_make(14, 120, 3); diff --git a/prefabs/pickup/objects/obj_demo_coin/Draw.gml b/prefabs/pickup/objects/obj_demo_coin/Draw.gml new file mode 100644 index 00000000..4bebf6d1 --- /dev/null +++ b/prefabs/pickup/objects/obj_demo_coin/Draw.gml @@ -0,0 +1,3 @@ +draw_set_colour(c_yellow); +draw_circle(x, y + pickup_bob(4, 4), 7, false); +draw_set_colour(c_white); diff --git a/prefabs/pickup/objects/obj_demo_coin/Step.gml b/prefabs/pickup/objects/obj_demo_coin/Step.gml new file mode 100644 index 00000000..d45b68cd --- /dev/null +++ b/prefabs/pickup/objects/obj_demo_coin/Step.gml @@ -0,0 +1,5 @@ +var _who = pickup_step(coin, obj_demo); +if (_who != noone) { + _who.taken += 1; + instance_destroy(); +} diff --git a/prefabs/pickup/objects/obj_demo_coin/object.toml b/prefabs/pickup/objects/obj_demo_coin/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/pickup/objects/obj_demo_coin/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/pickup/prefab.toml b/prefabs/pickup/prefab.toml new file mode 100644 index 00000000..e9c63eb4 --- /dev/null +++ b/prefabs/pickup/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Collectables" + +# A library: installed under `prefabs//` in the target project and +# called as `::pickup::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Collection by distance rather than collision mask, so a pickup can be drawn rather than sprited. A magnet radius pulls one in faster the closer it gets, a bob offset keeps it looking loose on the floor without moving the instance out of range, and scatter drops a handful around a point — what a destroyed enemy leaves behind." +category = "library" +tags = ["capability:pickups", "capability:collectables", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_coin", "rooms/rm_demo"] + +steps = [ + "`pickup_step` returns the collector and leaves the instance alive, so score it, play a sound, then `instance_destroy()` — in that order.", +] diff --git a/prefabs/pickup/project.toml b/prefabs/pickup/project.toml new file mode 100644 index 00000000..7246d230 --- /dev/null +++ b/prefabs/pickup/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "pickup" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/pickup/rooms/rm_demo/layers/Background.toml b/prefabs/pickup/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/pickup/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/pickup/rooms/rm_demo/layers/Instances.toml b/prefabs/pickup/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/pickup/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/pickup/rooms/rm_demo/room.toml b/prefabs/pickup/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/pickup/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/pickup/scripts/scr_pickup/scr_pickup.gml b/prefabs/pickup/scripts/scr_pickup/scr_pickup.gml new file mode 100644 index 00000000..3709bf19 --- /dev/null +++ b/prefabs/pickup/scripts/scr_pickup/scr_pickup.gml @@ -0,0 +1,53 @@ +/// Things lying on the floor that the player walks into. +/// +/// Distance from the collector rather than a collision mask, so a pickup can +/// be drawn rather than sprited, and so the magnet radius and the grab radius +/// are the same idea measured twice. + +/// @function pickup_make(grab, magnet, pull) +/// @description Pickup state: collected within `grab` pixels, drawn towards +/// the collector from `magnet` pixels at `pull` pixels a frame. Pass a magnet +/// of 0 for one that stays put. +function pickup_make(grab, magnet, pull) { + return { grab: grab, magnet: magnet, pull: pull, taken: false }; +} + +/// @function pickup_step(state, obj_collector) +/// @scope instance +/// @description Run from the pickup's own Step. Drifts towards the nearest +/// collector once inside the magnet radius and returns the collector that +/// took it, or `noone`. Does not destroy the instance — scoring, a sound and +/// a popup usually want to happen first. +function pickup_step(state, obj_collector) { + if (state.taken) return noone; + var _who = instance_nearest(x, y, obj_collector); + if (_who == noone) return noone; + + var _dist = point_distance(x, y, _who.x, _who.y); + if (_dist <= state.grab) { + state.taken = true; + return _who; + } + if (state.magnet > state.grab && _dist <= state.magnet) { + // Faster the closer it gets, so a near miss still resolves quickly + // instead of trailing the player at a fixed distance forever. + // A floor under the pull, so a pickup at the very edge of the + // magnet still closes instead of trailing the player forever. + var _floor = state.pull * 0.35; + var _speed = state.pull * (1 - (_dist - state.grab) + / max(1, state.magnet - state.grab)) + _floor; + var _dir = point_direction(x, y, _who.x, _who.y); + x += lengthdir_x(_speed, _dir); + y += lengthdir_y(_speed, _dir); + } + return noone; +} + +/// @function pickup_bob(height, speed) +/// @description A vertical offset to add at draw time, so a pickup reads as +/// loose on the floor rather than painted onto it. Purely cosmetic — it does +/// not move the instance, so it cannot drift it out of grab range. +function pickup_bob(height, speed) { + return sin(current_time / 1000 * speed) * height; +} + diff --git a/prefabs/pickup/scripts/scr_pickup_scatter/scr_pickup_scatter.gml b/prefabs/pickup/scripts/scr_pickup_scatter/scr_pickup_scatter.gml new file mode 100644 index 00000000..d32115a8 --- /dev/null +++ b/prefabs/pickup/scripts/scr_pickup_scatter/scr_pickup_scatter.gml @@ -0,0 +1,21 @@ +/// @function pickup_scatter(obj, count, from_x, from_y, spread) +/// @description Drop `count` of `obj` around a point, jittered within +/// `spread` and kept inside the room. What a destroyed enemy leaves behind. +function pickup_scatter(obj, count, from_x, from_y, spread) { + var _made = []; + for (var i = 0; i < count; i++) { + var _dir = irandom(359); + var _len = random(spread); + var _px = clamp(from_x + lengthdir_x(_len, _dir), 8, room_width - 8); + var _py = clamp(from_y + lengthdir_y(_len, _dir), 8, room_height - 8); + array_push(_made, instance_create_depth(_px, _py, 0, obj)); + } + return _made; +} + +/// @function pickup_remaining(obj) +/// @description How many are still uncollected — the number a "collect them +/// all" objective is checking. +function pickup_remaining(obj) { + return instance_number(obj); +} diff --git a/prefabs/pickup/thumbnail.png b/prefabs/pickup/thumbnail.png new file mode 100644 index 00000000..59d14e41 Binary files /dev/null and b/prefabs/pickup/thumbnail.png differ diff --git a/prefabs/platformer/LICENSE b/prefabs/platformer/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/platformer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/platformer/README.md b/prefabs/platformer/README.md new file mode 100644 index 00000000..4a6c47c2 --- /dev/null +++ b/prefabs/platformer/README.md @@ -0,0 +1,69 @@ +# platformer + +Run, jump, collect, reach the flag — a whole side-view game to start from. + +## The level is the tile layer + +The terrain lives in `rooms/rm_level/layers/Tiles.toml`, and that is the only +copy of it. There is no map file beside it to keep in step — a second copy of +the shape is a thing that can disagree with the tiles, and the way to not have +that problem is to not have the second copy. + +Read the shape back out of the layer: + +```console +$ gmx autotile shape rm_level +14 rows ↓ × 90 cols → — ops address row,col, zero-based + 0 10 20 30 + 01234567890123456789012345678901 + 6 .........####............. + 7 ......................###. + 9 ########........####...... +``` + +Edit it with the same command, which redraws the tiles and the legend in the +same run: + +``` +gmx autotile shape rm_level --op 'rect 6,20 6,26 fill' +``` + +The tile index is never yours to pick: each filled cell gets the tile that fits +its neighbours, out of the `[autotile]` table in +`tilesets/ts_terrain/tileset.toml`. That is why a platform's ends are drawn +differently from its middle without you saying so, and why widening one redraws +its cap and adds the masks it now needs. A shape the sheet cannot draw is +refused with the offending cells named, rather than written and left to look +wrong — `--repair` adjusts the shape until it can be drawn, and prints the diff +before changing anything. + +The actors — player, coins, enemies, goal — are placed instances in the same +room. Drag them in the editor, or edit +`rooms/rm_level/layers/Instances.toml`. + +## Collision is against the tile layer + +`::motion::` resolves one axis at a time against `Tiles`. Both halves matter: +moving both axes at once before backing out of a wall is what produces a +character who sticks to ceilings, and tile index 0 is always empty, so the +sheet's first row is left blank and the 47-tile template starts on its second. + +## The jump is three numbers and two forgivenesses + +`jump_strength`, `gravity_per_frame` and `fall_max` set the arc. The two that +decide whether it *feels* right are in `::motion::` rather than here: + +- **Coyote time** — jumping still works for a few frames after walking off a + ledge. Without it, a player who presses on the exact frame they leave the + ground gets nothing and blames the controls. +- **Input buffering** — a press slightly before landing is remembered and fires + on touchdown. + +Variable height comes from halving upward velocity when the key is released, so +a tap is always a small hop at any frame rate. + +## What it does not do + +No moving platforms, no checkpoints, one level. Falling out of the world +restarts the room. Those are the next things to add, and they are deliberately +absent — this is a floor to build on, not a game to ship. diff --git a/prefabs/platformer/demo.gametest.json b/prefabs/platformer/demo.gametest.json new file mode 100644 index 00000000..571eb93f --- /dev/null +++ b/prefabs/platformer/demo.gametest.json @@ -0,0 +1,112 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 190, + "stop_on_error": true, + "chunks": { + "enter": { + "gml": "room_goto(rm_level);" + }, + "init": { + "gml": "global.gt_seen=false; global.gt_ground=false; global.gt_x=0; global.gt_y=0; global.gt_enemies = instance_number(obj_enemy); global.gt_min = global.gt_enemies;" + }, + "watch": { + "gml": "if (instance_exists(obj_player)) { global.gt_seen = true; if (obj_player.jump.on_ground) global.gt_ground = true; global.gt_y = max(global.gt_y, obj_player.y); global.gt_x = max(global.gt_x, obj_player.x); } global.gt_min = min(global.gt_min, instance_number(obj_enemy));" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "grounded": { + "gml": "return global.gt_ground;" + }, + "in_world": { + "gml": "return global.gt_y < room_height;" + }, + "moved_right": { + "gml": "return global.gt_x > 112;" + }, + "patrols_kept": { + "gml": "return global.gt_min >= global.gt_enemies;" + } + }, + "timeline": [ + { + "frame": 30, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 140, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 180, + "assert": { + "id": "seen", + "name": "the player exists", + "expect": true + } + }, + { + "frame": 180, + "assert": { + "id": "grounded", + "name": "and stands on tile terrain", + "expect": true + } + }, + { + "frame": 180, + "assert": { + "id": "in_world", + "name": "the player stands on the level rather than falling through it", + "expect": true + } + }, + { + "frame": 180, + "assert": { + "id": "moved_right", + "name": "and can walk along it", + "expect": true + } + }, + { + "frame": 180, + "assert": { + "id": "patrols_kept", + "name": "and every patrol turns at its ledge rather than walking off", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 1, + "until": 178, + "code": [ + "watch" + ] + }, + { + "frame": 2, + "code": [ + "enter" + ] + } + ] +} diff --git a/prefabs/platformer/objects/obj_coin/object.toml b/prefabs/platformer/objects/obj_coin/object.toml new file mode 100644 index 00000000..96e96a4d --- /dev/null +++ b/prefabs/platformer/objects/obj_coin/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_coin" diff --git a/prefabs/platformer/objects/obj_enemy/Create.gml b/prefabs/platformer/objects/obj_enemy/Create.gml new file mode 100644 index 00000000..442e0766 --- /dev/null +++ b/prefabs/platformer/objects/obj_enemy/Create.gml @@ -0,0 +1,28 @@ +var _t = platformer_tuning(); +walk = ::motion::motion_platformer_make({ + run_speed: _t.enemy_speed, + jump_strength: _t.jump_strength, + gravity_per_frame: _t.gravity_per_frame, + tilemap: ::motion::motion_tilemap("Tiles") +}); + +// -1 or 1, not a GameMaker `direction` in degrees. `direction` is a built-in +// that drives the engine's own motion, and borrowing it for a sign is how a +// patrol ends up moving in a way nothing in this file explains. +facing = 1; + +// Settle onto the ground under wherever it was put. +// +// A patrol placed in open air — a coordinate someone guessed, a block deleted +// from under it — otherwise falls straight out of the level and is gone, with +// nothing on screen and nothing in a log to say so. Landing it on the first +// solid below turns that mistake into a working enemy in the obvious place. +// An enemy already standing on a block does not move at all. +while (y < room_height && !::motion::motion_blocked(noone, walk.tilemap, x, y + 1)) { + y += 1; +} + +// Where it goes back to if it ever ends up below the level anyway — over a +// pit, say, where the loop above found nothing to stand on. +start_x = x; +start_y = y; diff --git a/prefabs/platformer/objects/obj_enemy/Step.gml b/prefabs/platformer/objects/obj_enemy/Step.gml new file mode 100644 index 00000000..4ade992d --- /dev/null +++ b/prefabs/platformer/objects/obj_enemy/Step.gml @@ -0,0 +1,24 @@ +// Turn at a wall, and at the edge of the platform — without the second test +// a patrol walks off the ledge it was placed on and the level empties itself +// within a few seconds. +var _cell = platformer_tuning().cell; +var _ahead_x = x + facing * _cell * 0.6; +var _wall = ::motion::motion_blocked(noone, walk.tilemap, x + facing * walk.run_speed, y); +// A point under the foot ahead, not the whole body: the mask is wider than +// the gap it is meant to notice. +var _floor_ahead = ::motion::motion_solid_at( + noone, walk.tilemap, _ahead_x, bbox_bottom + _cell * 0.5); +if (_wall || (walk.on_ground && !_floor_ahead)) facing = -facing; + +::motion::motion_platformer_step(walk, noone, facing, false, false); + +// Put a fallen patrol back where it started rather than letting it drop +// forever. The ledge test above keeps a *placed* enemy on its platform, but +// one dropped into open air — an enemy added at a guessed coordinate, or a +// block deleted out from under it — otherwise falls out of the level and is +// gone, with nothing on screen and nothing in any log to say so. +if (y > room_height + _cell * 4) { + x = start_x; + y = start_y; + walk.vy = 0; +} diff --git a/prefabs/platformer/objects/obj_enemy/object.toml b/prefabs/platformer/objects/obj_enemy/object.toml new file mode 100644 index 00000000..59dd107e --- /dev/null +++ b/prefabs/platformer/objects/obj_enemy/object.toml @@ -0,0 +1,6 @@ +#:schema ../../.gm-schema/v1.json#object + +# Same split as the player: `sprite` is swappable art, `mask` is the patrol's +# body. See obj_player. +sprite = "spr_enemy" +mask = "spr_enemy" diff --git a/prefabs/platformer/objects/obj_goal/object.toml b/prefabs/platformer/objects/obj_goal/object.toml new file mode 100644 index 00000000..6aaf4e3c --- /dev/null +++ b/prefabs/platformer/objects/obj_goal/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_goal" diff --git a/prefabs/platformer/objects/obj_level/Create.gml b/prefabs/platformer/objects/obj_level/Create.gml new file mode 100644 index 00000000..d8eb58a2 --- /dev/null +++ b/prefabs/platformer/objects/obj_level/Create.gml @@ -0,0 +1,9 @@ +// The kernel owns run state, score and save. It has to be booted before +// anything reads them — obj_kernel's own Step does, on frame one. +::kernel::kernel_boot(); + +// The level is placed in rm_level, so a block can be dragged in an editor. +// The terrain is the room's `Tiles` layer and is the only copy of the shape — +// read and edit it with `gmx autotile shape rm_level`. +global.coins_total = 0; +global.coins_taken = 0; diff --git a/prefabs/platformer/objects/obj_level/Other_RoomStart.gml b/prefabs/platformer/objects/obj_level/Other_RoomStart.gml new file mode 100644 index 00000000..fccb70ef --- /dev/null +++ b/prefabs/platformer/objects/obj_level/Other_RoomStart.gml @@ -0,0 +1,3 @@ +// Counted here rather than in Create: Create runs as each instance is made, +// so a controller placed first in the layer would count a half-built room. +global.coins_total = instance_number(obj_coin); diff --git a/prefabs/platformer/objects/obj_level/object.toml b/prefabs/platformer/objects/obj_level/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/platformer/objects/obj_level/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/platformer/objects/obj_player/Create.gml b/prefabs/platformer/objects/obj_player/Create.gml new file mode 100644 index 00000000..2720af43 --- /dev/null +++ b/prefabs/platformer/objects/obj_player/Create.gml @@ -0,0 +1,13 @@ +var _t = platformer_tuning(); +jump = ::motion::motion_platformer_make({ + run_speed: _t.run_speed, + jump_strength: _t.jump_strength, + gravity_per_frame: _t.gravity_per_frame, + // Terrain is the `Tiles` layer, not one instance per cell: it is a layer + // an editor can paint, and it is what the level map compiles into. + tilemap: ::motion::motion_tilemap("Tiles") +}); +cam = ::camera::camera_follow_make(camera_get_view_width(view_camera[0]), + camera_get_view_height(view_camera[0])); +::camera::camera_follow_snap(cam, x, y); +won = false; diff --git a/prefabs/platformer/objects/obj_player/Draw_GUI.gml b/prefabs/platformer/objects/obj_player/Draw_GUI.gml new file mode 100644 index 00000000..3ced64a9 --- /dev/null +++ b/prefabs/platformer/objects/obj_player/Draw_GUI.gml @@ -0,0 +1,4 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, + "coins " + string(global.coins_taken) + "/" + string(global.coins_total)); +if (won) ::kernel::kernel_draw_text(16, 72, "you made it — press R to run it again"); diff --git a/prefabs/platformer/objects/obj_player/Step.gml b/prefabs/platformer/objects/obj_player/Step.gml new file mode 100644 index 00000000..62547292 --- /dev/null +++ b/prefabs/platformer/objects/obj_player/Step.gml @@ -0,0 +1,42 @@ +if (won) exit; + +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +::motion::motion_platformer_step( + jump, noone, _dx, + keyboard_check(vk_space) || keyboard_check(vk_up), + keyboard_check_pressed(vk_space) || keyboard_check_pressed(vk_up) +); + +// Falling out of the world is a loss, not a soft-lock. +if (y > room_height + 200) { + ::feel::feel_shake(0.25, 6); + room_restart(); +} + +// Touching a patrol costs the run. Checked before the pickups so a frame +// that is both a coin and a death does not award the coin first. +if (instance_place(x, y, obj_enemy) != noone) { + ::feel::feel_shake(0.3, 8); + room_restart(); + exit; +} + +var _coin = instance_place(x, y, obj_coin); +if (_coin != noone) { + var _t = platformer_tuning(); + global.coins_taken += 1; + ::kernel::kernel_score_add(_t.coin_score); + // GUI space, like the rest of the HUD — a world-space popup would + // scroll away with the camera. + ::feel::feel_pop(display_get_gui_width() / 2, 96, "+" + string(_t.coin_score)); + instance_destroy(_coin); +} + +if (instance_place(x, y, obj_goal) != noone) { + won = true; + ::feel::feel_hitstop(0.12); + ::kernel::kernel_game_over(true); +} + +var _p = ::camera::camera_follow_step(cam, x, y); +camera_set_view_pos(view_camera[0], _p.x, _p.y); diff --git a/prefabs/platformer/objects/obj_player/Step_End.gml b/prefabs/platformer/objects/obj_player/Step_End.gml new file mode 100644 index 00000000..670a7a47 --- /dev/null +++ b/prefabs/platformer/objects/obj_player/Step_End.gml @@ -0,0 +1 @@ +if (won && keyboard_check_pressed(ord("R"))) room_restart(); diff --git a/prefabs/platformer/objects/obj_player/object.toml b/prefabs/platformer/objects/obj_player/object.toml new file mode 100644 index 00000000..58bd5866 --- /dev/null +++ b/prefabs/platformer/objects/obj_player/object.toml @@ -0,0 +1,8 @@ +#:schema ../../.gm-schema/v1.json#object + +# `sprite` is the picture and `mask` is the hitbox, deliberately named apart +# even though they are the same sprite today. Swapping in real art is then one +# line: point `sprite` at the catalog pose and the tuned hitbox stays where the +# movement was tuned against it, however tall the art turns out to be. +sprite = "spr_player" +mask = "spr_player" diff --git a/prefabs/platformer/prefab.toml b/prefabs/platformer/prefab.toml new file mode 100644 index 00000000..2228c3f3 --- /dev/null +++ b/prefabs/platformer/prefab.toml @@ -0,0 +1,65 @@ +schema_version = 1 +name = "Platformer" +description = "Run, jump, collect, reach the flag — a whole side-view game to start from." +category = "game" +tags = ["genre:platformer", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "motion", "camera"] + +# The terrain lives in the layer, and `gmx autotile shape` is how it is +# read and edited, so this is the seam far more often than any GML file. +seam = "rooms/rm_level/layers/Tiles.toml" + +steps = [ + "`platformer.gametest.json` came with the recipe and already passes: the player exists, stands on the tile terrain without falling through, walks along it, and every patrol turns at its ledge. Run it with `gmx test platformer.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The terrain is the `Tiles` layer of rm_level, and it is the only copy of it. `gmx autotile shape rm_level` prints it back as a block of `#` and `.` with the axes numbered, and edits go through the same command: `gmx autotile shape rm_level --op 'rect 6,20 6,26 fill'` widens a platform and redraws the tiles and the legend in the same run. There is no map file to keep in step, which is the point — a second copy of the shape is a thing that can disagree with the tiles, and this one cannot. You never pick tile indices: each filled cell gets the tile that fits its neighbours from `ts_terrain`'s `[autotile]` table, which is why a platform's ends look different from its middle and why widening one redraws its cap. A shape the sheet cannot draw is refused with the offending cells named, rather than written and left to look wrong.", + "The actors are placed instances in the same room, so drag them in the editor or edit `rooms/rm_level/layers/Instances.toml`. They are placed at their own size rather than stretched to the cell: each placeholder is drawn at the height its `[art]` slot asks for and doubles as the `mask`, so swapping in real art at the same height leaves the hitbox alone.", + "The placeholder art is flat colour, and swapping it is data, not code. The `gmx prefab add ... --fit-height` lines printed above carry the size each slot wants; use those numbers rather than picking your own, and add a character's poses in one call so they share a factor. Then point `sprite` at the art in `object.toml`. `obj_player` and `obj_enemy` already carry `mask = \"spr_player\"` / `\"spr_enemy\"`, so the placeholder keeps being the hitbox the movement was tuned against and poses animate through `sprite_index` without moving it. Nothing scales at runtime, so the room file keeps saying what the game draws.", + "Terrain art is the tileset's sheet, and `ts_terrain` reads it as a 47-tile auto-tile template: an 8-wide grid whose first row is blank (tile index 0 is GameMaker's empty tile) and whose next six rows are the template. Replacing the art means replacing `sprites/spr_terrain_sheet/frame_000.png` with a sheet in that layout and re-running `gmx autotile derive ts_terrain --template 47 --origin 1,0 --force`, then `gmx autotile sample ts_terrain --render /tmp/t.png --tile-size 32` to check it by eye. `gmx new tileset --from-sheet ... --pick` cuts a handful of cells out of a catalog tilesheet instead, which is right for scenery and wrong here: a picked subset has none of the cells a table names, so the terrain would stop auto-tiling.", + "Size gaps and ledges from the jump rather than by eye. A jump clears `jump_strength^2 / (2 * gravity_per_frame)` pixels of height and `run_speed * 2 * jump_strength / gravity_per_frame` across — at the shipped numbers that is ~110px (3.4 cells) up and ~160px (5 cells) over, measured at 115px in practice because whole-frame steps overshoot slightly. A ledge above 3.4 cells needs a second jump; a gap under 5 cells is crossable.", + "The jump is variable-height: `motion_platformer_step` halves upward velocity on every frame `jump_held` is false. A one-frame tap rises ~19px, not ~115px. This matters most in gametests — hold the key down across the ascent or the jump you are asserting on barely leaves the ground.", + "Stomping enemies is `motion_platformer_stomp(jump, obj_enemy, bounce)`, and two things decide whether it feels fair. Call it *before* the contact-death test in obj_player's Step — a stomp is a frame where both are true and the first test wins. And hold `jump_held` true for a few frames after one, or the same variable-height rule above cuts the bounce to nothing for a player who was not already holding jump.", +] + +[roles] +obj_level = "boots the kernel and counts the coins the room holds" +obj_player = "run and jump against the `Tiles` terrain, camera follow, coin/goal pickup" +obj_coin = "a collectible; scores and vanishes" +obj_goal = "touching it ends the run as won" +obj_enemy = "patrols its platform and turns at the ledge" +rm_level = "the only room — `Tiles` is the terrain movement collides against, and the only copy of the level shape" + +# Heights are in pixels of content, against `[tuning] cell = 32`. A character +# a little taller than a cell reads as a character; one sized exactly to the +# cell reads as another block. These are the numbers the movement was tuned +# around, so an art pass has them rather than guessing. +[art.player] +catalog = "ninja_woods/sprites/spr_player_idle" +placeholder = "flat rectangle" +fit_height = 40 + +[art.enemy] +catalog = "ninja_woods/sprites/spr_enemy1_walk" +placeholder = "flat rectangle, shorter than the player" +fit_height = 34 + +[art.coin] +catalog = "ninja_woods/sprites/spr_coin" +placeholder = "flat square" +# Smaller than the 32px cell: a pickup sized to the grid reads as terrain. +fit_height = 24 + +[art.terrain] +catalog = "world_tilesets/sprites/spr_illustrated_side_Flameville" +placeholder = "a two-cell sheet: blank, then one flat block" +fit_height = 32 + +[tuning] +cell = 32 +run_speed = 4 +jump_strength = 11 +gravity_per_frame = 0.55 +coin_score = 10 +# Slower than the player on purpose: a patrol you cannot outrun is a wall. +enemy_speed = 1.5 diff --git a/prefabs/platformer/project.toml b/prefabs/platformer/project.toml new file mode 100644 index 00000000..3e466a78 --- /dev/null +++ b/prefabs/platformer/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "platformer" + +[room_order] +order = ["rm_level"] diff --git a/prefabs/platformer/rooms/rm_level/layers/Background.toml b/prefabs/platformer/rooms/rm_level/layers/Background.toml new file mode 100644 index 00000000..b7944fb4 --- /dev/null +++ b/prefabs/platformer/rooms/rm_level/layers/Background.toml @@ -0,0 +1,11 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +# Deliberately far back. This is the fallback fill, so anything added later — +# a sky gradient, parallax bands — has room to sit in front of it. At a depth +# near the terrain's it instead *covers* them: an opaque fill hides every band +# behind it, and the room looks like the art was never added. +depth = 1000 + +[background] +colour = "#1B2233" diff --git a/prefabs/platformer/rooms/rm_level/layers/Instances.toml b/prefabs/platformer/rooms/rm_level/layers/Instances.toml new file mode 100644 index 00000000..6cc5a7b3 --- /dev/null +++ b/prefabs/platformer/rooms/rm_level/layers/Instances.toml @@ -0,0 +1,134 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +# The actors of the level. This layer is the only copy of where they are — +# drag them in the room editor, or edit the entries here. The terrain they +# stand on is the `Tiles` layer beside this one. +[[entries]] +id = "inst_level" +object = "obj_level" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_kernel" +object = "::kernel::obj_kernel" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_obj_coin_0" +object = "obj_coin" +x = 656.0 +y = 80.0 + +[[entries]] +id = "inst_obj_coin_1" +object = "obj_coin" +x = 688.0 +y = 80.0 + +[[entries]] +id = "inst_obj_coin_2" +object = "obj_coin" +x = 1488.0 +y = 80.0 + +[[entries]] +id = "inst_obj_coin_3" +object = "obj_coin" +x = 1520.0 +y = 80.0 + +[[entries]] +id = "inst_obj_coin_4" +object = "obj_coin" +x = 1552.0 +y = 80.0 + +[[entries]] +id = "inst_obj_coin_5" +object = "obj_coin" +x = 2448.0 +y = 112.0 + +[[entries]] +id = "inst_obj_coin_6" +object = "obj_coin" +x = 2480.0 +y = 112.0 + +[[entries]] +id = "inst_obj_coin_7" +object = "obj_coin" +x = 2512.0 +y = 112.0 + +[[entries]] +id = "inst_obj_coin_8" +object = "obj_coin" +x = 1872.0 +y = 144.0 + +[[entries]] +id = "inst_obj_coin_9" +object = "obj_coin" +x = 336.0 +y = 176.0 + +[[entries]] +id = "inst_obj_coin_10" +object = "obj_coin" +x = 368.0 +y = 176.0 + +[[entries]] +id = "inst_obj_enemy_0" +object = "obj_enemy" +x = 1072.0 +y = 192.0 + +[[entries]] +id = "inst_obj_coin_11" +object = "obj_coin" +x = 752.0 +y = 208.0 + +[[entries]] +id = "inst_obj_coin_12" +object = "obj_coin" +x = 2224.0 +y = 208.0 + +[[entries]] +id = "inst_obj_enemy_1" +object = "obj_enemy" +x = 1616.0 +y = 256.0 + +[[entries]] +id = "inst_obj_enemy_2" +object = "obj_enemy" +x = 2608.0 +y = 256.0 + +[[entries]] +id = "inst_obj_goal_0" +object = "obj_goal" +x = 2704.0 +y = 256.0 + +[[entries]] +id = "inst_obj_player_0" +object = "obj_player" +x = 112.0 +y = 288.0 + +[[entries]] +id = "inst_obj_enemy_3" +object = "obj_enemy" +x = 400.0 +y = 288.0 + diff --git a/prefabs/platformer/rooms/rm_level/layers/Tiles.toml b/prefabs/platformer/rooms/rm_level/layers/Tiles.toml new file mode 100644 index 00000000..1f363c03 --- /dev/null +++ b/prefabs/platformer/rooms/rm_level/layers/Tiles.toml @@ -0,0 +1,36 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "tiles" +depth = 50 +tileset = "ts_terrain" + +[autotile] +seed = 0 + +# Generated by `gmx autotile apply` from this layer's terrain. +# 90×14 tiles (47-tile set "default"). +# Redraw it with `gmx autotile shape` or the room editor; edits here +# are overwritten. +[legend] +"." = { tile = 0 } # empty +"2" = { tile = 51 } # E| +"d" = { tile = 53 } # W| +"g" = { tile = 41 } # EW| + +[map] +rows = """ +.......................................................................................... +.......................................................................................... +.......................................................................................... +...................2ggd......................2gggd........................................ +............................................................................2gggd......... +.........................................................2gd.............................. +.........2ggd............................................................................. +......................2gd........2gd................................2gd................... +.................................................2ggd.........................2gggggggd... +2ggggggd........2ggd........2ggd..........2gd................2ggd......................... +..........................................................................2ggd............ +........2ggd............2ggd.........2gd.............2gd.................................. +.......................................................................................... +2ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggd +""" diff --git a/prefabs/platformer/rooms/rm_level/room.toml b/prefabs/platformer/rooms/rm_level/room.toml new file mode 100644 index 00000000..cdab3af0 --- /dev/null +++ b/prefabs/platformer/rooms/rm_level/room.toml @@ -0,0 +1,16 @@ +#:schema ../../.gm-schema/v1.json#room + +# Wider than the view so the follow camera has somewhere to go. +size = { width = 2880, height = 448 } +enable_views = true + +# `enable_views` hands rendering to the viewports, so a room that turns it on +# without a visible one draws nothing at all — a black screen with the HUD on +# top, which validates, builds and boots without complaint. `view = { width, +# height }` sizes the camera and is not a viewport; this is. +[[views]] +visible = true +w_view = 640 +h_view = 360 +w_port = 960 +h_port = 540 diff --git a/prefabs/platformer/sprites/spr_coin/frame_000.png b/prefabs/platformer/sprites/spr_coin/frame_000.png new file mode 100644 index 00000000..e6d8cd05 Binary files /dev/null and b/prefabs/platformer/sprites/spr_coin/frame_000.png differ diff --git a/prefabs/platformer/sprites/spr_coin/sprite.toml b/prefabs/platformer/sprites/spr_coin/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/platformer/sprites/spr_coin/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/platformer/sprites/spr_enemy/frame_000.png b/prefabs/platformer/sprites/spr_enemy/frame_000.png new file mode 100644 index 00000000..f2031484 Binary files /dev/null and b/prefabs/platformer/sprites/spr_enemy/frame_000.png differ diff --git a/prefabs/platformer/sprites/spr_enemy/sprite.toml b/prefabs/platformer/sprites/spr_enemy/sprite.toml new file mode 100644 index 00000000..4fb2a24c --- /dev/null +++ b/prefabs/platformer/sprites/spr_enemy/sprite.toml @@ -0,0 +1,7 @@ +#:schema ../../.gm-schema/v1.json#sprite + +# Bottom, like the player: both are placed by the map's third pass, whose `y` +# is the bottom of the cell. A centre origin puts half the sprite inside the +# block it stands on, and a body wedged in a wall cannot move at all — it +# renders in the right place and simply never patrols. +origin = "bottom" diff --git a/prefabs/platformer/sprites/spr_goal/frame_000.png b/prefabs/platformer/sprites/spr_goal/frame_000.png new file mode 100644 index 00000000..d6097efe Binary files /dev/null and b/prefabs/platformer/sprites/spr_goal/frame_000.png differ diff --git a/prefabs/platformer/sprites/spr_goal/sprite.toml b/prefabs/platformer/sprites/spr_goal/sprite.toml new file mode 100644 index 00000000..807b3295 --- /dev/null +++ b/prefabs/platformer/sprites/spr_goal/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "bottom" diff --git a/prefabs/platformer/sprites/spr_player/frame_000.png b/prefabs/platformer/sprites/spr_player/frame_000.png new file mode 100644 index 00000000..2e7537a9 Binary files /dev/null and b/prefabs/platformer/sprites/spr_player/frame_000.png differ diff --git a/prefabs/platformer/sprites/spr_player/sprite.toml b/prefabs/platformer/sprites/spr_player/sprite.toml new file mode 100644 index 00000000..de740728 --- /dev/null +++ b/prefabs/platformer/sprites/spr_player/sprite.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#sprite + +# Bottom-centre: the character stands ON its y, so floors line up +# without half-height maths in every draw call. +origin = "bottom" diff --git a/prefabs/platformer/sprites/spr_terrain_sheet/frame_000.png b/prefabs/platformer/sprites/spr_terrain_sheet/frame_000.png new file mode 100644 index 00000000..91c8b8a4 Binary files /dev/null and b/prefabs/platformer/sprites/spr_terrain_sheet/frame_000.png differ diff --git a/prefabs/platformer/sprites/spr_terrain_sheet/sprite.toml b/prefabs/platformer/sprites/spr_terrain_sheet/sprite.toml new file mode 100644 index 00000000..265fed33 --- /dev/null +++ b/prefabs/platformer/sprites/spr_terrain_sheet/sprite.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#sprite diff --git a/prefabs/platformer/thumbnail.png b/prefabs/platformer/thumbnail.png new file mode 100644 index 00000000..1c03de62 Binary files /dev/null and b/prefabs/platformer/thumbnail.png differ diff --git a/prefabs/platformer/tilesets/ts_terrain/tileset.toml b/prefabs/platformer/tilesets/ts_terrain/tileset.toml new file mode 100644 index 00000000..5aa163f2 --- /dev/null +++ b/prefabs/platformer/tilesets/ts_terrain/tileset.toml @@ -0,0 +1,62 @@ +#:schema ../../.gm-schema/v1.json#tileset + +# The sheet's first row is blank on purpose: tile index 0 is GameMaker's empty +# tile, so the 47-tile template starts on row 1 (`derive --origin 1,0`) and no +# art lands in a cell nothing can name. +sprite = "spr_terrain_sheet" + +[tile_size] +width = 32 +height = 32 + +[autotile.default] +template = "sides_47" + +[autotile.default.tiles] +"ESW|" = 28 +"ESW|SE" = 29 +"ESW|SE,SW" = 31 +"ESW|SW" = 30 +"ES|" = 42 +"ES|SE" = 43 +"EW|" = 41 +"E|" = 51 +"NESW|" = 8 +"NESW|NE" = 10 +"NESW|NE,NW" = 11 +"NESW|NE,NW,SE" = 15 +"NESW|NE,NW,SE,SW" = 23 +"NESW|NE,NW,SW" = 19 +"NESW|NE,SE" = 14 +"NESW|NE,SE,SW" = 22 +"NESW|NE,SW" = 18 +"NESW|NW" = 9 +"NESW|NW,SE" = 13 +"NESW|NW,SE,SW" = 21 +"NESW|NW,SW" = 17 +"NESW|SE" = 12 +"NESW|SE,SW" = 20 +"NESW|SW" = 16 +"NES|" = 24 +"NES|NE" = 25 +"NES|NE,SE" = 27 +"NES|SE" = 26 +"NEW|" = 36 +"NEW|NE" = 38 +"NEW|NE,NW" = 39 +"NEW|NW" = 37 +"NE|" = 48 +"NE|NE" = 49 +"NSW|" = 32 +"NSW|NW" = 34 +"NSW|NW,SW" = 35 +"NSW|SW" = 33 +"NS|" = 40 +"NW|" = 46 +"NW|NW" = 47 +"N|" = 52 +"SW|" = 44 +"SW|SW" = 45 +"S|" = 50 +"W|" = 53 +"|" = 54 diff --git a/prefabs/powerup/LICENSE b/prefabs/powerup/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/powerup/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/powerup/README.md b/prefabs/powerup/README.md new file mode 100644 index 00000000..3569c536 --- /dev/null +++ b/prefabs/powerup/README.md @@ -0,0 +1,3 @@ +# powerup + +Temporary effects, and what a second pickup does to the first. diff --git a/prefabs/powerup/objects/obj_demo/Create.gml b/prefabs/powerup/objects/obj_demo/Create.gml new file mode 100644 index 00000000..003be0fd --- /dev/null +++ b/prefabs/powerup/objects/obj_demo/Create.gml @@ -0,0 +1,2 @@ +buffs = powerup_make(); +log_lines = []; diff --git a/prefabs/powerup/objects/obj_demo/Draw_GUI.gml b/prefabs/powerup/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..6a99246d --- /dev/null +++ b/prefabs/powerup/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,19 @@ +draw_text(40, 30, "powerup demo — 1 refresh, 2 extend, 3 stack; BACKSPACE clears"); +draw_text(40, 50, "press the same key again while it runs to see the rule differ"); + +var _names = powerup_names(buffs); +for (var i = 0; i < array_length(_names); i++) { + var _n = _names[i]; + var _y = 110 + i * 46; + draw_text(40, _y, _n + " x" + string(powerup_level(buffs, _n)) + + " " + string_format(powerup_seconds(buffs, _n), 1, 1) + "s"); + draw_rectangle(240, _y, 240 + 300 * powerup_fraction(buffs, _n), _y + 14, false); + draw_rectangle(240, _y, 540, _y + 14, true); +} + +// A multiplier with no branch: level is 0 when the effect is not running. +draw_text(40, 300, "damage multiplier " + + string_format(1 + powerup_level(buffs, "damage") * 0.5, 1, 1)); +for (var i = 0; i < array_length(log_lines); i++) { + draw_text(640, 110 + i * 20, log_lines[i]); +} diff --git a/prefabs/powerup/objects/obj_demo/Step.gml b/prefabs/powerup/objects/obj_demo/Step.gml new file mode 100644 index 00000000..af4b3303 --- /dev/null +++ b/prefabs/powerup/objects/obj_demo/Step.gml @@ -0,0 +1,13 @@ +// Three keys, three stacking rules — the whole point of the library is +// that a repeat pickup does something different in each. +if (keyboard_check_pressed(ord("1"))) powerup_grant(buffs, "speed", 4, "refresh"); +if (keyboard_check_pressed(ord("2"))) powerup_grant(buffs, "shield", 4, "extend"); +if (keyboard_check_pressed(ord("3"))) powerup_grant(buffs, "damage", 4, "stack"); +if (keyboard_check_pressed(vk_backspace)) powerup_clear(buffs); + +// Expiries come back once, so an effect is undone exactly once. +var _ended = powerup_step(buffs); +for (var i = 0; i < array_length(_ended); i++) { + array_push(log_lines, _ended[i] + " ended"); +} +while (array_length(log_lines) > 6) array_delete(log_lines, 0, 1); diff --git a/prefabs/powerup/objects/obj_demo/object.toml b/prefabs/powerup/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/powerup/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/powerup/prefab.toml b/prefabs/powerup/prefab.toml new file mode 100644 index 00000000..984313b7 --- /dev/null +++ b/prefabs/powerup/prefab.toml @@ -0,0 +1,12 @@ +schema_version = 1 +name = "Timed power-ups" + +# A library: installed under `prefabs//` in the target project and +# called as `::powerup::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Named effects with a duration and a stacking rule — refresh the clock, extend it, or add a level. That rule is the whole design: picking one by accident is how a power-up ends up either useless or permanent. `powerup_step` returns what expired this frame, so an effect is undone exactly once instead of every frame after it ends." +category = "library" +tags = ["capability:powerups", "capability:buffs", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/powerup/project.toml b/prefabs/powerup/project.toml new file mode 100644 index 00000000..627a9f12 --- /dev/null +++ b/prefabs/powerup/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "powerup" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/powerup/rooms/rm_demo/layers/Background.toml b/prefabs/powerup/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/powerup/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/powerup/rooms/rm_demo/layers/Instances.toml b/prefabs/powerup/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/powerup/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/powerup/rooms/rm_demo/room.toml b/prefabs/powerup/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/powerup/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/powerup/scripts/scr_powerup/scr_powerup.gml b/prefabs/powerup/scripts/scr_powerup/scr_powerup.gml new file mode 100644 index 00000000..f84d1b05 --- /dev/null +++ b/prefabs/powerup/scripts/scr_powerup/scr_powerup.gml @@ -0,0 +1,66 @@ +/// Temporary changes to how the game plays. +/// +/// The rule that matters is what a second pickup of the same thing does +/// while the first is still running. Refreshing, extending and stacking are +/// three different games, and picking one by accident is how a power-up +/// ends up either useless or permanent. + +/// @function powerup_make() +/// @description An empty set of active effects. +function powerup_make() { + return { active: {} }; +} + +/// @function powerup_grant(state, name, seconds, mode) +/// @description Start or renew `name` for `seconds`. `mode` decides what a +/// repeat pickup does: "refresh" resets the clock, "extend" adds to it, +/// "stack" adds a level and resets the clock. +function powerup_grant(state, name, seconds, mode) { + if (!variable_struct_exists(state.active, name)) { + state.active[$ name] = { left: seconds, span: seconds, level: 1 }; + return 1; + } + var _e = state.active[$ name]; + switch (mode) { + case "extend": + _e.left += seconds; + _e.span = max(_e.span, _e.left); + break; + case "stack": + _e.level += 1; + _e.left = seconds; + _e.span = seconds; + break; + default: + _e.left = seconds; + _e.span = seconds; + } + return _e.level; +} + +/// @function powerup_step(state) +/// @description Age every effect and drop the expired ones. Returns an array +/// of the names that ended this frame, so a caller can undo each exactly +/// once rather than checking for absence every frame. +function powerup_step(state) { + var _dt = delta_time / 1000000; + var _ended = []; + var _names = variable_struct_get_names(state.active); + for (var i = 0; i < array_length(_names); i++) { + var _n = _names[i]; + var _e = state.active[$ _n]; + _e.left -= _dt; + if (_e.left <= 0) { + array_push(_ended, _n); + variable_struct_remove(state.active, _n); + } + } + return _ended; +} + +/// @function powerup_clear(state) +/// @description End everything at once. A new run, or a death that should +/// cost the player what they were carrying. +function powerup_clear(state) { + state.active = {}; +} diff --git a/prefabs/powerup/scripts/scr_powerup_query/scr_powerup_query.gml b/prefabs/powerup/scripts/scr_powerup_query/scr_powerup_query.gml new file mode 100644 index 00000000..59ddced3 --- /dev/null +++ b/prefabs/powerup/scripts/scr_powerup_query/scr_powerup_query.gml @@ -0,0 +1,33 @@ +/// @function powerup_has(state, name) +function powerup_has(state, name) { + return variable_struct_exists(state.active, name); +} + +/// @function powerup_level(state, name) +/// @description How many are stacked, or 0 when it is not running — so a +/// multiplier can be `1 + powerup_level(...) * 0.5` with no branch. +function powerup_level(state, name) { + if (!powerup_has(state, name)) return 0; + return state.active[$ name].level; +} + +/// @function powerup_seconds(state, name) +function powerup_seconds(state, name) { + if (!powerup_has(state, name)) return 0; + return state.active[$ name].left; +} + +/// @function powerup_fraction(state, name) +/// @description How much of the effect is left, 0..1, for a pip or a ring. +function powerup_fraction(state, name) { + if (!powerup_has(state, name)) return 0; + var _e = state.active[$ name]; + return _e.span <= 0 ? 0 : clamp(_e.left / _e.span, 0, 1); +} + +/// @function powerup_names(state) +/// @description Every running effect, for drawing the row of icons. +function powerup_names(state) { + return variable_struct_get_names(state.active); +} + diff --git a/prefabs/powerup/thumbnail.png b/prefabs/powerup/thumbnail.png new file mode 100644 index 00000000..ca6c0f46 Binary files /dev/null and b/prefabs/powerup/thumbnail.png differ diff --git a/prefabs/projectile/LICENSE b/prefabs/projectile/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/projectile/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/projectile/README.md b/prefabs/projectile/README.md new file mode 100644 index 00000000..4a253f38 --- /dev/null +++ b/prefabs/projectile/README.md @@ -0,0 +1,3 @@ +# projectile + +Fire something, let it travel, and make sure it stops existing. diff --git a/prefabs/projectile/objects/obj_demo/Create.gml b/prefabs/projectile/objects/obj_demo/Create.gml new file mode 100644 index 00000000..efd3c9df --- /dev/null +++ b/prefabs/projectile/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +// One barrel aims where the target is, the other where it will be. At this +// range the gap between the two is the whole point of `projectile_lead`. +cooldown = projectile_cooldown_make(4); diff --git a/prefabs/projectile/objects/obj_demo/Draw_GUI.gml b/prefabs/projectile/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..b34944dd --- /dev/null +++ b/prefabs/projectile/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,4 @@ +draw_text(40, 30, "projectile demo — hold SPACE to fire"); +draw_text(40, 190, "aimed"); +draw_text(40, 330, "led"); +draw_text(40, 470, "shots live: " + string(instance_number(obj_demo_shot))); diff --git a/prefabs/projectile/objects/obj_demo/Step.gml b/prefabs/projectile/objects/obj_demo/Step.gml new file mode 100644 index 00000000..22b09ea2 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo/Step.gml @@ -0,0 +1,9 @@ +var _target = instance_find(obj_demo_target, 0); +if (_target == noone) exit; + +if (keyboard_check(vk_space) && projectile_ready(cooldown)) { + projectile_launch(obj_demo_shot, 80, 200, + projectile_aim(80, 200, _target.x, _target.y), 9, 3); + projectile_launch(obj_demo_shot, 80, 340, + projectile_lead(80, 340, _target, 9), 9, 3); +} diff --git a/prefabs/projectile/objects/obj_demo/object.toml b/prefabs/projectile/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/projectile/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/projectile/objects/obj_demo_shot/Create.gml b/prefabs/projectile/objects/obj_demo_shot/Create.gml new file mode 100644 index 00000000..75bc02a9 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_shot/Create.gml @@ -0,0 +1,3 @@ +// projectile_launch sets this; a default keeps the object usable if +// something creates one directly. +projectile_life = 2; diff --git a/prefabs/projectile/objects/obj_demo_shot/Step.gml b/prefabs/projectile/objects/obj_demo_shot/Step.gml new file mode 100644 index 00000000..1ff99f9d --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_shot/Step.gml @@ -0,0 +1,5 @@ +var _hit = projectile_step(obj_demo_target); +if (_hit != noone) { + _hit.flash = 0.2; + instance_destroy(); +} diff --git a/prefabs/projectile/objects/obj_demo_shot/object.toml b/prefabs/projectile/objects/obj_demo_shot/object.toml new file mode 100644 index 00000000..6b5c5086 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_shot/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_shot" diff --git a/prefabs/projectile/objects/obj_demo_target/Create.gml b/prefabs/projectile/objects/obj_demo_target/Create.gml new file mode 100644 index 00000000..90ba2208 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_target/Create.gml @@ -0,0 +1,3 @@ +flash = 0; +direction = 90; +speed = 2; diff --git a/prefabs/projectile/objects/obj_demo_target/Draw.gml b/prefabs/projectile/objects/obj_demo_target/Draw.gml new file mode 100644 index 00000000..2b471aef --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_target/Draw.gml @@ -0,0 +1,2 @@ +image_blend = flash > 0 ? c_white : c_red; +draw_self(); diff --git a/prefabs/projectile/objects/obj_demo_target/Step.gml b/prefabs/projectile/objects/obj_demo_target/Step.gml new file mode 100644 index 00000000..8adf7962 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_target/Step.gml @@ -0,0 +1,3 @@ +// Drifts up and down so `projectile_lead` has something to lead. +if (y < 120 || y > 420) direction = -direction; +flash = max(0, flash - delta_time / 1000000); diff --git a/prefabs/projectile/objects/obj_demo_target/object.toml b/prefabs/projectile/objects/obj_demo_target/object.toml new file mode 100644 index 00000000..5ed2ac43 --- /dev/null +++ b/prefabs/projectile/objects/obj_demo_target/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_demo_target" diff --git a/prefabs/projectile/prefab.toml b/prefabs/projectile/prefab.toml new file mode 100644 index 00000000..e0c7f7a9 --- /dev/null +++ b/prefabs/projectile/prefab.toml @@ -0,0 +1,17 @@ +schema_version = 1 +name = "Shooting" + +# A library: installed under `prefabs//` in the target project and +# called as `::projectile::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Launch a projectile at a direction and speed, age it, destroy it on timeout or once it is off the room, and report what it hit. Plus aiming at a point, leading a moving target, and a fire-rate cooldown that holding the key cannot outrun. The lifetime is the part everyone forgets: a bullet that flies off the room is still simulated, and the frame rate falls away minutes after the mistake." +category = "library" +tags = ["capability:shooting", "capability:combat", "genre:shooter", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_shot", "objects/obj_demo_target", "rooms/rm_demo", "sprites/spr_demo_shot", "sprites/spr_demo_target"] + +steps = [ + "`projectile_step` runs in the projectile object's own Step event, not the shooter's.", + "Give the projectile object a sprite — a spriteless object has no collision mask, so it will never report a hit.", +] diff --git a/prefabs/projectile/project.toml b/prefabs/projectile/project.toml new file mode 100644 index 00000000..6543a8a3 --- /dev/null +++ b/prefabs/projectile/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "projectile" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/projectile/rooms/rm_demo/layers/Background.toml b/prefabs/projectile/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/projectile/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/projectile/rooms/rm_demo/layers/Instances.toml b/prefabs/projectile/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..de0d48a6 --- /dev/null +++ b/prefabs/projectile/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_demo_target" +object = "obj_demo_target" +x = 760.0 +y = 270.0 diff --git a/prefabs/projectile/rooms/rm_demo/room.toml b/prefabs/projectile/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/projectile/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/projectile/scripts/scr_projectile/scr_projectile.gml b/prefabs/projectile/scripts/scr_projectile/scr_projectile.gml new file mode 100644 index 00000000..3d40d454 --- /dev/null +++ b/prefabs/projectile/scripts/scr_projectile/scr_projectile.gml @@ -0,0 +1,47 @@ +/// Things that are fired, travel, and stop existing. +/// +/// The lifetime is not decoration. A bullet that leaves the room without +/// being destroyed is still simulated, and a game that fires ten a second +/// accumulates thousands of them over a few minutes — the frame rate falls +/// away long after the mistake, which is what makes it hard to find. + +/// @function projectile_launch(obj, from_x, from_y, dir, speed, seconds) +/// @description Create one at (from_x, from_y) travelling in `dir` degrees +/// at `speed` pixels a frame, dead after `seconds`. Returns the instance so +/// a caller can set damage or a colour on it. +function projectile_launch(obj, from_x, from_y, dir, speed, seconds) { + var _p = instance_create_depth(from_x, from_y, 0, obj); + _p.direction = dir; + _p.speed = speed; + _p.image_angle = dir; + _p.projectile_life = seconds; + return _p; +} + +/// @function projectile_step(obj_hit) +/// @scope instance +/// @description Run from the projectile's own Step. Ages it, destroys it on +/// timeout or once it is fully outside the room, and returns the first +/// `obj_hit` it is touching — or `noone`. Destroying it on a hit is the +/// caller's choice, because a piercing shot is the same code minus that line. +function projectile_step(obj_hit) { + projectile_life -= delta_time / 1000000; + if (projectile_life <= 0 || projectile_offscreen()) { + instance_destroy(); + return noone; + } + return obj_hit == noone ? noone : instance_place(x, y, obj_hit); +} + +/// @function projectile_offscreen() +/// @scope instance +/// @description True once this instance is past every room edge by its own +/// sprite width, so something spawned just outside is not killed instantly. +function projectile_offscreen() { + // A spriteless projectile has no width to measure, so assume a small + // one — generous enough that a shot spawned just off-screen is not + // destroyed before it enters. + var _pad = sprite_exists(sprite_index) ? sprite_get_width(sprite_index) : 16; + return x < -_pad || y < -_pad || x > room_width + _pad || y > room_height + _pad; +} + diff --git a/prefabs/projectile/scripts/scr_projectile_aim/scr_projectile_aim.gml b/prefabs/projectile/scripts/scr_projectile_aim/scr_projectile_aim.gml new file mode 100644 index 00000000..9ece9c43 --- /dev/null +++ b/prefabs/projectile/scripts/scr_projectile_aim/scr_projectile_aim.gml @@ -0,0 +1,34 @@ +/// @function projectile_aim(from_x, from_y, to_x, to_y) +/// @description The direction in degrees from one point to another — what +/// `projectile_launch` wants for a shot at a target that is standing still. +function projectile_aim(from_x, from_y, to_x, to_y) { + return point_direction(from_x, from_y, to_x, to_y); +} + +/// @function projectile_lead(from_x, from_y, target, bullet_speed) +/// @description Aim where a moving `target` will be rather than where it is. +/// Falls back to aiming straight at it when the target is faster than the +/// shot, which is the case where no interception exists. +/// +/// Returns `-1` when there is nothing to shoot at — a destroyed target or a +/// non-positive speed. Check for it: every real direction is 0–359, and 0 is +/// due east, so returning 0 there fires a live shot to the right instead of +/// holding fire. +function projectile_lead(from_x, from_y, target, bullet_speed) { + if (!instance_exists(target) || bullet_speed <= 0) { + return -1; + } + var _straight = point_direction(from_x, from_y, target.x, target.y); + // No interception exists once the target outruns the shot: the meeting + // point solves behind the shooter and the extrapolation diverges, so the + // turret would aim further and further from anything real. + if (target.speed >= bullet_speed) { + return _straight; + } + var _dist = point_distance(from_x, from_y, target.x, target.y); + var _t = _dist / bullet_speed; + var _tx = target.x + lengthdir_x(target.speed * _t, target.direction); + var _ty = target.y + lengthdir_y(target.speed * _t, target.direction); + return point_direction(from_x, from_y, _tx, _ty); +} + diff --git a/prefabs/projectile/scripts/scr_projectile_cooldown/scr_projectile_cooldown.gml b/prefabs/projectile/scripts/scr_projectile_cooldown/scr_projectile_cooldown.gml new file mode 100644 index 00000000..311da057 --- /dev/null +++ b/prefabs/projectile/scripts/scr_projectile_cooldown/scr_projectile_cooldown.gml @@ -0,0 +1,17 @@ +/// @function projectile_cooldown_make(shots_per_second) +/// @description State for a weapon that fires at a fixed rate. +function projectile_cooldown_make(shots_per_second) { + var _rate = max(0.0001, shots_per_second); + return { gap: 1 / _rate, left: 0 }; +} + +/// @function projectile_ready(cooldown) +/// @description Age the cooldown by one frame and report whether a shot is +/// allowed. Returns true at most once per gap however often it is asked, +/// so holding the fire key cannot outrun the rate. +function projectile_ready(cooldown) { + cooldown.left = max(0, cooldown.left - delta_time / 1000000); + if (cooldown.left > 0) return false; + cooldown.left = cooldown.gap; + return true; +} diff --git a/prefabs/projectile/sprites/spr_demo_shot/frame_000.png b/prefabs/projectile/sprites/spr_demo_shot/frame_000.png new file mode 100644 index 00000000..b971e8cb Binary files /dev/null and b/prefabs/projectile/sprites/spr_demo_shot/frame_000.png differ diff --git a/prefabs/projectile/sprites/spr_demo_shot/sprite.toml b/prefabs/projectile/sprites/spr_demo_shot/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/projectile/sprites/spr_demo_shot/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/projectile/sprites/spr_demo_target/frame_000.png b/prefabs/projectile/sprites/spr_demo_target/frame_000.png new file mode 100644 index 00000000..a62f0c3c Binary files /dev/null and b/prefabs/projectile/sprites/spr_demo_target/frame_000.png differ diff --git a/prefabs/projectile/sprites/spr_demo_target/sprite.toml b/prefabs/projectile/sprites/spr_demo_target/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/projectile/sprites/spr_demo_target/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/projectile/thumbnail.png b/prefabs/projectile/thumbnail.png new file mode 100644 index 00000000..3aefdc9d Binary files /dev/null and b/prefabs/projectile/thumbnail.png differ diff --git a/prefabs/quiz_trivia/LICENSE b/prefabs/quiz_trivia/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/quiz_trivia/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/quiz_trivia/README.md b/prefabs/quiz_trivia/README.md new file mode 100644 index 00000000..ef09cbf9 --- /dev/null +++ b/prefabs/quiz_trivia/README.md @@ -0,0 +1,22 @@ +# quiz_trivia + +A whole quiz game — questions against the clock, streak scoring, lives, results. + +## What you change first + +**The questions.** They are a table in `prefab.toml`, compiled into +`scripts/scr_quiz_trivia_data/`. Edit the generated script for a one-off, +or the prefab if you want `gmx prefab check --write` to keep producing them. + +**`obj_quiz`.** Everything about how this particular game plays — the +verdict pause, the timer bar, what a streak is worth — is in that one +object, and it is yours. + +## What you do not have to maintain + +Score, combo, lives, the save store and the high score are `::kernel::`. +Hover, press and layout are `::ui_board::`. Hit-stop, shake and the score +popups are `::feel::`. Those are prefabs: call them, do not read them. + +Two answers or nine is the same code — `ui_layout_grid` places whatever +you added, so adding an answer is adding a row to the table. diff --git a/prefabs/quiz_trivia/demo.gametest.json b/prefabs/quiz_trivia/demo.gametest.json new file mode 100644 index 00000000..ce0e2b7e --- /dev/null +++ b/prefabs/quiz_trivia/demo.gametest.json @@ -0,0 +1,74 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 160, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_quiz); global.gt_seen=false; global.gt_q=0; global.gt_index=-99; global.gt_t0=undefined; global.gt_counts=false; global.gt_advanced=false;" + }, + "watch": { + "gml": "if (instance_exists(obj_quiz)) { with (obj_quiz) { global.gt_seen = true; global.gt_q = array_length(questions); if (global.gt_index == -99) global.gt_index = index; if (index > global.gt_index) global.gt_advanced = true; if (global.gt_t0 == undefined) global.gt_t0 = time_left; if (time_left < global.gt_t0 - 0.5) global.gt_counts = true; } }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "questions": { + "gml": "return global.gt_q > 0;" + }, + "showing": { + "gml": "return global.gt_index >= 0;" + }, + "counts": { + "gml": "return global.gt_counts;" + } + }, + "timeline": [ + { + "frame": 150, + "assert": { + "id": "seen", + "name": "the quiz runs in rm_quiz", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "questions", + "name": "the question table is loaded", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "showing", + "name": "a question is on the board", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "counts", + "name": "the answer clock counts down", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 148, + "code": [ + "watch" + ] + } + ] +} diff --git a/prefabs/quiz_trivia/objects/obj_quiz/Create.gml b/prefabs/quiz_trivia/objects/obj_quiz/Create.gml new file mode 100644 index 00000000..4beb6249 --- /dev/null +++ b/prefabs/quiz_trivia/objects/obj_quiz/Create.gml @@ -0,0 +1,80 @@ +// Hand the generated content and tuning to the kernel. Every component +// reads through ::kernel::kernel_data / kernel_tuning from here on, so +// none of them needs to know this template's name. +::kernel::kernel_boot(); +::kernel::kernel_data_source(quiz_trivia_data, quiz_trivia_tuning()); + +questions = ::kernel::kernel_data("questions"); +board = ::ui_board::ui_board_make(); + +index = -1; +lives_left = ::kernel::kernel_tuning("lives", 3); +time_left = 0; +verdict = ""; +verdict_left = 0; +chosen = -1; + +/// Build the answer board for question `n`, or end the run when the +/// questions run out. +next_question = function(n) { + if (n >= array_length(questions)) { + ::kernel::kernel_game_over("cleared"); + return; + } + index = n; + chosen = -1; + verdict = ""; + time_left = ::kernel::kernel_tuning("question_time", 12); + + var _q = questions[n]; + ::ui_board::ui_clear(board); + for (var i = 0; i < array_length(_q.answers); i++) { + ::ui_board::ui_add(board, "a" + string(i), 0, 0, 0, 0, _q.answers[i], i); + } + // Two columns: four answers read as a block rather than a list, and + // the layout is the same code for three answers or six. + ::ui_board::ui_layout_grid(board, 2, 160, 250, 300, 76, 16); +}; + +/// Resolve `pick` against the current question. `pick` of -1 is a +/// timeout, which counts as wrong but is worth distinguishing in the +/// verdict line. +answer = function(pick) { + var _q = questions[index]; + chosen = pick; + var _right = (pick == _q.correct); + + if (_right) { + ::kernel::kernel_combo_bump(); + var _base = ::kernel::kernel_tuning("points_correct", 100) + + ::kernel::kernel_tuning("streak_bonus", 25) * (::kernel::kernel_combo() - 1); + var _gained = ::kernel::kernel_score_add(_base); + verdict = "correct"; + ::feel::feel_pop(480, 300, "+" + string(_gained), c_lime); + ::feel::feel_hitstop(0.06); + } else { + ::kernel::kernel_combo_break(); + lives_left -= 1; + verdict = (pick == -1) ? "out of time" : "wrong"; + ::feel::feel_shake(0.3, 10); + ::feel::feel_pop(480, 300, verdict, c_red); + } + + // Tint the board so the right answer is visible during the pause, + // and stop it accepting a second click. + for (var i = 0; i < array_length(_q.answers); i++) { + ::ui_board::ui_set_enabled(board, "a" + string(i), false); + if (i == _q.correct) ::ui_board::ui_set_tint(board, "a" + string(i), c_lime); + else if (i == pick) ::ui_board::ui_set_tint(board, "a" + string(i), c_red); + } + + verdict_left = 0.9; + if (lives_left <= 0) ::kernel::kernel_game_over("out of lives"); +}; + +next_question(0); +::kernel::kernel_state_set(::kernel::kernel_states().play); + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +question_time = ::kernel::kernel_tuning("question_time", 12); diff --git a/prefabs/quiz_trivia/objects/obj_quiz/Draw_GUI.gml b/prefabs/quiz_trivia/objects/obj_quiz/Draw_GUI.gml new file mode 100644 index 00000000..8614d9ce --- /dev/null +++ b/prefabs/quiz_trivia/objects/obj_quiz/Draw_GUI.gml @@ -0,0 +1,37 @@ +var _w = ::kernel::kernel_gui_width(); +var _s = ::kernel::kernel_state(); + +if (_s == ::kernel::kernel_states().over) { + ::kernel::kernel_draw_panel(0, 0, _w, ::kernel::kernel_gui_height(), c_black, 0.7); + ::kernel::kernel_draw_text(_w / 2, 180, "RUN OVER", c_white, fa_center); + ::kernel::kernel_draw_text(_w / 2, 220, ::kernel::kernel_result(), c_ltgray, fa_center); + ::kernel::kernel_draw_text(_w / 2, 270, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); + ::kernel::kernel_draw_text(_w / 2, 300, + "best streak x" + string(::kernel::kernel_combo_best()), c_ltgray, fa_center); + ::kernel::kernel_draw_text(_w / 2, 340, + "best ever " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); + exit; +} + +if (!::kernel::kernel_playing()) exit; + +var _q = questions[index]; + +// Question card. +::kernel::kernel_draw_panel(120, 120, _w - 240, 100, c_black, 0.45); +::kernel::kernel_draw_text(_w / 2, 152, _q.ask, c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 190, + "question " + string(index + 1) + " of " + string(array_length(questions)), + c_gray, fa_center); + +::ui_board::ui_draw(board); + +// Timer bar: the clock is the pressure, so it gets a bar and not a number. +var _frac = clamp(time_left / question_time, 0, 1); +::kernel::kernel_draw_panel(120, 108, _w - 240, 6, c_black, 0.5); +draw_set_colour(_frac < 0.25 ? c_red : c_aqua); +draw_rectangle(120, 108, 120 + (_w - 240) * _frac, 114, false); +draw_set_colour(c_white); + +::kernel::kernel_draw_text(_w - 16, 32, "lives " + string(lives_left), c_white, fa_right); diff --git a/prefabs/quiz_trivia/objects/obj_quiz/Step.gml b/prefabs/quiz_trivia/objects/obj_quiz/Step.gml new file mode 100644 index 00000000..9cfec267 --- /dev/null +++ b/prefabs/quiz_trivia/objects/obj_quiz/Step.gml @@ -0,0 +1,23 @@ +if (!::kernel::kernel_playing()) exit; + +var _dt = delta_time / 1000000; + +// A verdict freezes the board briefly so the player sees which answer +// was right before the next question replaces it. +if (verdict != "") { + verdict_left -= _dt; + if (verdict_left <= 0) next_question(index + 1); + exit; +} + +time_left -= _dt; +if (time_left <= 0) { + answer(-1); + exit; +} + +var _hit = ::ui_board::ui_update(board); +if (_hit != "") { + var _spot = ::ui_board::ui_get(board, _hit); + answer(_spot.payload); +} diff --git a/prefabs/quiz_trivia/objects/obj_quiz/object.toml b/prefabs/quiz_trivia/objects/obj_quiz/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/quiz_trivia/objects/obj_quiz/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/quiz_trivia/prefab.toml b/prefabs/quiz_trivia/prefab.toml new file mode 100644 index 00000000..de7343af --- /dev/null +++ b/prefabs/quiz_trivia/prefab.toml @@ -0,0 +1,74 @@ +schema_version = 1 +name = "Quiz / trivia" +install = "copy" +description = "Answer questions against the clock, with a combo for a streak." +category = "game" +tags = ["genre:quiz", "role:template"] + +# Direct composition. kernel and feel arrive transitively through +# ui-board's `requires` too; listing them is the declaration of intent. +requires = ["kernel", "feel", "ui_board"] + +seam = "scripts/scr_quiz_trivia_data/scr_quiz_trivia_data.gml" + +steps = [ + "`quiz_trivia.gametest.json` came with the prefab and already passes: the quiz runs in rm_quiz, the question table is loaded, a question is on the board, and the answer clock counts down. Run it with `gmx test quiz_trivia.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", +] + +[roles] +obj_quiz = "the whole game: the question list, the answer board, the clock and the verdict" +rm_quiz = "the only room — the board is laid out in GUI space, so its size is the window size" + +[tuning] +question_time = 12.0 +points_correct = 100 +streak_bonus = 25 +lives = 3 + +[art.answer_panel] +catalog = "ui_panels_flat/sprites/Flat_Blue_Panel" +placeholder = "drawn rectangle" + +[art.subject] +catalog = "iconic_animals/sprites/spr_cartoon_cat_black" +placeholder = "answer text only" + +[[content.questions]] +ask = "Which of these animals barks?" +answers = ["Dog", "Cat", "Cow", "Duck"] +correct = 0 + +[[content.questions]] +ask = "Which one lays eggs?" +answers = ["Horse", "Hen", "Pig", "Sheep"] +correct = 1 + +[[content.questions]] +ask = "Which lives in the ocean?" +answers = ["Camel", "Wolf", "Dolphin", "Goat"] +correct = 2 + +[[content.questions]] +ask = "Which of these can fly?" +answers = ["Penguin", "Ostrich", "Emu", "Falcon"] +correct = 3 + +[[content.questions]] +ask = "Which is the largest?" +answers = ["Blue whale", "Elephant", "Giraffe", "Bear"] +correct = 0 + +[[content.questions]] +ask = "Which changes colour to hide?" +answers = ["Badger", "Chameleon", "Otter", "Moose"] +correct = 1 + +[[content.questions]] +ask = "Which has eight legs?" +answers = ["Beetle", "Ant", "Spider", "Moth"] +correct = 2 + +[[content.questions]] +ask = "Which sleeps upside down?" +answers = ["Squirrel", "Rabbit", "Mole", "Bat"] +correct = 3 diff --git a/prefabs/quiz_trivia/project.toml b/prefabs/quiz_trivia/project.toml new file mode 100644 index 00000000..bd7e91df --- /dev/null +++ b/prefabs/quiz_trivia/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "quiz_trivia" + +[room_order] +order = ["rm_quiz"] diff --git a/prefabs/quiz_trivia/rooms/rm_quiz/layers/Background.toml b/prefabs/quiz_trivia/rooms/rm_quiz/layers/Background.toml new file mode 100644 index 00000000..27a8de59 --- /dev/null +++ b/prefabs/quiz_trivia/rooms/rm_quiz/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#161A28" diff --git a/prefabs/quiz_trivia/rooms/rm_quiz/layers/Instances.toml b/prefabs/quiz_trivia/rooms/rm_quiz/layers/Instances.toml new file mode 100644 index 00000000..f616c25c --- /dev/null +++ b/prefabs/quiz_trivia/rooms/rm_quiz/layers/Instances.toml @@ -0,0 +1,16 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_kernel" +object = "::kernel::obj_kernel" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_quiz" +object = "obj_quiz" +x = 0.0 +y = 0.0 diff --git a/prefabs/quiz_trivia/rooms/rm_quiz/room.toml b/prefabs/quiz_trivia/rooms/rm_quiz/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/quiz_trivia/rooms/rm_quiz/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/quiz_trivia/thumbnail.png b/prefabs/quiz_trivia/thumbnail.png new file mode 100644 index 00000000..238ccab2 Binary files /dev/null and b/prefabs/quiz_trivia/thumbnail.png differ diff --git a/prefabs/rhythm/LICENSE b/prefabs/rhythm/LICENSE new file mode 100644 index 00000000..ff03466c --- /dev/null +++ b/prefabs/rhythm/LICENSE @@ -0,0 +1,26 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +The art and audio in this prefab are generated placeholders, released into the +public domain (CC0 1.0). Replace them freely. diff --git a/prefabs/rhythm/README.md b/prefabs/rhythm/README.md new file mode 100644 index 00000000..614cae61 --- /dev/null +++ b/prefabs/rhythm/README.md @@ -0,0 +1,37 @@ +# Rhythm + +Four lanes, arrow keys, notes falling to a 120 BPM track. Hit a note as it +crosses the line to score; let it past and the combo breaks. + +The point of this one is its clock. `obj_rhythm` reads +`audio_sound_get_track_position` once per step and every other number in the +game is derived from it — where each note is drawn, whether a press counts, +and when the song is over. There is no frame counter anywhere, which is what +keeps the notes on the beat rather than near it. + +## Playing it + + gmx run . + +Arrow keys, one per lane. The count-in is four beats; the first note lands two +seconds in. + +## Checking it + + gmx test rhythm.gametest.json --project . + +It needs a runtime whose headless audio works, and until one ships that means +passing `--dev-runtime`. On an older runtime all eight asserts fail with `0.0` +and the HUD sits at `0.00 / 8.00s`: that is the bug below being reported, not +a broken recipe. + +Eight asserts, covering the whole audio path this game depends on: that +`audio_play_sound` hands back a playable voice, that its position advances at +one second per sixty frames, that the chart spawns notes off that position, +that a press on the beat is judged a hit, and that the position clamps at the +end of the track instead of running past it. + +Those asserts are worth more than they look. All of them are true of a working +runner and all of them were silently false of a headless one until recently: +a run with no audio device reported position 0 forever, so a rhythm game built +on it looked like it was working and simply never spawned a note. diff --git a/prefabs/rhythm/demo.gametest.json b/prefabs/rhythm/demo.gametest.json new file mode 100644 index 00000000..8009db75 --- /dev/null +++ b/prefabs/rhythm/demo.gametest.json @@ -0,0 +1,233 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 570, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "global.rh_probe_5s = false; global.rh_probe_end = false;" + }, + "probe_5s": { + "gml": "global.rh_probe_5s = (global.rh_time > 4.6 && global.rh_time < 5.4);" + }, + "probe_end": { + "gml": "global.rh_probe_end = (global.rh_time > 7.5 && global.rh_time < 8.1);" + }, + "voice_valid": { + "gml": "return global.rh_voice >= 0;" + }, + "clock_ran": { + "gml": "return global.rh_time_max > 7.5;" + }, + "clock_at_5s": { + "gml": "return global.rh_probe_5s;" + }, + "notes_spawned": { + "gml": "return global.rh_notes_max > 0;" + }, + "notes_hit": { + "gml": "return global.rh_hits >= 5;" + }, + "combo_built": { + "gml": "return global.rh_best_combo >= 3;" + }, + "song_finished": { + "gml": "return global.rh_finished;" + }, + "clamped": { + "gml": "return global.rh_probe_end;" + } + }, + "timeline": [ + { + "frame": 2, + "code": [ + "init" + ] + }, + { + "frame": 120, + "inputs": [ + { + "key": "vk_left", + "state": "down" + } + ] + }, + { + "frame": 122, + "inputs": [ + { + "key": "vk_left", + "state": "up" + } + ] + }, + { + "frame": 150, + "inputs": [ + { + "key": "vk_down", + "state": "down" + } + ] + }, + { + "frame": 152, + "inputs": [ + { + "key": "vk_down", + "state": "up" + } + ] + }, + { + "frame": 180, + "inputs": [ + { + "key": "vk_up", + "state": "down" + } + ] + }, + { + "frame": 182, + "inputs": [ + { + "key": "vk_up", + "state": "up" + } + ] + }, + { + "frame": 210, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 212, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 240, + "inputs": [ + { + "key": "vk_left", + "state": "down" + } + ] + }, + { + "frame": 242, + "inputs": [ + { + "key": "vk_left", + "state": "up" + } + ] + }, + { + "frame": 270, + "inputs": [ + { + "key": "vk_down", + "state": "down" + } + ] + }, + { + "frame": 272, + "inputs": [ + { + "key": "vk_down", + "state": "up" + } + ] + }, + { + "frame": 300, + "code": [ + "probe_5s" + ] + }, + { + "frame": 560, + "code": [ + "probe_end" + ] + }, + { + "frame": 565, + "assert": { + "id": "voice_valid", + "name": "audio_play_sound returns a playable voice", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "clock_ran", + "name": "and its track position advances across the whole song", + "expect": true + } + }, + { + "frame": 300, + "assert": { + "id": "clock_at_5s", + "name": "at a rate of one second per sixty frames", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "notes_spawned", + "name": "the chart spawns notes off that position", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "notes_hit", + "name": "a press on the beat is judged a hit", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "combo_built", + "name": "and consecutive hits build a combo", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "song_finished", + "name": "the song ends itself at the end of the track", + "expect": true + } + }, + { + "frame": 565, + "assert": { + "id": "clamped", + "name": "and the position clamps there rather than running on", + "expect": true + } + } + ] +} diff --git a/prefabs/rhythm/objects/obj_note/Create.gml b/prefabs/rhythm/objects/obj_note/Create.gml new file mode 100644 index 00000000..1f678f44 --- /dev/null +++ b/prefabs/rhythm/objects/obj_note/Create.gml @@ -0,0 +1,6 @@ +// Set by the controller the frame this is created. +time = 0; +lane = 0; +owner = noone; +judged = false; +depth = 0; diff --git a/prefabs/rhythm/objects/obj_note/Step.gml b/prefabs/rhythm/objects/obj_note/Step.gml new file mode 100644 index 00000000..20fae313 --- /dev/null +++ b/prefabs/rhythm/objects/obj_note/Step.gml @@ -0,0 +1,8 @@ +if (!instance_exists(owner)) exit; + +// Position is a pure function of where the song is, so a note is never +// integrated frame by frame and can never drift away from its own beat. +x = owner.lane_x[lane]; +y = owner.hit_y - (time - owner.song_time) * owner.scroll_px; + +if (y > room_height + 64) instance_destroy(); diff --git a/prefabs/rhythm/objects/obj_note/object.toml b/prefabs/rhythm/objects/obj_note/object.toml new file mode 100644 index 00000000..5e8ee2fc --- /dev/null +++ b/prefabs/rhythm/objects/obj_note/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_note" diff --git a/prefabs/rhythm/objects/obj_rhythm/Create.gml b/prefabs/rhythm/objects/obj_rhythm/Create.gml new file mode 100644 index 00000000..2cbd509d --- /dev/null +++ b/prefabs/rhythm/objects/obj_rhythm/Create.gml @@ -0,0 +1,66 @@ +// The song is the clock. Every note position and every judgement below is +// measured against `audio_sound_get_track_position`, never against a frame +// count, so a stutter moves the picture and never the beat. + +depth = 100; + +// The kernel owns run state, score and combo, and has to be booted before +// anything reads them. +::kernel::kernel_boot(); +::kernel::kernel_state_set(::kernel::kernel_states().play); +// This game draws its own HUD, because the song clock belongs beside the +// score rather than under it. +::kernel::kernel_hud_visible(false); +// And it owns pausing or nothing does: the kernel's pause stops the game +// while the track keeps playing, and the chart is never in step again. +::kernel::kernel_pause_enabled(false); + +var _t = rhythm_tuning(); +song_length = _t.song_length; +bpm = _t.bpm; +beat = 60 / bpm; + +lanes = 4; +lane_x = [200, 280, 360, 440]; +lane_key = [vk_left, vk_down, vk_up, vk_right]; +lane_name = ["left", "down", "up", "right"]; +hit_y = 400; +// Pixels a note travels per second of lead time. With hit_y at 400 a note is +// on screen for a little over a second and a half before it lands. +scroll_px = _t.scroll_px; + +// Seconds either side of the note. `miss_window` is also the width of the +// press that registers at all, so it is the outer edge of `good`. +perfect_window = _t.perfect_window; +good_window = _t.good_window; +miss_window = _t.miss_window; + +// One note per beat from beat 4, lanes cycling. Beats 0-3 are the count-in. +chart = []; +for (var b = 4; b < 16; b++) { + array_push(chart, [b * beat, (b - 4) % lanes]); +} +spawned = 0; + +voice = audio_play_sound(snd_song, 10, false); +song_time = 0; +finished = false; + +hits = 0; +misses = 0; +judgement = ""; +judgement_at = -99; + +// Mirrored to globals because a gametest can only assert on globals, and +// because "did the transport actually run" is the question this game exists +// to answer. +global.rh_voice = voice; +global.rh_time = 0; +global.rh_time_max = 0; +global.rh_hits = 0; +global.rh_misses = 0; +global.rh_score = 0; +global.rh_best_combo = 0; +global.rh_finished = false; +global.rh_notes_max = 0; +global.rh_end_time = -1; diff --git a/prefabs/rhythm/objects/obj_rhythm/Draw.gml b/prefabs/rhythm/objects/obj_rhythm/Draw.gml new file mode 100644 index 00000000..1376a758 --- /dev/null +++ b/prefabs/rhythm/objects/obj_rhythm/Draw.gml @@ -0,0 +1,19 @@ +// The playfield, behind the notes. `make_colour_rgb` rather than a `$` literal: +// GameMaker's hex literals are $BBGGRR, so an RGB value pasted from a palette +// comes out with its red and blue swapped. +var _lane_a = make_colour_rgb(30, 33, 48); +var _lane_b = make_colour_rgb(36, 40, 56); +var _rim = make_colour_rgb(58, 64, 84); + +draw_set_alpha(1); +for (var l = 0; l < lanes; l++) { + var cx = lane_x[l]; + draw_set_colour(l % 2 == 0 ? _lane_a : _lane_b); + draw_rectangle(cx - 38, 0, cx + 38, room_height, false); +} +draw_sprite_ext(spr_hitline, 0, 320, hit_y, 1, 1, 0, c_white, 0.85); +draw_set_colour(_rim); +for (var l = 0; l < lanes; l++) { + draw_rectangle(lane_x[l] - 30, hit_y - 12, lane_x[l] + 30, hit_y + 12, true); +} +draw_set_colour(c_white); diff --git a/prefabs/rhythm/objects/obj_rhythm/Draw_GUI.gml b/prefabs/rhythm/objects/obj_rhythm/Draw_GUI.gml new file mode 100644 index 00000000..3298cb87 --- /dev/null +++ b/prefabs/rhythm/objects/obj_rhythm/Draw_GUI.gml @@ -0,0 +1,30 @@ +draw_set_colour(c_white); +draw_set_halign(fa_left); +draw_text(16, 12, "SCORE " + string(::kernel::kernel_score())); +draw_text(16, 32, "COMBO " + string(::kernel::kernel_combo()) + + " BEST " + string(::kernel::kernel_combo_best())); +draw_text(16, 52, "HIT " + string(hits) + " MISS " + string(misses)); + +draw_set_halign(fa_right); +draw_text(624, 12, string_format(song_time, 1, 2) + " / " + string_format(song_length, 1, 2) + "s"); +draw_set_halign(fa_left); + +// The judgement fades rather than latching, so the screen says what just +// happened rather than what happened at some point. +if (song_time - judgement_at < 0.35) { + draw_set_halign(fa_center); + draw_text(320, 300, judgement); + draw_set_halign(fa_left); +} + +if (finished) { + draw_set_halign(fa_center); + draw_text(320, 200, "SONG COMPLETE"); + draw_text(320, 220, string(hits) + " hit, " + string(misses) + " missed"); + draw_text(320, 240, "SCORE " + string(::kernel::kernel_score())); + draw_set_halign(fa_left); +} else if (song_time < 1.0) { + draw_set_halign(fa_center); + draw_text(320, 200, "ARROW KEYS - hit the note on the line"); + draw_set_halign(fa_left); +} diff --git a/prefabs/rhythm/objects/obj_rhythm/Step.gml b/prefabs/rhythm/objects/obj_rhythm/Step.gml new file mode 100644 index 00000000..c952ee46 --- /dev/null +++ b/prefabs/rhythm/objects/obj_rhythm/Step.gml @@ -0,0 +1,61 @@ +song_time = audio_sound_get_track_position(voice); + +// Spawn a note once it is within one screen's approach of the line. +var lead = (hit_y + 40) / scroll_px; +while (spawned < array_length(chart) && chart[spawned][0] - song_time <= lead) { + var n = instance_create_layer(0, 0, "Instances", obj_note); + n.time = chart[spawned][0]; + n.lane = chart[spawned][1]; + n.owner = id; + spawned += 1; +} + +// A press judges the nearest unjudged note in that lane. +for (var l = 0; l < lanes; l++) { + if (!keyboard_check_pressed(lane_key[l])) continue; + var best = noone; + var best_d = 9999; + with (obj_note) { + if (lane != l || judged) continue; + var d = abs(time - other.song_time); + if (d < best_d) { best_d = d; best = id; } + } + if (best == noone || best_d > miss_window) continue; + best.judged = true; + hits += 1; + // Bump before scoring: `kernel_score_add` scales by the combo multiplier, + // so the hit that extends a run is the one that benefits from it. + ::kernel::kernel_combo_bump(); + ::kernel::kernel_score_add((best_d <= perfect_window) ? 100 : 50); + judgement = (best_d <= perfect_window) ? "PERFECT" : "GOOD"; + judgement_at = song_time; + audio_play_sound(snd_hit, 5, false); + instance_destroy(best); +} + +// A note the song has left behind is a miss. +with (obj_note) { + if (judged) continue; + if (other.song_time - time <= other.miss_window) continue; + judged = true; + other.misses += 1; + ::kernel::kernel_combo_break(); + other.judgement = "MISS"; + other.judgement_at = other.song_time; + instance_destroy(); +} + +if (!finished && song_time >= song_length - 0.02) { + finished = true; + global.rh_end_time = song_time; + ::kernel::kernel_game_over("song cleared"); +} + +global.rh_time = song_time; +global.rh_time_max = max(global.rh_time_max, song_time); +global.rh_hits = hits; +global.rh_misses = misses; +global.rh_score = ::kernel::kernel_score(); +global.rh_best_combo = ::kernel::kernel_combo_best(); +global.rh_finished = finished; +global.rh_notes_max = max(global.rh_notes_max, instance_number(obj_note)); diff --git a/prefabs/rhythm/objects/obj_rhythm/object.toml b/prefabs/rhythm/objects/obj_rhythm/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/rhythm/objects/obj_rhythm/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/rhythm/prefab.toml b/prefabs/rhythm/prefab.toml new file mode 100644 index 00000000..4d3747a0 --- /dev/null +++ b/prefabs/rhythm/prefab.toml @@ -0,0 +1,41 @@ +schema_version = 1 +name = "Rhythm" +description = "Notes fall to the beat and you hit them on the line — a whole rhythm game whose clock is the song itself." +category = "game" +tags = ["genre:rhythm", "capability:audio", "role:template"] + +install = "copy" +requires = ["kernel"] + +# The chart is the game. Adding, moving or removing a note is editing this +# array, and nothing else has to move with it. +seam = "objects/obj_rhythm/Create.gml" + +steps = [ + "`rhythm.gametest.json` came with the recipe: the song's transport runs, notes are spawned from it, a press on the beat scores, and the track clamps at its end. Run it with `gmx test rhythm.gametest.json --project .` before and after a change, and extend it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "It needs a runtime whose headless audio works, and until one ships that means `--dev-runtime`. On an older runtime every one of the eight asserts fails with `0.0`, because `audio_play_sound` returns a negative handle and `audio_sound_get_track_position` then reads 0 on every frame: no note is ever spawned and the HUD sits at `0.00 / 8.00s` for the whole run. That is the runtime, not the chart and not the test — a game built on it looks like it is working and simply never starts. This recipe is the regression test for it, so eight red asserts here are the failure being reported rather than a recipe to repair.", + "The song is the clock. `obj_rhythm`'s Step reads `audio_sound_get_track_position(voice)` once and every note position, judgement and ending is measured against that one number. Do not add a frame counter beside it: two clocks drift apart, and the one the player hears is the one that is right. A note's `y` is a pure function of `time - song_time`, so a note can never wander off its own beat.", + "The chart is the `chart` array in `obj_rhythm`'s Create: one `[seconds, lane]` pair per note, lanes 0-3 left to right on the arrow keys. It is written as a loop over beats because the shipped track is a metronome-steady 120 BPM, so beat `b` is at `b * beat` seconds; replace the loop with literal pairs the moment the music stops being a grid.", + "Swapping the music is `gmx new sound snd_song --source ` over the existing asset, and then two numbers have to follow it: `song_length` in Create must match the new track's duration, and `bpm` must match its tempo or the beat-indexed chart lands between beats. A chart written for one track means nothing against another.", + "Timing windows are `perfect_window`, `good_window` and `miss_window`, in seconds. They are seconds rather than frames on purpose — a window in frames says something different at 30fps than at 60, and the player's ear is measuring in seconds either way.", + "`snd_song` is `compression = \"uncompressed\"`. A track that decodes on first play starts late, and every note in the chart is then early by however long the decode took. Leave it uncompressed until the track is long enough that its size matters, and check the first note by ear after changing it.", + "The placeholder art is flat colour and swapping it is data, not code: point `sprite` at new art in `objects/obj_note/object.toml`. `spr_hitline` is drawn by `obj_rhythm`'s Draw event rather than being an object, so a wider playfield means changing the draw call and the `lane_x` array together.", +] + +traps = [ + "The kernel's pause is on by default and this game turns it off in `obj_rhythm`'s Create. It stops the game while the track keeps playing, so the song runs on behind a frozen screen and every note after it is off the beat — no error, and a chart that simply never lines up again. Anything else driven by audio rather than by frames has the same problem.", + "`song_length` and `bpm` are read from `[tuning]` and have to match the actual track. A chart is written in beats, so a wrong `bpm` lands every note between beats and a wrong `song_length` ends the song early or never — both look like a mistuned chart rather than a mismatched number.", +] + +[roles] +obj_rhythm = "owns the song, the chart, the judging and the HUD — the only clock in the game" +obj_note = "one note; its position is read from the song's playback position, never integrated" +rm_song = "the one room — a fixed screen, no views, so nothing has to track the camera" + +[tuning] +bpm = 120 +song_length = 8.0 +scroll_px = 300 +perfect_window = 0.05 +good_window = 0.15 +miss_window = 0.20 diff --git a/prefabs/rhythm/project.toml b/prefabs/rhythm/project.toml new file mode 100644 index 00000000..a7debe32 --- /dev/null +++ b/prefabs/rhythm/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "rhythm" + +[room_order] +order = ["rm_song"] diff --git a/prefabs/rhythm/rooms/rm_song/layers/Background.toml b/prefabs/rhythm/rooms/rm_song/layers/Background.toml new file mode 100644 index 00000000..38501277 --- /dev/null +++ b/prefabs/rhythm/rooms/rm_song/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 1000 + +[background] +colour = "#12141C" diff --git a/prefabs/rhythm/rooms/rm_song/layers/Instances.toml b/prefabs/rhythm/rooms/rm_song/layers/Instances.toml new file mode 100644 index 00000000..435d3d67 --- /dev/null +++ b/prefabs/rhythm/rooms/rm_song/layers/Instances.toml @@ -0,0 +1,17 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +# One controller. Notes are created onto this same layer as the song plays. +[[entries]] +id = "inst_rhythm" +object = "obj_rhythm" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_kernel" +object = "::kernel::obj_kernel" +x = 0.0 +y = 0.0 diff --git a/prefabs/rhythm/rooms/rm_song/room.toml b/prefabs/rhythm/rooms/rm_song/room.toml new file mode 100644 index 00000000..4ed1d69e --- /dev/null +++ b/prefabs/rhythm/rooms/rm_song/room.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#room + +# One screen, no views: the playfield is the whole room, so there is no camera +# to keep in step with the chart. +size = { width = 640, height = 480 } diff --git a/prefabs/rhythm/sounds/snd_hit/sound.toml b/prefabs/rhythm/sounds/snd_hit/sound.toml new file mode 100644 index 00000000..e1f87e3d --- /dev/null +++ b/prefabs/rhythm/sounds/snd_hit/sound.toml @@ -0,0 +1,5 @@ +#:schema ../../.gm-schema/v1.json#sound + +channel_format = "mono" +compression = "uncompressed" +preload = true diff --git a/prefabs/rhythm/sounds/snd_hit/source.wav b/prefabs/rhythm/sounds/snd_hit/source.wav new file mode 100644 index 00000000..c026589e Binary files /dev/null and b/prefabs/rhythm/sounds/snd_hit/source.wav differ diff --git a/prefabs/rhythm/sounds/snd_song/sound.toml b/prefabs/rhythm/sounds/snd_song/sound.toml new file mode 100644 index 00000000..16960748 --- /dev/null +++ b/prefabs/rhythm/sounds/snd_song/sound.toml @@ -0,0 +1,6 @@ +#:schema ../../.gm-schema/v1.json#sound + +channel_format = "mono" +# Uncompressed: the chart is read against `audio_sound_get_track_position`, and +# a decode that starts late shifts every note with it. +compression = "uncompressed" diff --git a/prefabs/rhythm/sounds/snd_song/source.wav b/prefabs/rhythm/sounds/snd_song/source.wav new file mode 100644 index 00000000..5e4a8d2e Binary files /dev/null and b/prefabs/rhythm/sounds/snd_song/source.wav differ diff --git a/prefabs/rhythm/sprites/spr_hitline/frame_000.png b/prefabs/rhythm/sprites/spr_hitline/frame_000.png new file mode 100644 index 00000000..992bbbf1 Binary files /dev/null and b/prefabs/rhythm/sprites/spr_hitline/frame_000.png differ diff --git a/prefabs/rhythm/sprites/spr_hitline/sprite.toml b/prefabs/rhythm/sprites/spr_hitline/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/rhythm/sprites/spr_hitline/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/rhythm/sprites/spr_note/frame_000.png b/prefabs/rhythm/sprites/spr_note/frame_000.png new file mode 100644 index 00000000..a266d3d5 Binary files /dev/null and b/prefabs/rhythm/sprites/spr_note/frame_000.png differ diff --git a/prefabs/rhythm/sprites/spr_note/sprite.toml b/prefabs/rhythm/sprites/spr_note/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/rhythm/sprites/spr_note/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/rhythm/thumbnail.png b/prefabs/rhythm/thumbnail.png new file mode 100644 index 00000000..a5304a16 Binary files /dev/null and b/prefabs/rhythm/thumbnail.png differ diff --git a/prefabs/rooms_as_state_machines/LICENSE b/prefabs/rooms_as_state_machines/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/rooms_as_state_machines/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/rooms_as_state_machines/README.md b/prefabs/rooms_as_state_machines/README.md new file mode 100644 index 00000000..c6f2bb28 --- /dev/null +++ b/prefabs/rooms_as_state_machines/README.md @@ -0,0 +1,25 @@ +# rooms_as_state_machines + +Title → game → results → loop. The skeleton almost every game starts from. + +## The decisions this makes for you + +**Rooms as states, not one room with a mode flag.** Rooms give you free +teardown: leaving a room destroys its instances, so state cannot leak +between screens. Use a single room with a state variable instead only when +the screens share heavy live state that is expensive to rebuild — a paused +3D scene behind a menu, say. + +**Every room has a Background layer.** This is not decoration. The layer +is what clears the surface each frame; without one, draw calls accumulate +visually and the screen smears. + +**State that must survive a transition goes in the kernel**, which is +persistent, or in a global. Instance variables do not survive +`room_goto` — that is the point of using rooms. + +## What is already done for you + +The run lifecycle, score, combo, save and the high score are +`::kernel::`. This prefab is the three rooms and the three controllers +that move between them; the machinery underneath is not yours to maintain. diff --git a/prefabs/rooms_as_state_machines/demo.gametest.json b/prefabs/rooms_as_state_machines/demo.gametest.json new file mode 100644 index 00000000..071d3871 --- /dev/null +++ b/prefabs/rooms_as_state_machines/demo.gametest.json @@ -0,0 +1,81 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 710, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_title=false; global.gt_game=false; global.gt_over=false;" + }, + "watch": { + "gml": "if (room == rm_title) global.gt_title = true; if (room == rm_game) global.gt_game = true; if (room == rm_gameover) global.gt_over = true;" + }, + "title": { + "gml": "return global.gt_title;" + }, + "game": { + "gml": "return global.gt_game;" + }, + "over": { + "gml": "return global.gt_over;" + } + }, + "timeline": [ + { + "frame": 30, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 33, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + }, + { + "frame": 700, + "assert": { + "id": "title", + "name": "the game opens on the title room", + "expect": true + } + }, + { + "frame": 700, + "assert": { + "id": "game", + "name": "confirm moves the title on to the game room", + "expect": true + } + }, + { + "frame": 700, + "assert": { + "id": "over", + "name": "the ten-second round ends in the gameover room", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 700, + "code": [ + "watch" + ] + } + ] +} diff --git a/prefabs/rooms_as_state_machines/objects/obj_game_controller/Create.gml b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Create.gml new file mode 100644 index 00000000..b65bbb0d --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Create.gml @@ -0,0 +1,2 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); diff --git a/prefabs/rooms_as_state_machines/objects/obj_game_controller/Draw_GUI.gml b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Draw_GUI.gml new file mode 100644 index 00000000..008c4d03 --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Draw_GUI.gml @@ -0,0 +1 @@ +::kernel::kernel_draw_text(16, 60, "your game goes here — SPACE scores, 10s ends it"); diff --git a/prefabs/rooms_as_state_machines/objects/obj_game_controller/Step.gml b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Step.gml new file mode 100644 index 00000000..f255185c --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_game_controller/Step.gml @@ -0,0 +1,10 @@ +if (!::kernel::kernel_playing()) exit; + +// Placeholder for your game. Replace everything below. +if (::kernel::kernel_action_pressed("confirm")) { + ::kernel::kernel_score_add(100); +} +if (::kernel::kernel_elapsed() > 10) { + ::kernel::kernel_game_over("time"); + room_goto(rm_gameover); +} diff --git a/prefabs/rooms_as_state_machines/objects/obj_game_controller/object.toml b/prefabs/rooms_as_state_machines/objects/obj_game_controller/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_game_controller/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Draw_GUI.gml b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Draw_GUI.gml new file mode 100644 index 00000000..4e92c345 --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Draw_GUI.gml @@ -0,0 +1,7 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 244, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 274, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 320, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Step.gml b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/object.toml b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_gameover_controller/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/rooms_as_state_machines/objects/obj_title_controller/Create.gml b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Create.gml new file mode 100644 index 00000000..10f64870 --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Create.gml @@ -0,0 +1,4 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_state_set(::kernel::kernel_states().title); diff --git a/prefabs/rooms_as_state_machines/objects/obj_title_controller/Draw_GUI.gml b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Draw_GUI.gml new file mode 100644 index 00000000..ee510e17 --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Draw_GUI.gml @@ -0,0 +1,5 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "YOUR GAME", c_white, fa_center); +::kernel::kernel_draw_text( + _w / 2, 260, + rooms_as_state_machines_tuning().title_prompt, c_ltgray, fa_center); diff --git a/prefabs/rooms_as_state_machines/objects/obj_title_controller/Step.gml b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Step.gml new file mode 100644 index 00000000..d3ad81a7 --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_title_controller/Step.gml @@ -0,0 +1,3 @@ +// Release rather than press: whatever key brought you here from the +// results screen is often still down on the frame this room starts. +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_game); diff --git a/prefabs/rooms_as_state_machines/objects/obj_title_controller/object.toml b/prefabs/rooms_as_state_machines/objects/obj_title_controller/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/rooms_as_state_machines/objects/obj_title_controller/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/rooms_as_state_machines/prefab.toml b/prefabs/rooms_as_state_machines/prefab.toml new file mode 100644 index 00000000..cf502940 --- /dev/null +++ b/prefabs/rooms_as_state_machines/prefab.toml @@ -0,0 +1,32 @@ +schema_version = 1 +name = "Rooms as state machines" +description = "Title → game → results → loop. The skeleton almost every game starts from." +category = "game" +tags = ["genre:structure", "role:template"] + +# A project: it owns project.toml and [room_order], so it cannot be merged +# into a game that already has them. +install = "copy" +requires = ["kernel"] + +seam = "objects/obj_game_controller/Step.gml" + +steps = [ + "`rooms_as_state_machines.gametest.json` came with the prefab and already passes: the game opens on the title room, confirm moves it on to the game room, and the ten-second round ends in the gameover room. Run it with `gmx test rooms_as_state_machines.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "Put your game in obj_game_controller, or replace it entirely — it is a placeholder that ends the run on Space.", + "Every room needs a Background layer. Without one the surface is never cleared and draw calls pile up visually.", +] +traps = [ + "Pausing is a kernel state, not a room. `::kernel::kernel_state_set(::kernel::kernel_states().pause)` is the whole of it: every pack gates its Step on `::kernel::kernel_playing()`, so the game stops without any pack knowing pause exists. Adding a pause room instead means the game keeps simulating behind it.", +] + +[roles] +obj_title_controller = "boots the kernel, then confirm goes to rm_game" +obj_game_controller = "the placeholder game — your loop replaces its Step" +obj_gameover_controller = "shows score and best; confirm returns to rm_title" +rm_title = "boot room — its size is the window size" +rm_game = "where the game is played" +rm_gameover = "results" + +[tuning] +title_prompt = "press SPACE to start" diff --git a/prefabs/rooms_as_state_machines/project.toml b/prefabs/rooms_as_state_machines/project.toml new file mode 100644 index 00000000..fb2a0c2b --- /dev/null +++ b/prefabs/rooms_as_state_machines/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "rooms_as_state_machines" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_game", "rm_gameover"] diff --git a/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Background.toml b/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Background.toml new file mode 100644 index 00000000..5d68eb9a --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Background.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer + +# Load-bearing: this clears the surface each frame. A room without a +# background layer accumulates every draw call visually. +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Instances.toml b/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Instances.toml new file mode 100644 index 00000000..8770a83c --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_game/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_game" +object = "obj_game_controller" +x = 0.0 +y = 0.0 diff --git a/prefabs/rooms_as_state_machines/rooms/rm_game/room.toml b/prefabs/rooms_as_state_machines/rooms/rm_game/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_game/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Background.toml b/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Background.toml new file mode 100644 index 00000000..5d68eb9a --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Background.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer + +# Load-bearing: this clears the surface each frame. A room without a +# background layer accumulates every draw call visually. +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Instances.toml b/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Instances.toml new file mode 100644 index 00000000..1851cea0 --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_gameover/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_over" +object = "obj_gameover_controller" +x = 0.0 +y = 0.0 diff --git a/prefabs/rooms_as_state_machines/rooms/rm_gameover/room.toml b/prefabs/rooms_as_state_machines/rooms/rm_gameover/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_gameover/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Background.toml b/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..5d68eb9a --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Background.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer + +# Load-bearing: this clears the surface each frame. A room without a +# background layer accumulates every draw call visually. +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Instances.toml b/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..d2eef0c3 --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,16 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_kernel" +object = "::kernel::obj_kernel" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_title" +object = "obj_title_controller" +x = 0.0 +y = 0.0 diff --git a/prefabs/rooms_as_state_machines/rooms/rm_title/room.toml b/prefabs/rooms_as_state_machines/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/rooms_as_state_machines/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/rooms_as_state_machines/thumbnail.png b/prefabs/rooms_as_state_machines/thumbnail.png new file mode 100644 index 00000000..e8822133 Binary files /dev/null and b/prefabs/rooms_as_state_machines/thumbnail.png differ diff --git a/prefabs/save_slots/LICENSE b/prefabs/save_slots/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/save_slots/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/save_slots/README.md b/prefabs/save_slots/README.md new file mode 100644 index 00000000..052d1ddc --- /dev/null +++ b/prefabs/save_slots/README.md @@ -0,0 +1,3 @@ +# save_slots + +Several saved games, written so a crash cannot destroy one. diff --git a/prefabs/save_slots/objects/obj_demo/Create.gml b/prefabs/save_slots/objects/obj_demo/Create.gml new file mode 100644 index 00000000..277a966f --- /dev/null +++ b/prefabs/save_slots/objects/obj_demo/Create.gml @@ -0,0 +1,2 @@ +note = ""; +level = 1; diff --git a/prefabs/save_slots/objects/obj_demo/Draw_GUI.gml b/prefabs/save_slots/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..a30ae176 --- /dev/null +++ b/prefabs/save_slots/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,15 @@ +draw_text(40, 30, "save-slots demo — UP/DOWN sets the level, 1-3 saves to a slot"); +draw_text(40, 50, "SPACE reads slot 0, BACKSPACE deletes every slot"); +draw_text(40, 90, "level to save: " + string(level)); + +// A slot-select screen reads summaries, not whole saves. +for (var i = 0; i < 3; i++) { + var _sum = save_slots_summary(i, ["level", "played"]); + var _y = 140 + i * 40; + draw_rectangle(40, _y, 460, _y + 30, true); + draw_text(52, _y + 6, _sum.used + ? "slot " + string(i) + " — level " + string(_sum.level) + + ", " + string(_sum.played) + "s" + : "slot " + string(i) + " — empty"); +} +draw_text(40, 300, note); diff --git a/prefabs/save_slots/objects/obj_demo/Step.gml b/prefabs/save_slots/objects/obj_demo/Step.gml new file mode 100644 index 00000000..4620ee19 --- /dev/null +++ b/prefabs/save_slots/objects/obj_demo/Step.gml @@ -0,0 +1,17 @@ +if (keyboard_check_pressed(vk_up)) level += 1; +if (keyboard_check_pressed(vk_down)) level = max(1, level - 1); + +for (var i = 0; i < 3; i++) { + if (keyboard_check_pressed(ord(string(i + 1)))) { + note = save_slots_write(i, { level: level, played: current_time div 1000 }) + ? "wrote slot " + string(i) : "slot " + string(i) + " would not write"; + } +} +if (keyboard_check_pressed(vk_space)) { + var _s = save_slots_read(0, { level: -1 }); + note = "slot 0 holds level " + string(_s.level); +} +if (keyboard_check_pressed(vk_backspace)) { + for (var i = 0; i < 3; i++) save_slots_delete(i); + note = "all cleared"; +} diff --git a/prefabs/save_slots/objects/obj_demo/object.toml b/prefabs/save_slots/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/save_slots/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/save_slots/prefab.toml b/prefabs/save_slots/prefab.toml new file mode 100644 index 00000000..5b76a291 --- /dev/null +++ b/prefabs/save_slots/prefab.toml @@ -0,0 +1,12 @@ +schema_version = 1 +name = "Save slots" + +# A library: installed under `prefabs//` in the target project and +# called as `::save_slots::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Numbered slots holding a struct each, written to a temporary file and moved into place so a crash mid-write cannot leave a save that exists, parses as nothing, and has replaced the good one. Reading a corrupt slot returns your fallback rather than throwing. Includes a summary read for a slot-select screen that wants a level and a play time without loading everything." +category = "library" +tags = ["capability:save", "capability:persistence", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/save_slots/project.toml b/prefabs/save_slots/project.toml new file mode 100644 index 00000000..c461c2da --- /dev/null +++ b/prefabs/save_slots/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "save-slots" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/save_slots/rooms/rm_demo/layers/Background.toml b/prefabs/save_slots/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/save_slots/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/save_slots/rooms/rm_demo/layers/Instances.toml b/prefabs/save_slots/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/save_slots/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/save_slots/rooms/rm_demo/room.toml b/prefabs/save_slots/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/save_slots/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/save_slots/scripts/scr_save_slots/scr_save_slots.gml b/prefabs/save_slots/scripts/scr_save_slots/scr_save_slots.gml new file mode 100644 index 00000000..bc2bec4d --- /dev/null +++ b/prefabs/save_slots/scripts/scr_save_slots/scr_save_slots.gml @@ -0,0 +1,111 @@ +/// Several saved games, and the screen that lists them. +/// +/// Writing to a temporary file and moving it into place is not caution for +/// its own sake: a crash partway through a write leaves a file that exists, +/// parses as nothing, and takes the save with it. The player loses hours to +/// a bug they will never be able to describe. + +/// @function save_slots_path(slot) +/// @description The filename a slot uses. +function save_slots_path(slot) { + return "save_" + string(slot) + ".json"; +} + +/// @function save_slots_exists(slot) +function save_slots_exists(slot) { + return file_exists(save_slots_path(slot)); +} + +/// @function save_slots_write(slot, data) +/// @description Write a struct to a slot. Returns whether it was written. +/// +/// Goes to a temporary file first and is moved into place once complete, so +/// a crash mid-write cannot leave a half-written save that parses as +/// nothing and destroys the old one with it. +function save_slots_write(slot, data) { + var _final = save_slots_path(slot); + var _temp = _final + ".part"; + var _f = file_text_open_write(_temp); + if (_f < 0) return false; + file_text_write_string(_f, json_stringify(data)); + file_text_close(_f); + + if (file_exists(_final)) file_delete(_final); + file_rename(_temp, _final); + return file_exists(_final); +} + +/// @function save_slots_read(slot, fallback) +/// @description The struct in a slot, or `fallback` when it is empty or +/// unreadable. A corrupt save starts a new game rather than ending the +/// session — which takes a guard, because the obvious `try`/`catch` around +/// `json_parse` does not work (see below). +function save_slots_read(slot, fallback) { + var _path = save_slots_path(slot); + if (!file_exists(_path)) return fallback; + + var _f = file_text_open_read(_path); + if (_f < 0) return fallback; + var _text = ""; + while (!file_text_eof(_f)) { + _text += file_text_read_string(_f); + file_text_readln(_f); + } + file_text_close(_f); + + // Checked *before* parsing, not caught after. `json_parse` on malformed + // input raises an error `try`/`catch` cannot intercept — measured on the + // runner: a plain `throw` is caught, and a bad `json_parse` in the same + // shape kills the game. So a guard is the only thing standing between a + // truncated save and a session that ends when the player loads it. + if (!save_slots_looks_like_json(_text)) return fallback; + + var _data = json_parse(_text); + return is_struct(_data) ? _data : fallback; +} + +/// @function save_slots_looks_like_json(text) +/// @description Whether `text` is structurally plausible as a JSON object: +/// non-empty, braced, and balanced outside of strings. +/// +/// Not a parser, and not trying to be — it exists to reject the corruption +/// that actually happens, which is a file truncated by a crash or a disk +/// filling up. Anything that gets past it is `json_parse`'s problem, and +/// `is_struct` catches the rest. +function save_slots_looks_like_json(text) { + var _t = string_trim(text); + var _n = string_length(_t); + if (_n < 2) return false; + if (string_char_at(_t, 1) != "{" || string_char_at(_t, _n) != "}") return false; + + var _depth = 0; + var _in_string = false; + var _escaped = false; + for (var i = 1; i <= _n; i++) { + var _c = string_char_at(_t, i); + if (_escaped) { _escaped = false; continue; } + if (_in_string) { + if (_c == "\\") _escaped = true; + else if (_c == "\"") _in_string = false; + continue; + } + switch (_c) { + case "\"": _in_string = true; break; + case "{": case "[": _depth += 1; break; + case "}": case "]": + _depth -= 1; + if (_depth < 0) return false; + break; + } + } + return _depth == 0 && !_in_string; +} + +/// @function save_slots_delete(slot) +function save_slots_delete(slot) { + var _path = save_slots_path(slot); + if (!file_exists(_path)) return false; + file_delete(_path); + return true; +} + diff --git a/prefabs/save_slots/scripts/scr_save_slots_summary/scr_save_slots_summary.gml b/prefabs/save_slots/scripts/scr_save_slots_summary/scr_save_slots_summary.gml new file mode 100644 index 00000000..f24faa79 --- /dev/null +++ b/prefabs/save_slots/scripts/scr_save_slots_summary/scr_save_slots_summary.gml @@ -0,0 +1,30 @@ +/// @function save_slots_summary(slot, keys) +/// @description The named fields of a slot as a struct, for a slot-select +/// screen that wants a level and a play time without loading the whole save. +/// Missing keys come back as `undefined`. +function save_slots_summary(slot, keys) { + var _data = save_slots_read(slot, undefined); + var _out = { used: _data != undefined }; + if (_data == undefined) return _out; + for (var i = 0; i < array_length(keys); i++) { + var _k = keys[i]; + _out[$ _k] = variable_struct_exists(_data, _k) ? _data[$ _k] : undefined; + } + return _out; +} + +/// @function save_slots_used(count) +/// @description Which of slots 0..count-1 hold a save, as an array of bools. +function save_slots_used(count) { + var _out = []; + for (var i = 0; i < count; i++) array_push(_out, save_slots_exists(i)); + return _out; +} + +/// @function save_slots_copy(from_slot, to_slot) +/// @description Duplicate a save — what a "branch here" or a backup does. +function save_slots_copy(from_slot, to_slot) { + var _data = save_slots_read(from_slot, undefined); + if (_data == undefined) return false; + return save_slots_write(to_slot, _data); +} diff --git a/prefabs/save_slots/thumbnail.png b/prefabs/save_slots/thumbnail.png new file mode 100644 index 00000000..c37139e8 Binary files /dev/null and b/prefabs/save_slots/thumbnail.png differ diff --git a/prefabs/snake/LICENSE b/prefabs/snake/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/snake/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/snake/README.md b/prefabs/snake/README.md new file mode 100644 index 00000000..e2d0f452 --- /dev/null +++ b/prefabs/snake/README.md @@ -0,0 +1,42 @@ +# snake + +Eat, grow, and do not bite yourself — one step at a time on a grid. + +## It moves on a tick, not on a frame + +The snake advances every `step_seconds`, not every frame. That is what makes +the game readable — a body that moved continuously would need sub-cell +positions and the collision would stop being exact. + +Turning is **buffered**: a key press records `next_dir`, and the tick applies +it. Without the buffer a player who taps twice between two ticks loses the +first input; with it, and without the reversal guard below, they turn back +into their own neck. + +```gml +if (keyboard_check_pressed(vk_left) && dir.col == 0) next_dir = { col: -1, row: 0 }; +``` + +The `dir.col == 0` test is the reversal guard: you may only turn onto an axis +you are not already travelling along. + +## Growing is not adding, it is failing to remove + +Every tick pushes a new head. Eating simply *skips* deleting the tail: + +```gml +array_insert(body, 0, _next); +if (ate) { ... } else { array_delete(body, array_length(body) - 1, 1); } +``` + +One branch, and the body length takes care of itself. + +## The difficulty curve is three numbers + +`step_seconds` falls by `step_gain` on every meal, never below `step_floor`. +That is the whole ramp — there is nothing else to tune. + +## What it does not do + +No walls inside the board, no wrap-around edges, no second snake. Wrapping is +a two-line change in the bounds check if you prefer it to dying. diff --git a/prefabs/snake/demo.gametest.json b/prefabs/snake/demo.gametest.json new file mode 100644 index 00000000..dd8e92bd --- /dev/null +++ b/prefabs/snake/demo.gametest.json @@ -0,0 +1,139 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 160, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen = false; global.gt_head0 = undefined; global.gt_moved = false; global.gt_turned = false; global.gt_len = 0; global.gt_ended = false;" + }, + "watch": { + "gml": "if (room == rm_results) global.gt_ended = true; if (instance_exists(obj_snake)) { with (obj_snake) { global.gt_seen = true; var _h = body[0]; if (global.gt_head0 == undefined) global.gt_head0 = { col: _h.col, row: _h.row }; if (_h.col != global.gt_head0.col || _h.row != global.gt_head0.row) global.gt_moved = true; if (dir.row == -1) global.gt_turned = true; global.gt_len = max(global.gt_len, array_length(body)); } }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "moved": { + "gml": "return global.gt_moved;" + }, + "turned": { + "gml": "return global.gt_turned;" + }, + "has_body": { + "gml": "return global.gt_len >= 3;" + }, + "alive": { + "gml": "return !global.gt_ended;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_up", + "state": "down" + } + ] + }, + { + "frame": 23, + "inputs": [ + { + "key": "vk_up", + "state": "up" + } + ] + }, + { + "frame": 45, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 48, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 150, + "assert": { + "id": "seen", + "name": "obj_snake runs in rm_play", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "has_body", + "name": "the snake has a body of at least three cells", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "moved", + "name": "the snake advances a cell on its own tick", + "expect": true + } + }, + { + "frame": 150, + "assert": { + "id": "turned", + "name": "pressing up turns the snake upward", + "expect": true + } + }, + { + "frame": 60, + "assert": { + "id": "alive", + "name": "it is still playing a second in \u2014 collision has not killed it at once", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 148, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/snake/objects/obj_results/Draw_GUI.gml b/prefabs/snake/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/snake/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/snake/objects/obj_results/Step.gml b/prefabs/snake/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/snake/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/snake/objects/obj_results/object.toml b/prefabs/snake/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/snake/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/snake/objects/obj_snake/Create.gml b/prefabs/snake/objects/obj_snake/Create.gml new file mode 100644 index 00000000..1b8dcce7 --- /dev/null +++ b/prefabs/snake/objects/obj_snake/Create.gml @@ -0,0 +1,28 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +cols = ::kernel::kernel_tuning("cols", 24); +rows = ::kernel::kernel_tuning("rows", 16); +cell = ::kernel::kernel_tuning("cell", 30); +board = ::grid::grid_make(cols, rows, cell, cell, + (room_width - cols * cell) / 2, (room_height - rows * cell) / 2); + +// The body is a list of cells, head first. Growing is not removing the tail. +body = []; +for (var i = 0; i < ::kernel::kernel_tuning("start_length", 4); i++) { + array_push(body, { col: 8 - i, row: 8 }); +} +dir = { col: 1, row: 0 }; +// Buffered so a fast double-tap cannot reverse you into your own neck +// between two ticks. +next_dir = { col: 1, row: 0 }; + +tick = ::kernel::kernel_tuning("step_seconds", 0.16); +clock = tick; +food = { col: 16, row: 8 }; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +food_score = ::kernel::kernel_tuning("food_score", 10); +step_floor = ::kernel::kernel_tuning("step_floor", 0.06); +step_gain = ::kernel::kernel_tuning("step_gain", 0.004); diff --git a/prefabs/snake/objects/obj_snake/Draw.gml b/prefabs/snake/objects/obj_snake/Draw.gml new file mode 100644 index 00000000..b6205a1f --- /dev/null +++ b/prefabs/snake/objects/obj_snake/Draw.gml @@ -0,0 +1,13 @@ +var _f = snake_cell_centre(board, food.col, food.row); +draw_set_colour(c_yellow); +draw_circle(_f.x, _f.y, cell * 0.32, false); + +draw_set_colour(c_lime); +for (var i = 0; i < array_length(body); i++) { + var _p = snake_cell_centre(board, body[i].col, body[i].row); + var _r = cell * (i == 0 ? 0.46 : 0.38); + draw_rectangle(_p.x - _r, _p.y - _r, _p.x + _r, _p.y + _r, false); +} +draw_set_colour(c_white); +draw_rectangle(board.ox, board.oy, + board.ox + cols * cell, board.oy + rows * cell, true); diff --git a/prefabs/snake/objects/obj_snake/Draw_GUI.gml b/prefabs/snake/objects/obj_snake/Draw_GUI.gml new file mode 100644 index 00000000..32e986d2 --- /dev/null +++ b/prefabs/snake/objects/obj_snake/Draw_GUI.gml @@ -0,0 +1,2 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, "length " + string(array_length(body))); diff --git a/prefabs/snake/objects/obj_snake/Step.gml b/prefabs/snake/objects/obj_snake/Step.gml new file mode 100644 index 00000000..9c52008c --- /dev/null +++ b/prefabs/snake/objects/obj_snake/Step.gml @@ -0,0 +1,42 @@ +if (!::kernel::kernel_playing()) exit; +::kernel::kernel_tick(); + +// Turning is recorded now and applied on the next tick. A reversal onto +// your own neck is rejected outright. +if (keyboard_check_pressed(vk_left) && dir.col == 0) next_dir = { col: -1, row: 0 }; +if (keyboard_check_pressed(vk_right) && dir.col == 0) next_dir = { col: 1, row: 0 }; +if (keyboard_check_pressed(vk_up) && dir.row == 0) next_dir = { col: 0, row: -1 }; +if (keyboard_check_pressed(vk_down) && dir.row == 0) next_dir = { col: 0, row: 1 }; + +clock -= delta_time / 1000000; +if (clock > 0) exit; +clock += tick; +dir = next_dir; + +var _head = body[0]; +var _next = { col: _head.col + dir.col, row: _head.row + dir.row }; + +// The wall and yourself are the same ending. +if (_next.col < 0 || _next.row < 0 || _next.col >= cols || _next.row >= rows) { + snake_end(); + exit; +} +for (var i = 0; i < array_length(body); i++) { + if (body[i].col == _next.col && body[i].row == _next.row) { + snake_end(); + exit; + } +} + +array_insert(body, 0, _next); +if (_next.col == food.col && _next.row == food.row) { + ::kernel::kernel_score_add(food_score); + var _p = snake_cell_centre(board, food.col, food.row); + ::feel::feel_pop(_p.x, _p.y, "+" + string(food_score), c_yellow); + tick = max(step_floor, + tick - step_gain); + food = snake_place_food(); +} else { + // Not eating means dropping the tail — which is what makes eating grow. + array_delete(body, array_length(body) - 1, 1); +} diff --git a/prefabs/snake/objects/obj_snake/object.toml b/prefabs/snake/objects/obj_snake/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/snake/objects/obj_snake/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/snake/objects/obj_title/Create.gml b/prefabs/snake/objects/obj_title/Create.gml new file mode 100644 index 00000000..35faf863 --- /dev/null +++ b/prefabs/snake/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(snake_data, snake_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/snake/objects/obj_title/Draw_GUI.gml b/prefabs/snake/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..962b2cac --- /dev/null +++ b/prefabs/snake/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "SNAKE", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "eat, grow, do not bite yourself", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/snake/objects/obj_title/Step.gml b/prefabs/snake/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/snake/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/snake/objects/obj_title/object.toml b/prefabs/snake/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/snake/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/snake/prefab.toml b/prefabs/snake/prefab.toml new file mode 100644 index 00000000..47f63329 --- /dev/null +++ b/prefabs/snake/prefab.toml @@ -0,0 +1,37 @@ +schema_version = 1 +name = "Snake" +description = "Eat, grow, and do not bite yourself — one step at a time on a grid." +category = "game" +tags = ["genre:arcade", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "grid"] + +seam = "scripts/scr_snake_data/scr_snake_data.gml" + +steps = [ + "`snake.gametest.json` came with the prefab and already passes: the snake runs in rm_play, has a body, advances a cell on its own tick, turns when you press a direction, and survives an open path. Run it with `gmx test snake.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "`step_seconds` is the whole difficulty curve — it shrinks each time you eat, down to `step_floor`.", + "The snake moves on a fixed tick rather than every frame. Turning is buffered until the next tick, so a fast double-tap cannot reverse you into your own neck.", +] +traps = [ + "`obj_snake` is a controller, not the snake. Its own `x`/`y` stay at 0 for the whole run: the snake is `body`, an array of `{ col, row }` cells with the head first, drawn from the grid. Anything reading `obj_snake.x` to find the snake is reading 0.", +] + +[roles] +obj_snake = "the whole game: the body, the tick, the food and the collision" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "length reached, and the best so far" + +[tuning] +cols = 24 +rows = 16 +cell = 30 +# Seconds between steps. Falls by `step_gain` each time you eat, never below +# `step_floor` — the entire difficulty curve is these three numbers. +step_seconds = 0.16 +step_gain = 0.004 +step_floor = 0.06 +food_score = 10 +start_length = 4 diff --git a/prefabs/snake/project.toml b/prefabs/snake/project.toml new file mode 100644 index 00000000..1dd1fbc3 --- /dev/null +++ b/prefabs/snake/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "snake" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/snake/rooms/rm_play/layers/Background.toml b/prefabs/snake/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/snake/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/snake/rooms/rm_play/layers/Instances.toml b/prefabs/snake/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..eee5481a --- /dev/null +++ b/prefabs/snake/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_snake" +object = "obj_snake" +x = 0.0 +y = 0.0 diff --git a/prefabs/snake/rooms/rm_play/room.toml b/prefabs/snake/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/snake/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/snake/rooms/rm_results/layers/Background.toml b/prefabs/snake/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/snake/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/snake/rooms/rm_results/layers/Instances.toml b/prefabs/snake/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/snake/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/snake/rooms/rm_results/room.toml b/prefabs/snake/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/snake/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/snake/rooms/rm_title/layers/Background.toml b/prefabs/snake/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/snake/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/snake/rooms/rm_title/layers/Instances.toml b/prefabs/snake/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/snake/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/snake/rooms/rm_title/room.toml b/prefabs/snake/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/snake/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/snake/scripts/scr_snake_play/scr_snake_play.gml b/prefabs/snake/scripts/scr_snake_play/scr_snake_play.gml new file mode 100644 index 00000000..1dc9e5d8 --- /dev/null +++ b/prefabs/snake/scripts/scr_snake_play/scr_snake_play.gml @@ -0,0 +1,36 @@ +/// @function snake_cell_centre(board, col, row) +/// @description The middle of a cell. `grid_cell_to_px` gives the top-left +/// corner, and drawing a square body from a corner puts the snake half a +/// cell up and left of the food it is eating. +function snake_cell_centre(board, col, row) { + var _p = ::grid::grid_cell_to_px(board, col, row); + return { x: _p.x + board.cell_w / 2, y: _p.y + board.cell_h / 2 }; +} + +/// @function snake_place_food() +/// @description A free cell for the next food. Rejects cells the body +/// occupies by resampling, which is fine while the board is mostly empty +/// and is the reason a nearly-full board can take a moment. +function snake_place_food() { + var _tries = 0; + while (_tries < 400) { + var _c = irandom(cols - 1); + var _r = irandom(rows - 1); + var _clear = true; + for (var i = 0; i < array_length(body); i++) { + if (body[i].col == _c && body[i].row == _r) { _clear = false; break; } + } + if (_clear) return { col: _c, row: _r }; + _tries += 1; + } + return { col: 0, row: 0 }; +} + +/// @function snake_end() +/// @description Bank the score and show the results. One place, because +/// hitting a wall and biting yourself are the same ending. +function snake_end() { + ::kernel::kernel_game_over("bitten"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); +} diff --git a/prefabs/snake/thumbnail.png b/prefabs/snake/thumbnail.png new file mode 100644 index 00000000..48eca917 Binary files /dev/null and b/prefabs/snake/thumbnail.png differ diff --git a/prefabs/sokoban/LICENSE b/prefabs/sokoban/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/sokoban/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/sokoban/README.md b/prefabs/sokoban/README.md new file mode 100644 index 00000000..6c5ac6c3 --- /dev/null +++ b/prefabs/sokoban/README.md @@ -0,0 +1,38 @@ +# sokoban + +Push every crate onto a mark. You can push, never pull, so a mistake is permanent. + +## Undo is a mechanic, not a convenience + +Pushing is irreversible: a crate in a corner stays there. A Sokoban without +undo is one you restart from the beginning on every mistake, which is why +nobody finishes it. + +Undo restores a **whole board snapshot** rather than trying to reverse the +last push. That stays correct for a move that both walked and pushed, where +reversing would have to undo two things in the right order. + +The snapshot copies row by row. Assigning the arrays would share them, and the +undo would then mutate along with the board it is meant to restore — a bug +that looks like undo simply not working. + +## Marks are stored separately from crates + +A crate standing on a mark must not erase it, or the level becomes unwinnable +the moment you push the first crate into place and the mark disappears. Two +parallel grids, one for what is in a cell and one for what the cell is. + +`soko_solved` counts marks rather than crates, so a level with a spare crate +still completes. + +## Levels are text + +`content.levels` is an array of string blocks — `#` wall, `$` crate, `.` mark, +`@` start, `*` a crate already on a mark. Adding a level is adding a block. +Nothing else changes. + +## What it does not do + +No move limit, no par score, no level select — levels run in order and the +game ends after the last. Score is negative per move and positive per level, +which is a placeholder for whatever scoring you actually want. diff --git a/prefabs/sokoban/demo.gametest.json b/prefabs/sokoban/demo.gametest.json new file mode 100644 index 00000000..df5dac8b --- /dev/null +++ b/prefabs/sokoban/demo.gametest.json @@ -0,0 +1,193 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 210, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_p0=undefined; global.gt_moved=false; global.gt_hist_max=0; global.gt_hist_now=0; global.gt_crates=0;" + }, + "watch": { + "gml": "if (instance_exists(obj_sokoban)) { with (obj_sokoban) { global.gt_seen = true; if (global.gt_p0 == undefined) global.gt_p0 = { c: board.px, r: board.py }; if (board.px != global.gt_p0.c || board.py != global.gt_p0.r) global.gt_moved = true; global.gt_hist_max = max(global.gt_hist_max, array_length(history)); global.gt_hist_now = array_length(history); var _n = 0; for (var _r = 0; _r < array_length(board.cells); _r++) { var _row = board.cells[_r]; for (var _c = 0; _c < array_length(_row); _c++) if (_row[_c] == \"$\") _n += 1; } global.gt_crates = max(global.gt_crates, _n); } }" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "crates": { + "gml": "return global.gt_crates > 0;" + }, + "moved": { + "gml": "return global.gt_moved;" + }, + "recorded": { + "gml": "return global.gt_hist_max > 0;" + }, + "undone": { + "gml": "return global.gt_hist_now < global.gt_hist_max;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 23, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 40, + "inputs": [ + { + "key": "vk_down", + "state": "down" + } + ] + }, + { + "frame": 43, + "inputs": [ + { + "key": "vk_down", + "state": "up" + } + ] + }, + { + "frame": 60, + "inputs": [ + { + "key": "vk_left", + "state": "down" + } + ] + }, + { + "frame": 63, + "inputs": [ + { + "key": "vk_left", + "state": "up" + } + ] + }, + { + "frame": 80, + "inputs": [ + { + "key": "vk_up", + "state": "down" + } + ] + }, + { + "frame": 83, + "inputs": [ + { + "key": "vk_up", + "state": "up" + } + ] + }, + { + "frame": 140, + "inputs": [ + { + "key": 90, + "state": "down" + } + ] + }, + { + "frame": 143, + "inputs": [ + { + "key": 90, + "state": "up" + } + ] + }, + { + "frame": 200, + "assert": { + "id": "seen", + "name": "the sokoban controller runs in rm_play", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "crates", + "name": "the level loads with crates to push", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "moved", + "name": "a direction key walks the pusher to another cell", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "recorded", + "name": "a real move is pushed onto the undo history", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "undone", + "name": "Z pops a move back off the history", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 2, + "until": 199, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/sokoban/objects/obj_block/object.toml b/prefabs/sokoban/objects/obj_block/object.toml new file mode 100644 index 00000000..a8bacdc5 --- /dev/null +++ b/prefabs/sokoban/objects/obj_block/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_block" diff --git a/prefabs/sokoban/objects/obj_crate/object.toml b/prefabs/sokoban/objects/obj_crate/object.toml new file mode 100644 index 00000000..41f060fc --- /dev/null +++ b/prefabs/sokoban/objects/obj_crate/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_crate" diff --git a/prefabs/sokoban/objects/obj_pusher/object.toml b/prefabs/sokoban/objects/obj_pusher/object.toml new file mode 100644 index 00000000..e7c4932d --- /dev/null +++ b/prefabs/sokoban/objects/obj_pusher/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_pusher" diff --git a/prefabs/sokoban/objects/obj_results/Draw_GUI.gml b/prefabs/sokoban/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..183db496 --- /dev/null +++ b/prefabs/sokoban/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, ::kernel::kernel_result() == "cleared" + ? "CLEARED" : "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 250, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 285, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 350, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/sokoban/objects/obj_results/Step.gml b/prefabs/sokoban/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/sokoban/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/sokoban/objects/obj_results/object.toml b/prefabs/sokoban/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/sokoban/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/sokoban/objects/obj_sokoban/Create.gml b/prefabs/sokoban/objects/obj_sokoban/Create.gml new file mode 100644 index 00000000..e256fea9 --- /dev/null +++ b/prefabs/sokoban/objects/obj_sokoban/Create.gml @@ -0,0 +1,13 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); + +cell = ::kernel::kernel_tuning("cell", 40); +levels = ::kernel::kernel_data("levels"); +level_at = 0; +board = soko_load(levels[level_at]); +history = []; + +// Tuning read once here rather than every frame: one visible block +// of every knob this object answers to, and no struct lookup in Step. +move_score = ::kernel::kernel_tuning("move_score", -1); +level_score = ::kernel::kernel_tuning("level_score", 500); diff --git a/prefabs/sokoban/objects/obj_sokoban/Draw.gml b/prefabs/sokoban/objects/obj_sokoban/Draw.gml new file mode 100644 index 00000000..8a1d7076 --- /dev/null +++ b/prefabs/sokoban/objects/obj_sokoban/Draw.gml @@ -0,0 +1,25 @@ +var _ox = (room_width - array_length(board.cells[0]) * cell) / 2; +var _oy = (room_height - array_length(board.cells) * cell) / 2; + +for (var r = 0; r < array_length(board.cells); r++) { + for (var c = 0; c < array_length(board.cells[r]); c++) { + var _x = _ox + c * cell + cell / 2; + var _y = _oy + r * cell + cell / 2; + + if (soko_marked(board, c, r)) { + draw_set_colour(c_olive); + draw_circle(_x, _y, cell * 0.18, false); + draw_set_colour(c_white); + } + var _what = soko_at(board, c, r); + if (_what == "#") draw_sprite(spr_block, 0, _x, _y); + // A crate on its mark is drawn differently, or the player cannot + // see how close they are without counting. + else if (_what == "$") { + draw_sprite_ext(spr_crate, 0, _x, _y, 1, 1, 0, + soko_marked(board, c, r) ? c_lime : c_white, 1); + } + } +} +draw_sprite(spr_pusher, 0, + _ox + board.px * cell + cell / 2, _oy + board.py * cell + cell / 2); diff --git a/prefabs/sokoban/objects/obj_sokoban/Draw_GUI.gml b/prefabs/sokoban/objects/obj_sokoban/Draw_GUI.gml new file mode 100644 index 00000000..3b057e1d --- /dev/null +++ b/prefabs/sokoban/objects/obj_sokoban/Draw_GUI.gml @@ -0,0 +1,5 @@ +::kernel::kernel_draw_text(16, 16, + "level " + string(level_at + 1) + " of " + string(array_length(levels))); +::kernel::kernel_draw_text(16, 40, "moves " + string(board.moves)); +::kernel::kernel_draw_text(16, 64, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 96, "Z undo R restart"); diff --git a/prefabs/sokoban/objects/obj_sokoban/Step.gml b/prefabs/sokoban/objects/obj_sokoban/Step.gml new file mode 100644 index 00000000..1225637c --- /dev/null +++ b/prefabs/sokoban/objects/obj_sokoban/Step.gml @@ -0,0 +1,45 @@ +if (!::kernel::kernel_playing()) exit; + +var _dx = keyboard_check_pressed(vk_right) - keyboard_check_pressed(vk_left); +var _dy = keyboard_check_pressed(vk_down) - keyboard_check_pressed(vk_up); + +// One axis per move. Pressing Right and Down on the same frame used to issue +// `soko_move(board, 1, 1)`, which cuts the corner between two walls and can +// push a crate diagonally — a move the rules forbid, and one that either +// "solves" an unsolvable level or wedges a crate where undo cannot reach. +if (_dx != 0 && _dy != 0) _dy = 0; + +if (_dx != 0 || _dy != 0) { + // Snapshot before, keep it only if something actually moved — so a + // walk into a wall does not fill the undo stack with nothing. + var _before = soko_snapshot(board); + if (soko_move(board, _dx, _dy)) { + array_push(history, _before); + ::kernel::kernel_score_add(move_score); + } +} + +// Undo restores a whole board rather than reversing a push, which stays +// correct for a move that both walked and pushed. +if (keyboard_check_pressed(ord("Z")) && array_length(history) > 0) { + soko_restore(board, history[array_length(history) - 1]); + array_delete(history, array_length(history) - 1, 1); +} +if (keyboard_check_pressed(ord("R"))) { + board = soko_load(levels[level_at]); + history = []; +} + +if (soko_solved(board)) { + ::kernel::kernel_score_add(level_score); + ::feel::feel_shake(0.2, 6); + level_at += 1; + if (level_at >= array_length(levels)) { + ::kernel::kernel_game_over("cleared"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); + exit; + } + board = soko_load(levels[level_at]); + history = []; +} diff --git a/prefabs/sokoban/objects/obj_sokoban/object.toml b/prefabs/sokoban/objects/obj_sokoban/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/sokoban/objects/obj_sokoban/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/sokoban/objects/obj_title/Create.gml b/prefabs/sokoban/objects/obj_title/Create.gml new file mode 100644 index 00000000..4d6c7c2a --- /dev/null +++ b/prefabs/sokoban/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(sokoban_data, sokoban_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); +::kernel::kernel_hud_visible(false); diff --git a/prefabs/sokoban/objects/obj_title/Draw_GUI.gml b/prefabs/sokoban/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..93ec5752 --- /dev/null +++ b/prefabs/sokoban/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "SOKOBAN", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "push every crate onto a mark", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 330, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/sokoban/objects/obj_title/Step.gml b/prefabs/sokoban/objects/obj_title/Step.gml new file mode 100644 index 00000000..abd0f938 --- /dev/null +++ b/prefabs/sokoban/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_play); diff --git a/prefabs/sokoban/objects/obj_title/object.toml b/prefabs/sokoban/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/sokoban/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/sokoban/prefab.toml b/prefabs/sokoban/prefab.toml new file mode 100644 index 00000000..d051fdc7 --- /dev/null +++ b/prefabs/sokoban/prefab.toml @@ -0,0 +1,56 @@ +schema_version = 1 +name = "Sokoban" +description = "Push every crate onto a mark. You can push, never pull, so a mistake is permanent." +category = "game" +tags = ["genre:puzzle", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "tilemap"] + +seam = "scripts/scr_sokoban_data/scr_sokoban_data.gml" + +steps = [ + "`sokoban.gametest.json` came with the prefab and already passes: the controller runs in rm_play, the level loads with crates, a direction key walks the pusher, a real move lands on the undo history, and Z pops it back off. Run it with `gmx test sokoban.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "Levels are text maps — `#` wall, `$` crate, `.` mark, `@` start, `*` a crate already on a mark. Add a string block to `levels` and there is another level.", + "Undo is not a convenience here. Pushing is irreversible, so a puzzle without undo is a puzzle you restart from the beginning on every mistake — which is why nobody finishes it.", +] + +[roles] +obj_sokoban = "builds the level, moves everything, tracks undo and completion" +obj_pusher = "drawn at the player's cell; the controller does the moving" +obj_crate = "pushed one cell at a time, never pulled" +obj_block = "a wall" +rm_title = "boot room — its size is the window size" +rm_play = "the whole game" +rm_results = "levels cleared and moves taken" + +[tuning] +cell = 40 +move_score = -1 +level_score = 500 + +# `#` wall, `$` crate, `.` mark, `@` start, `*` crate already on a mark. +# Add a block and there is another level. +[content] +levels = [ + [ + " #####", + "### #", + "#.@$ #", + "### .$#", + "#.##$ #", + "# # . ##", + "#$ *$$.#", + "# . #", + "########", + ], + [ + "########", + "# #", + "# .$@$.#", + "# ## #", + "# .$ $.#", + "# #", + "########", + ], +] diff --git a/prefabs/sokoban/project.toml b/prefabs/sokoban/project.toml new file mode 100644 index 00000000..092964bc --- /dev/null +++ b/prefabs/sokoban/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "sokoban" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_play", "rm_results"] diff --git a/prefabs/sokoban/rooms/rm_play/layers/Background.toml b/prefabs/sokoban/rooms/rm_play/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/sokoban/rooms/rm_play/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/sokoban/rooms/rm_play/layers/Instances.toml b/prefabs/sokoban/rooms/rm_play/layers/Instances.toml new file mode 100644 index 00000000..dd81b9e0 --- /dev/null +++ b/prefabs/sokoban/rooms/rm_play/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_sokoban_0" +object = "obj_sokoban" +x = 0.0 +y = 0.0 diff --git a/prefabs/sokoban/rooms/rm_play/room.toml b/prefabs/sokoban/rooms/rm_play/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/sokoban/rooms/rm_play/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/sokoban/rooms/rm_results/layers/Background.toml b/prefabs/sokoban/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/sokoban/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/sokoban/rooms/rm_results/layers/Instances.toml b/prefabs/sokoban/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..4b9f4d4d --- /dev/null +++ b/prefabs/sokoban/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_results_0" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/sokoban/rooms/rm_results/room.toml b/prefabs/sokoban/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/sokoban/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/sokoban/rooms/rm_title/layers/Background.toml b/prefabs/sokoban/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/sokoban/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/sokoban/rooms/rm_title/layers/Instances.toml b/prefabs/sokoban/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..000509ed --- /dev/null +++ b/prefabs/sokoban/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_obj_title_0" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/sokoban/rooms/rm_title/room.toml b/prefabs/sokoban/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/sokoban/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/sokoban/scripts/scr_sokoban_play/scr_sokoban_play.gml b/prefabs/sokoban/scripts/scr_sokoban_play/scr_sokoban_play.gml new file mode 100644 index 00000000..646423de --- /dev/null +++ b/prefabs/sokoban/scripts/scr_sokoban_play/scr_sokoban_play.gml @@ -0,0 +1,116 @@ +/// The rules of pushing, kept out of the input handling. +/// +/// State is a grid of characters rather than instances, because undo has to +/// restore a whole board and copying an array is one line where re-creating +/// instances is a source of bugs. + +/// @function soko_load(rows) +/// @description Turn a level's strings into a mutable board. Marks are held +/// separately from crates, so a crate standing on a mark does not erase it — +/// which is the bug that makes a level unwinnable after one push. +function soko_load(rows) { + var _cells = []; + var _marks = []; + var _px = 0; + var _py = 0; + for (var _r = 0; _r < array_length(rows); _r++) { + var _line = rows[_r]; + var _crow = []; + var _mrow = []; + for (var _c = 1; _c <= string_length(_line); _c++) { + var _ch = string_char_at(_line, _c); + array_push(_mrow, (_ch == "." || _ch == "*")); + if (_ch == "#") array_push(_crow, "#"); + else if (_ch == "$" || _ch == "*") array_push(_crow, "$"); + else array_push(_crow, " "); + if (_ch == "@") { _px = _c - 1; _py = _r; } + } + array_push(_cells, _crow); + array_push(_marks, _mrow); + } + return { cells: _cells, marks: _marks, px: _px, py: _py, moves: 0 }; +} + +/// @function soko_at(board, col, row) +/// @description What is in a cell, or "#" outside the board — so every +/// caller can treat off-board as solid without a bounds check. +function soko_at(board, col, row) { + if (row < 0 || row >= array_length(board.cells)) return "#"; + var _row = board.cells[row]; + if (col < 0 || col >= array_length(_row)) return "#"; + return _row[col]; +} + +/// @function soko_marked(board, col, row) +function soko_marked(board, col, row) { + if (row < 0 || row >= array_length(board.marks)) return false; + var _row = board.marks[row]; + if (col < 0 || col >= array_length(_row)) return false; + return _row[col]; +} + +/// @function soko_snapshot(board) +/// @description A copy of everything a move can change. Undo restores this +/// wholesale rather than trying to reverse a push, which is the version that +/// stays correct when a move both walks and pushes. +function soko_snapshot(board) { + var _copy = []; + for (var r = 0; r < array_length(board.cells); r++) { + // Row by row: assigning the array would share it, and the undo + // would then mutate along with the board it is meant to restore. + var _row = []; + for (var c = 0; c < array_length(board.cells[r]); c++) { + array_push(_row, board.cells[r][c]); + } + array_push(_copy, _row); + } + return { cells: _copy, px: board.px, py: board.py, moves: board.moves }; +} + +/// @function soko_restore(board, snap) +function soko_restore(board, snap) { + board.cells = snap.cells; + board.px = snap.px; + board.py = snap.py; + board.moves = snap.moves; +} + +/// @function soko_move(board, dx, dy) +/// @description Walk one cell, pushing a crate if one is in the way and the +/// cell beyond it is free. Returns whether anything moved, so the caller +/// only snapshots and counts a move that actually happened. +/// +/// A crate is never pulled. That asymmetry is the entire puzzle. +function soko_move(board, dx, dy) { + var _nx = board.px + dx; + var _ny = board.py + dy; + var _target = soko_at(board, _nx, _ny); + + if (_target == "#") return false; + + if (_target == "$") { + var _bx = _nx + dx; + var _by = _ny + dy; + // A crate cannot be pushed into a wall or into another crate. + if (soko_at(board, _bx, _by) != " ") return false; + board.cells[_ny][_nx] = " "; + board.cells[_by][_bx] = "$"; + } + + board.px = _nx; + board.py = _ny; + board.moves += 1; + return true; +} + +/// @function soko_solved(board) +/// @description Whether every mark has a crate on it. Counting marks rather +/// than crates means a level with a spare crate still completes. +function soko_solved(board) { + for (var r = 0; r < array_length(board.marks); r++) { + for (var c = 0; c < array_length(board.marks[r]); c++) { + if (board.marks[r][c] && soko_at(board, c, r) != "$") return false; + } + } + return true; +} diff --git a/prefabs/sokoban/sprites/spr_block/frame_000.png b/prefabs/sokoban/sprites/spr_block/frame_000.png new file mode 100644 index 00000000..a36af715 Binary files /dev/null and b/prefabs/sokoban/sprites/spr_block/frame_000.png differ diff --git a/prefabs/sokoban/sprites/spr_block/sprite.toml b/prefabs/sokoban/sprites/spr_block/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/sokoban/sprites/spr_block/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/sokoban/sprites/spr_crate/frame_000.png b/prefabs/sokoban/sprites/spr_crate/frame_000.png new file mode 100644 index 00000000..7a7549de Binary files /dev/null and b/prefabs/sokoban/sprites/spr_crate/frame_000.png differ diff --git a/prefabs/sokoban/sprites/spr_crate/sprite.toml b/prefabs/sokoban/sprites/spr_crate/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/sokoban/sprites/spr_crate/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/sokoban/sprites/spr_pusher/frame_000.png b/prefabs/sokoban/sprites/spr_pusher/frame_000.png new file mode 100644 index 00000000..221e050a Binary files /dev/null and b/prefabs/sokoban/sprites/spr_pusher/frame_000.png differ diff --git a/prefabs/sokoban/sprites/spr_pusher/sprite.toml b/prefabs/sokoban/sprites/spr_pusher/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/sokoban/sprites/spr_pusher/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/sokoban/thumbnail.png b/prefabs/sokoban/thumbnail.png new file mode 100644 index 00000000..ba95c351 Binary files /dev/null and b/prefabs/sokoban/thumbnail.png differ diff --git a/prefabs/spawner/LICENSE b/prefabs/spawner/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/spawner/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/spawner/README.md b/prefabs/spawner/README.md new file mode 100644 index 00000000..e95648f1 --- /dev/null +++ b/prefabs/spawner/README.md @@ -0,0 +1,34 @@ +# spawner + +Spawn things on a timer, in waves, without flooding the room. + +## The alarm has to re-arm itself + +An alarm is a frame counter, not a repeating timer: it fires once and stops. +Re-arming it inside its own event *is* the mechanism, and forgetting to is why +a spawner appears to work and produces exactly one enemy. `Alarm_0.gml` ends +with `alarm[0] = interval;` for that reason and no other. + +The event filename grammar is closed — `Alarm_0` … `Alarm_11`. There is no +`Alarm_12` and no custom name. + +## The population cap is not optional + +`population_cap` is checked when a spawn is due, and a full room skips that +spawn rather than pausing the timer — so the wave keeps its rhythm and resumes +the moment there is space. Removing the cap does not make the game harder, it +makes minute ten a slideshow, and the frame rate falls off a cliff rather than +degrading. + +## Difficulty is one multiplier + +Each wave multiplies the interval by `interval_scale` and stops at +`interval_min`. Two numbers rather than a table of per-wave timings, because a +curve you can reason about is one you will actually tune. `wave_frames` sets how +long a wave lasts, in frames. + +## The trap worth knowing + +Give the spawned object a sprite. A spriteless object has no collision mask, so +nothing will ever hit it — the game runs, spawns correctly, and nothing can be +shot or touched, with no error anywhere to explain it. diff --git a/prefabs/spawner/objects/obj_demo/Create.gml b/prefabs/spawner/objects/obj_demo/Create.gml new file mode 100644 index 00000000..ab28bd6b --- /dev/null +++ b/prefabs/spawner/objects/obj_demo/Create.gml @@ -0,0 +1,16 @@ +// The kernel owns the run state, and `kernel_playing()` reads it. Without a +// boot it is simply not set, and the first object to ask dies with a bare +// "not set before reading it" naming a kernel script rather than the caller. +::kernel::kernel_boot(); + +// `spawns` ships as `noone` so a freshly applied recipe cannot spawn the +// wrong thing by accident. Pointing it at something is the one wiring step +// the prefab asks of you, and this demo is that step. +with (obj_spawner) { + spawns = obj_mote; + // The shipped interval is tuned for enemies you have to fight. A demo + // wants a room with something in it, so this hurries the first waves + // along: `interval` is what the alarm re-arms from, so both move. + interval = 20; + alarm[0] = interval; +} diff --git a/prefabs/spawner/objects/obj_demo/Draw_GUI.gml b/prefabs/spawner/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..697c6e69 --- /dev/null +++ b/prefabs/spawner/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,5 @@ +// obj_spawner draws its own wave read-out at the top left, so this sits +// clear of it rather than on top. +draw_text(40, 400, "spawner demo - `spawns` is wired to obj_mote; the wave curve"); +draw_text(40, 430, "shortens the interval, so the room fills faster as it goes."); +draw_text(40, 480, "alive " + string(instance_number(obj_mote))); diff --git a/prefabs/spawner/objects/obj_demo/object.toml b/prefabs/spawner/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/spawner/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/spawner/objects/obj_mote/Create.gml b/prefabs/spawner/objects/obj_mote/Create.gml new file mode 100644 index 00000000..4a3c0635 --- /dev/null +++ b/prefabs/spawner/objects/obj_mote/Create.gml @@ -0,0 +1,4 @@ +dir = irandom_range(0, 359); +spd = random_range(0.6, 1.8); +size = irandom_range(6, 14); +tint = choose(c_aqua, c_yellow, c_lime, c_orange); diff --git a/prefabs/spawner/objects/obj_mote/Draw.gml b/prefabs/spawner/objects/obj_mote/Draw.gml new file mode 100644 index 00000000..c43984ad --- /dev/null +++ b/prefabs/spawner/objects/obj_mote/Draw.gml @@ -0,0 +1,3 @@ +draw_set_colour(tint); +draw_rectangle(x - size, y - size, x + size, y + size, false); +draw_set_colour(c_white); diff --git a/prefabs/spawner/objects/obj_mote/Step.gml b/prefabs/spawner/objects/obj_mote/Step.gml new file mode 100644 index 00000000..331952dd --- /dev/null +++ b/prefabs/spawner/objects/obj_mote/Step.gml @@ -0,0 +1,5 @@ +x += lengthdir_x(spd, dir); +y += lengthdir_y(spd, dir); +if (x < -32 || x > room_width + 32 || y < -32 || y > room_height + 32) { + instance_destroy(); +} diff --git a/prefabs/spawner/objects/obj_mote/object.toml b/prefabs/spawner/objects/obj_mote/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/spawner/objects/obj_mote/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/spawner/objects/obj_spawner/Alarm_0.gml b/prefabs/spawner/objects/obj_spawner/Alarm_0.gml new file mode 100644 index 00000000..ccfc0d83 --- /dev/null +++ b/prefabs/spawner/objects/obj_spawner/Alarm_0.gml @@ -0,0 +1,11 @@ +var _t = spawner_tuning(); + +// The cap is checked at spawn time rather than by pausing the timer, so +// the wave keeps its rhythm and simply skips while the room is full. +if (spawns != noone && instance_number(spawns) < _t.population_cap) { + var _x = irandom_range(32, room_width - 32); + instance_create_layer(_x, -32, layer, spawns); + spawned += 1; +} + +alarm[0] = interval; diff --git a/prefabs/spawner/objects/obj_spawner/Create.gml b/prefabs/spawner/objects/obj_spawner/Create.gml new file mode 100644 index 00000000..1c182e8c --- /dev/null +++ b/prefabs/spawner/objects/obj_spawner/Create.gml @@ -0,0 +1,14 @@ +// What to spawn. Set this to your own object — it is left as `noone` so a +// freshly applied recipe cannot silently spawn the wrong thing. +spawns = noone; + +var _t = spawner_tuning(); +interval = _t.interval_start; +wave = 1; +wave_timer = _t.wave_frames; +spawned = 0; + +// Alarms are frame counters, not repeating timers: an alarm that is not +// re-armed inside its own event fires exactly once. Re-arming is the whole +// mechanism, and forgetting it is why a spawner "works" for one enemy. +alarm[0] = interval; diff --git a/prefabs/spawner/objects/obj_spawner/Draw_GUI.gml b/prefabs/spawner/objects/obj_spawner/Draw_GUI.gml new file mode 100644 index 00000000..6a2d9a41 --- /dev/null +++ b/prefabs/spawner/objects/obj_spawner/Draw_GUI.gml @@ -0,0 +1 @@ +::kernel::kernel_draw_text(16, 16, "wave " + string(wave)); diff --git a/prefabs/spawner/objects/obj_spawner/Step.gml b/prefabs/spawner/objects/obj_spawner/Step.gml new file mode 100644 index 00000000..b7dae0b4 --- /dev/null +++ b/prefabs/spawner/objects/obj_spawner/Step.gml @@ -0,0 +1,9 @@ +if (!::kernel::kernel_playing()) exit; + +var _t = spawner_tuning(); +wave_timer -= 1; +if (wave_timer <= 0) { + wave += 1; + wave_timer = _t.wave_frames; + interval = max(_t.interval_min, interval * _t.interval_scale); +} diff --git a/prefabs/spawner/objects/obj_spawner/object.toml b/prefabs/spawner/objects/obj_spawner/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/spawner/objects/obj_spawner/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/spawner/prefab.toml b/prefabs/spawner/prefab.toml new file mode 100644 index 00000000..63b1fe8a --- /dev/null +++ b/prefabs/spawner/prefab.toml @@ -0,0 +1,39 @@ +schema_version = 1 +name = "Waves and spawning" +description = "Spawn things on a timer, in waves, without flooding the room." +category = "component" +tags = ["capability:spawning", "role:template"] + +install = "copy" +requires = ["kernel"] + +seam = "objects/obj_spawner/Alarm_0.gml" + +steps = [ + "Place obj_spawner on an instance layer in your game room. It has no sprite — it is a controller, not something the player sees.", + "Set `spawns` in `obj_spawner`'s Create to the object you want — an object asset is not something a TOML value can name, so this is code rather than tuning. Give that object a sprite: a spriteless one has no collision mask, so nothing will ever hit it.", + "The population cap is what stops a long run turning into a slideshow. Raise it deliberately, after watching the frame rate, not before.", +] + +traps = [ + "`spawns` is `noone` until you set it in obj_spawner's Create. Until then the waves run, the timer fires and the counter climbs, and nothing is ever spawned — no error, and a HUD that says the game is working.", +] + +exclude = ["objects/obj_demo", "objects/obj_mote", "rooms/rm_demo"] + +[roles] +obj_spawner = "runs the waves — set its `spawns` to your object; it is `noone` until you do, so nothing spawns by accident" + +[tuning] +# Frames between spawns at wave 1. Alarms count in frames, so this is +# `room_speed` divided by spawns-per-second. +interval_start = 90 +# Every wave takes this fraction off the interval — the difficulty ramp. +interval_scale = 0.88 +# Never spawn faster than this, however long the run goes. +interval_min = 20 +# Frames per wave. +wave_frames = 600 +# Hard ceiling on live instances. The one number that decides whether +# minute ten is playable. +population_cap = 24 diff --git a/prefabs/spawner/project.toml b/prefabs/spawner/project.toml new file mode 100644 index 00000000..3ded1c59 --- /dev/null +++ b/prefabs/spawner/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "spawner" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/spawner/rooms/rm_demo/layers/Background.toml b/prefabs/spawner/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/spawner/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/spawner/rooms/rm_demo/layers/Instances.toml b/prefabs/spawner/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..61178df6 --- /dev/null +++ b/prefabs/spawner/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,15 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_spawner" +object = "obj_spawner" +x = 480.0 +y = 270.0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/spawner/rooms/rm_demo/room.toml b/prefabs/spawner/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/spawner/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/spawner/thumbnail.png b/prefabs/spawner/thumbnail.png new file mode 100644 index 00000000..6c04a68e Binary files /dev/null and b/prefabs/spawner/thumbnail.png differ diff --git a/prefabs/tilemap/LICENSE b/prefabs/tilemap/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/tilemap/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/tilemap/README.md b/prefabs/tilemap/README.md new file mode 100644 index 00000000..6d78b931 --- /dev/null +++ b/prefabs/tilemap/README.md @@ -0,0 +1,3 @@ +# tilemap + +One string per row, one character per cell — a level you can read in a diff. diff --git a/prefabs/tilemap/objects/obj_demo/Create.gml b/prefabs/tilemap/objects/obj_demo/Create.gml new file mode 100644 index 00000000..8b18623c --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo/Create.gml @@ -0,0 +1,14 @@ +// The map is the readable part: change a character and the level changes. +level = [ + "##########################", + "# o #", + "# #### #### #", + "# o o #", + "# #### @ #### #", + "# #", + "##########################", +]; +cell = 32; +built = tilemap_build(level, cell, { "#": obj_demo_block }); +start = tilemap_first(level, cell, "@", 100, 100); +coins = tilemap_count(level, "o"); diff --git a/prefabs/tilemap/objects/obj_demo/Draw.gml b/prefabs/tilemap/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..c8af63b4 --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo/Draw.gml @@ -0,0 +1,10 @@ +// The start marker and the coins are positions, not instances — found +// without spawning anything. +draw_set_colour(c_yellow); +var _spots = tilemap_find(level, cell, "o"); +for (var i = 0; i < array_length(_spots); i++) { + draw_circle(_spots[i].x, _spots[i].y, 7, false); +} +draw_set_colour(c_lime); +draw_circle(start.x, start.y, 11, false); +draw_set_colour(c_white); diff --git a/prefabs/tilemap/objects/obj_demo/Draw_GUI.gml b/prefabs/tilemap/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..ffa79865 --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,8 @@ +var _size = tilemap_size(level, cell); +draw_text(40, 250, "tilemap demo — " + string(built) + " blocks placed"); +draw_text(40, 270, "map wants " + string(_size.width) + " x " + string(_size.height)); +draw_text(40, 290, string(coins) + " coins marked, start at " + + string(start.x) + "," + string(start.y)); +var _c = tilemap_cell_at(cell, mouse_x, mouse_y); +draw_text(40, 320, "mouse cell " + string(_c.col) + "," + string(_c.row) + + " holds '" + tilemap_at(level, _c.col, _c.row) + "'"); diff --git a/prefabs/tilemap/objects/obj_demo/object.toml b/prefabs/tilemap/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/tilemap/objects/obj_demo_block/Draw.gml b/prefabs/tilemap/objects/obj_demo_block/Draw.gml new file mode 100644 index 00000000..14f66a48 --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo_block/Draw.gml @@ -0,0 +1,3 @@ +draw_set_colour(c_teal); +draw_rectangle(x - 16, y - 16, x + 16, y + 16, false); +draw_set_colour(c_white); diff --git a/prefabs/tilemap/objects/obj_demo_block/object.toml b/prefabs/tilemap/objects/obj_demo_block/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/tilemap/objects/obj_demo_block/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/tilemap/prefab.toml b/prefabs/tilemap/prefab.toml new file mode 100644 index 00000000..825c990b --- /dev/null +++ b/prefabs/tilemap/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Levels as text" + +# A library: installed under `prefabs//` in the target project and +# called as `::tilemap::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Build a room from an array of strings with a legend mapping characters to objects, find the cells holding a character without spawning anything, and size the room to the map. A level held as placed instances can only be changed in an editor; a level held as twelve strings can be changed in a diff, and you can see its shape while you do." +category = "library" +tags = ["capability:levels", "capability:tilemap", "role:library"] +exclude = ["objects/obj_demo", "objects/obj_demo_block", "rooms/rm_demo"] + +steps = [ + "Instances land centred in their cell, so give their sprites a centred origin or they will sit half a cell up and left.", +] diff --git a/prefabs/tilemap/project.toml b/prefabs/tilemap/project.toml new file mode 100644 index 00000000..dabae230 --- /dev/null +++ b/prefabs/tilemap/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "tilemap" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/tilemap/rooms/rm_demo/layers/Background.toml b/prefabs/tilemap/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/tilemap/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/tilemap/rooms/rm_demo/layers/Instances.toml b/prefabs/tilemap/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/tilemap/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/tilemap/rooms/rm_demo/room.toml b/prefabs/tilemap/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/tilemap/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/tilemap/scripts/scr_tilemap/scr_tilemap.gml b/prefabs/tilemap/scripts/scr_tilemap/scr_tilemap.gml new file mode 100644 index 00000000..a9f991bd --- /dev/null +++ b/prefabs/tilemap/scripts/scr_tilemap/scr_tilemap.gml @@ -0,0 +1,56 @@ +/// A level you can read in the source: one string per row, one character +/// per cell. +/// +/// The point is that the map stays editable by a person. A level held as +/// placed instances can only be changed in an editor; a level held as +/// twelve strings can be changed in a diff, and you can see the shape of it +/// while you do. + +/// @function tilemap_build(rows, cell, legend) +/// @description Place an instance for every character that appears in +/// `legend`, a struct of `{ "#": obj_wall, "o": obj_coin }`. Characters not +/// in the legend are empty space. Returns how many were placed. +/// +/// Instances are centred in their cell, so a sprite with a centred origin +/// lines up without an offset. +function tilemap_build(rows, cell, legend) { + var _made = 0; + for (var _r = 0; _r < array_length(rows); _r++) { + var _line = rows[_r]; + for (var _c = 1; _c <= string_length(_line); _c++) { + var _ch = string_char_at(_line, _c); + if (!variable_struct_exists(legend, _ch)) continue; + var _obj = legend[$ _ch]; + if (_obj == noone) continue; + instance_create_depth( + (_c - 1) * cell + cell / 2, _r * cell + cell / 2, 0, _obj); + _made += 1; + } + } + return _made; +} + +/// @function tilemap_size(rows, cell) +/// @description The pixel size the map wants, as `{width, height}`. Width +/// comes from the longest row, so a ragged map still covers everything. +function tilemap_size(rows, cell) { + var _cols = 0; + for (var i = 0; i < array_length(rows); i++) { + _cols = max(_cols, string_length(rows[i])); + } + return { width: _cols * cell, height: array_length(rows) * cell }; +} + +/// @function tilemap_count(rows, ch) +/// @description How many cells hold `ch` — the total a "collect them all" +/// objective is counting down from. +function tilemap_count(rows, ch) { + var _n = 0; + for (var _r = 0; _r < array_length(rows); _r++) { + var _line = rows[_r]; + for (var _c = 1; _c <= string_length(_line); _c++) { + if (string_char_at(_line, _c) == ch) _n += 1; + } + } + return _n; +} diff --git a/prefabs/tilemap/scripts/scr_tilemap_query/scr_tilemap_query.gml b/prefabs/tilemap/scripts/scr_tilemap_query/scr_tilemap_query.gml new file mode 100644 index 00000000..4bd43f4f --- /dev/null +++ b/prefabs/tilemap/scripts/scr_tilemap_query/scr_tilemap_query.gml @@ -0,0 +1,45 @@ +/// @function tilemap_find(rows, cell, ch) +/// @description Every position holding `ch`, as an array of `{x, y}` in room +/// coordinates. For the things a legend should not spawn — a start point, a +/// camera anchor — which want a position, not an instance. +function tilemap_find(rows, cell, ch) { + var _hits = []; + for (var _r = 0; _r < array_length(rows); _r++) { + var _line = rows[_r]; + for (var _c = 1; _c <= string_length(_line); _c++) { + if (string_char_at(_line, _c) != ch) continue; + array_push(_hits, { + x: (_c - 1) * cell + cell / 2, + y: _r * cell + cell / 2, + }); + } + } + return _hits; +} + +/// @function tilemap_first(rows, cell, ch, fallback_x, fallback_y) +/// @description The first position holding `ch`, or the fallback when the +/// map has none. A start marker someone deleted should not put the player +/// at (0, 0) without saying so. +function tilemap_first(rows, cell, ch, fallback_x, fallback_y) { + var _hits = tilemap_find(rows, cell, ch); + if (array_length(_hits) == 0) return { x: fallback_x, y: fallback_y }; + return _hits[0]; +} + +/// @function tilemap_at(rows, col, row) +/// @description The character at a cell, or " " outside the map — so callers +/// need no bounds check. +function tilemap_at(rows, col, row) { + if (row < 0 || row >= array_length(rows)) return " "; + var _line = rows[row]; + if (col < 0 || col >= string_length(_line)) return " "; + return string_char_at(_line, col + 1); +} + +/// @function tilemap_cell_at(cell, px, py) +/// @description Which cell a room position falls in, as `{col, row}`. +function tilemap_cell_at(cell, px, py) { + return { col: floor(px / cell), row: floor(py / cell) }; +} + diff --git a/prefabs/tilemap/thumbnail.png b/prefabs/tilemap/thumbnail.png new file mode 100644 index 00000000..d80feb3f Binary files /dev/null and b/prefabs/tilemap/thumbnail.png differ diff --git a/prefabs/timer/LICENSE b/prefabs/timer/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/timer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/timer/README.md b/prefabs/timer/README.md new file mode 100644 index 00000000..8b876d8f --- /dev/null +++ b/prefabs/timer/README.md @@ -0,0 +1,3 @@ +# timer + +Time a round, show it as 1:05, and fire once when it runs out. diff --git a/prefabs/timer/objects/obj_demo/Create.gml b/prefabs/timer/objects/obj_demo/Create.gml new file mode 100644 index 00000000..5fdd623d --- /dev/null +++ b/prefabs/timer/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +bout = timer_countdown(30); +run = timer_stopwatch(); +expired = 0; diff --git a/prefabs/timer/objects/obj_demo/Draw_GUI.gml b/prefabs/timer/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..72804d9f --- /dev/null +++ b/prefabs/timer/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,9 @@ +draw_text(40, 30, "timer demo — SPACE adds 5s, P pauses, ENTER restarts"); + +draw_text(40, 80, "round " + timer_format(timer_seconds(bout)) + + (bout.running ? "" : " (paused)")); +draw_rectangle(40, 104, 40 + 400 * timer_fraction(bout), 120, false); +draw_rectangle(40, 104, 440, 120, true); + +draw_text(40, 160, "run " + timer_format_ms(timer_seconds(run))); +draw_text(40, 200, "expiries counted: " + string(expired)); diff --git a/prefabs/timer/objects/obj_demo/Step.gml b/prefabs/timer/objects/obj_demo/Step.gml new file mode 100644 index 00000000..bf7a2c11 --- /dev/null +++ b/prefabs/timer/objects/obj_demo/Step.gml @@ -0,0 +1,8 @@ +// `timer_step` is true for exactly one frame, so this counts once per +// expiry rather than once per frame after zero. +if (timer_step(bout)) expired += 1; +timer_step(run); + +if (keyboard_check_pressed(vk_space)) timer_add(bout, 5); +if (keyboard_check_pressed(vk_enter)) timer_restart(bout, 30); +if (keyboard_check_pressed(ord("P"))) timer_pause(bout, bout.running); diff --git a/prefabs/timer/objects/obj_demo/object.toml b/prefabs/timer/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/timer/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/timer/prefab.toml b/prefabs/timer/prefab.toml new file mode 100644 index 00000000..bf9f87c0 --- /dev/null +++ b/prefabs/timer/prefab.toml @@ -0,0 +1,12 @@ +schema_version = 1 +name = "Countdowns and stopwatches" + +# A library: installed under `prefabs//` in the target project and +# called as `::timer::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Countdowns and stopwatches in seconds rather than frames, pausable, extendable, and formatted as m:ss or m:ss.cc. `timer_step` returns true on the single frame a countdown expires, so the round ends once instead of every frame after zero." +category = "library" +tags = ["capability:timing", "capability:hud", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/timer/project.toml b/prefabs/timer/project.toml new file mode 100644 index 00000000..d457f57a --- /dev/null +++ b/prefabs/timer/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "timer" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/timer/rooms/rm_demo/layers/Background.toml b/prefabs/timer/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/timer/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/timer/rooms/rm_demo/layers/Instances.toml b/prefabs/timer/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/timer/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/timer/rooms/rm_demo/room.toml b/prefabs/timer/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/timer/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/timer/scripts/scr_timer/scr_timer.gml b/prefabs/timer/scripts/scr_timer/scr_timer.gml new file mode 100644 index 00000000..d9ce8c64 --- /dev/null +++ b/prefabs/timer/scripts/scr_timer/scr_timer.gml @@ -0,0 +1,58 @@ +/// Countdowns, stopwatches, and turning seconds into something readable. +/// +/// In seconds rather than frames throughout. A game written in frames reads +/// fine until someone changes the room speed, and then every duration in it +/// is wrong at once. + +/// @function timer_countdown(seconds) +/// @description A timer counting down from `seconds`, already running. +function timer_countdown(seconds) { + return { left: seconds, span: seconds, up: false, running: true }; +} + +/// @function timer_stopwatch() +/// @description A timer counting up from zero, already running. +function timer_stopwatch() { + return { left: 0, span: 0, up: true, running: true }; +} + +/// @function timer_step(state) +/// @description Advance one frame. Returns true on the single frame a +/// countdown reaches zero, so the caller can end the round there rather than +/// testing for zero every frame afterwards and ending it repeatedly. +function timer_step(state) { + if (!state.running) return false; + var _dt = delta_time / 1000000; + if (state.up) { + state.left += _dt; + return false; + } + if (state.left <= 0) return false; + state.left = max(0, state.left - _dt); + return state.left <= 0; +} + +/// @function timer_pause(state, paused) +/// @description Stop or resume it. A paused timer is the difference between +/// a pause menu and a pause menu you can hide in. +function timer_pause(state, paused) { + state.running = !paused; +} + +/// @function timer_add(state, seconds) +/// @description Extend a countdown — a pickup that buys time. Never past +/// the span it started with, so the bar cannot overfill. +function timer_add(state, seconds) { + state.left = state.up ? state.left + seconds + : min(state.span, state.left + seconds); +} + +/// @function timer_restart(state, seconds) +/// @description Back to `seconds` and running. Pass a new span to change the +/// length at the same time. +function timer_restart(state, seconds) { + state.left = state.up ? 0 : seconds; + state.span = seconds; + state.running = true; +} + diff --git a/prefabs/timer/scripts/scr_timer_format/scr_timer_format.gml b/prefabs/timer/scripts/scr_timer_format/scr_timer_format.gml new file mode 100644 index 00000000..dfe4976d --- /dev/null +++ b/prefabs/timer/scripts/scr_timer_format/scr_timer_format.gml @@ -0,0 +1,18 @@ +/// @function timer_format(seconds) +/// @description `m:ss` — 65 becomes "1:05". Rounds up, so a countdown shows +/// "0:01" for the whole of its last second rather than "0:00" for half of it. +function timer_format(seconds) { + var _total = ceil(max(0, seconds)); + var _m = _total div 60; + var _s = _total mod 60; + return string(_m) + ":" + (_s < 10 ? "0" : "") + string(_s); +} + +/// @function timer_format_ms(seconds) +/// @description `m:ss.cc` with hundredths — for a stopwatch, where the +/// difference between two runs is often under a second. +function timer_format_ms(seconds) { + var _cs = floor(frac(max(0, seconds)) * 100); + return timer_format(floor(max(0, seconds))) + + "." + (_cs < 10 ? "0" : "") + string(_cs); +} diff --git a/prefabs/timer/scripts/scr_timer_read/scr_timer_read.gml b/prefabs/timer/scripts/scr_timer_read/scr_timer_read.gml new file mode 100644 index 00000000..ef6dd6e5 --- /dev/null +++ b/prefabs/timer/scripts/scr_timer_read/scr_timer_read.gml @@ -0,0 +1,21 @@ +/// @function timer_seconds(state) +/// @description Seconds remaining on a countdown, or elapsed on a stopwatch. +function timer_seconds(state) { + return state.left; +} + +/// @function timer_done(state) +/// @description True once a countdown has reached zero. Always false for a +/// stopwatch, which has no end to reach. +function timer_done(state) { + return !state.up && state.left <= 0; +} + +/// @function timer_fraction(state) +/// @description How much of a countdown is left, 0..1, for a bar. Zero for a +/// stopwatch, which has no span to be a fraction of. +function timer_fraction(state) { + if (state.up || state.span <= 0) return 0; + return state.left / state.span; +} + diff --git a/prefabs/timer/thumbnail.png b/prefabs/timer/thumbnail.png new file mode 100644 index 00000000..b0f99455 Binary files /dev/null and b/prefabs/timer/thumbnail.png differ diff --git a/prefabs/title_screen/LICENSE b/prefabs/title_screen/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/title_screen/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/title_screen/README.md b/prefabs/title_screen/README.md new file mode 100644 index 00000000..1b35eb67 --- /dev/null +++ b/prefabs/title_screen/README.md @@ -0,0 +1,23 @@ +# title_screen + +A logo that breathes, a prompt, and a key or click to start. + +## The decisions this makes for you + +**It animates on `delta_time`, not on frames.** A frame counter runs the +wiggle at double speed on a 120Hz display. This is the single most common +way a title screen differs between machines. + +**It advances on `confirm`, which is space, enter or a click.** Prefer +release over press if you add your own key handling: whatever key brought +the player here from a results screen is often still held on the frame +this room starts, and a press handler consumes it immediately. + +## Adding a logo + +``` +gmx prefab add ninja_woods/sprites/spr_game_logo --centre-origin +``` + +`--centre-origin` because the bob applies to the sprite's origin — with a +top-left origin the logo swings rather than breathes. diff --git a/prefabs/title_screen/objects/obj_title/Create.gml b/prefabs/title_screen/objects/obj_title/Create.gml new file mode 100644 index 00000000..0e64905e --- /dev/null +++ b/prefabs/title_screen/objects/obj_title/Create.gml @@ -0,0 +1,6 @@ +::kernel::kernel_state_set(::kernel::kernel_states().title); + +// Seconds, not frames: at 120Hz a frame counter runs the wiggle twice as +// fast, and this is the single most common way title screens differ +// between machines. +t = 0; diff --git a/prefabs/title_screen/objects/obj_title/Draw_GUI.gml b/prefabs/title_screen/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..0e8f0b89 --- /dev/null +++ b/prefabs/title_screen/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,11 @@ +var _w = ::kernel::kernel_gui_width(); +var _cfg = title_screen_tuning(); + +// The logo breathes; the prompt pulses on the same clock so they read as +// one object rather than two things animating independently. +var _bob = sin(t * _cfg.wiggle_hz * 2 * pi) * _cfg.wiggle_px; +::kernel::kernel_draw_text(_w / 2, 190 + _bob, _cfg.game_title, c_white, fa_center); + +draw_set_alpha(0.55 + 0.45 * ::feel::feel_ease((sin(t * 2) + 1) / 2, "in_out")); +::kernel::kernel_draw_text(_w / 2, 300, _cfg.prompt, c_ltgray, fa_center); +draw_set_alpha(1); diff --git a/prefabs/title_screen/objects/obj_title/Step.gml b/prefabs/title_screen/objects/obj_title/Step.gml new file mode 100644 index 00000000..6ac64dc8 --- /dev/null +++ b/prefabs/title_screen/objects/obj_title/Step.gml @@ -0,0 +1,6 @@ +t += delta_time / 1000000; + +if (::kernel::kernel_action_pressed("confirm")) { + ::feel::feel_hitstop(0.05); + room_goto_next(); +} diff --git a/prefabs/title_screen/objects/obj_title/object.toml b/prefabs/title_screen/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/title_screen/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/title_screen/prefab.toml b/prefabs/title_screen/prefab.toml new file mode 100644 index 00000000..169cabc5 --- /dev/null +++ b/prefabs/title_screen/prefab.toml @@ -0,0 +1,25 @@ +schema_version = 1 +name = "Title screen" +description = "A logo that breathes, a prompt, and a key or click to start." +category = "component" +tags = ["capability:ui", "role:template"] + +install = "copy" +requires = ["kernel", "feel"] + +seam = "objects/obj_title/Step.gml" + +steps = [ + "Place obj_title on an instance layer in your title room, alongside ::kernel::obj_kernel.", + "Set `start_room` in the generated tuning to the room you want SPACE to lead to.", + "For a logo sprite: `gmx prefab add ninja_woods/sprites/spr_game_logo --centre-origin`, then draw it above the title text.", +] + +[roles] +obj_title = "the title: animates the logo on a seconds clock and moves to the next room on confirm" + +[tuning] +game_title = "YOUR GAME" +prompt = "press SPACE or click to start" +wiggle_px = 6.0 +wiggle_hz = 1.4 diff --git a/prefabs/title_screen/thumbnail.png b/prefabs/title_screen/thumbnail.png new file mode 100644 index 00000000..4ee04931 Binary files /dev/null and b/prefabs/title_screen/thumbnail.png differ diff --git a/prefabs/top_down_arena/LICENSE b/prefabs/top_down_arena/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/top_down_arena/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/top_down_arena/README.md b/prefabs/top_down_arena/README.md new file mode 100644 index 00000000..756110eb --- /dev/null +++ b/prefabs/top_down_arena/README.md @@ -0,0 +1,50 @@ +# top_down_arena + +Survive waves in one room: move, dodge, collect, and last as long as you can. + +## One room, and a curve instead of levels + +There is no level two. Difficulty is entirely the spawn curve, which means the +whole game is tuned by three numbers in the data script: + +- `spawn_interval` — seconds between spawns on wave one +- `spawn_scale` — what that is multiplied by each wave, below 1 to get harder +- `spawn_floor` — the fastest it will ever go + +Those three decide whether minute five is playable, and they are the first +thing to change. `wave_budget` also raises how many arrive at once, which is +what keeps the curve climbing after the interval bottoms out — a curve that +only tightens the interval flattens completely once it hits the floor. + +## The invulnerability window is load-bearing + +`iframe_seconds` is not polish. An enemy that stays overlapping deals damage on +every frame it touches you, so without a window a single contact drains the +whole bar in a few frames and the run ends before the player understands they +were touched. The game validates, builds and boots perfectly while being +unplayable. + +The player flashes while the window is open, which is the other half — a player +who cannot see why they are not taking damage assumes the collision is broken. + +## Enemies push each other apart + +`ai_separate` runs once per frame from `obj_arena`, not from each enemy. It +moves the *other* instances, so calling it in every enemy's Step doubles every +push and the group jitters. + +Without it, chasers converge onto the same point and stack into what reads as a +single enemy — which then lands several hits at once, through the i-frame +window, because each is a separate collision. + +## Spawning happens at the edges + +A wave that appears on top of the player is not difficulty, it is a coin flip. +`obj_arena` picks a room edge and spawns there, so every enemy has to cross the +floor to reach you and you always have somewhere to run. + +## What it does not do + +No shooting, no power-ups, no second room. `projectile` and `powerup` are the +two prefabs that fit it best if you want them — `gmx prefab add projectile +--project .` and give the player a fire button. diff --git a/prefabs/top_down_arena/demo.gametest.json b/prefabs/top_down_arena/demo.gametest.json new file mode 100644 index 00000000..7ed0812a --- /dev/null +++ b/prefabs/top_down_arena/demo.gametest.json @@ -0,0 +1,110 @@ +{ + "seed": 7, + "fps": 60, + "total_frames": 210, + "stop_on_error": true, + "chunks": { + "init": { + "gml": "room_goto(rm_title); global.gt_seen=false; global.gt_x0=undefined; global.gt_moved=false; global.gt_enemies=0; global.gt_ended=false;" + }, + "watch": { + "gml": "if (room == rm_results) global.gt_ended = true; if (instance_exists(obj_player)) { with (obj_player) { global.gt_seen = true; if (global.gt_x0 == undefined) global.gt_x0 = x; if (x > global.gt_x0 + 8) global.gt_moved = true; } } global.gt_enemies = max(global.gt_enemies, instance_number(obj_enemy));" + }, + "seen": { + "gml": "return global.gt_seen;" + }, + "moved": { + "gml": "return global.gt_moved;" + }, + "spawns": { + "gml": "return global.gt_enemies > 0;" + }, + "alive": { + "gml": "return !global.gt_ended;" + } + }, + "timeline": [ + { + "frame": 20, + "inputs": [ + { + "key": "vk_right", + "state": "down" + } + ] + }, + { + "frame": 60, + "inputs": [ + { + "key": "vk_right", + "state": "up" + } + ] + }, + { + "frame": 200, + "assert": { + "id": "seen", + "name": "the player is in the arena", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "moved", + "name": "holding right walks the player", + "expect": true + } + }, + { + "frame": 200, + "assert": { + "id": "spawns", + "name": "the wave curve spawns enemies", + "expect": true + } + }, + { + "frame": 60, + "assert": { + "id": "alive", + "name": "the run is still going a second in \u2014 nothing kills the player at once", + "expect": true + } + }, + { + "frame": 5, + "code": [ + "init" + ] + }, + { + "frame": 6, + "every": 3, + "until": 198, + "code": [ + "watch" + ] + }, + { + "frame": 10, + "inputs": [ + { + "key": "vk_space", + "state": "down" + } + ] + }, + { + "frame": 12, + "inputs": [ + { + "key": "vk_space", + "state": "up" + } + ] + } + ] +} diff --git a/prefabs/top_down_arena/objects/obj_arena/Create.gml b/prefabs/top_down_arena/objects/obj_arena/Create.gml new file mode 100644 index 00000000..8a8c3f2c --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_arena/Create.gml @@ -0,0 +1,7 @@ +// Entering `play` resets score, combo and the run clock — see the kernel. +::kernel::kernel_state_set(::kernel::kernel_states().play); +::kernel::kernel_hud_visible(false); + +var _t = top_down_arena_tuning(); +curve = ::wave::wave_make( + _t.spawn_interval, _t.spawn_scale, _t.spawn_floor, _t.wave_seconds); diff --git a/prefabs/top_down_arena/objects/obj_arena/Draw_GUI.gml b/prefabs/top_down_arena/objects/obj_arena/Draw_GUI.gml new file mode 100644 index 00000000..a82d3518 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_arena/Draw_GUI.gml @@ -0,0 +1,6 @@ +::kernel::kernel_draw_text(16, 16, "score " + string(::kernel::kernel_score())); +::kernel::kernel_draw_text(16, 40, "wave " + string(::wave::wave_number(curve))); +::kernel::kernel_draw_text(16, 64, "time " + string(floor(::kernel::kernel_elapsed())) + "s"); + +var _p = instance_find(obj_player, 0); +if (_p != noone) ::health::health_draw_bar(16, 96, 220, 18, _p.vitals); diff --git a/prefabs/top_down_arena/objects/obj_arena/Step.gml b/prefabs/top_down_arena/objects/obj_arena/Step.gml new file mode 100644 index 00000000..a2a43a5a --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_arena/Step.gml @@ -0,0 +1,34 @@ +if (!::kernel::kernel_playing()) exit; + +::kernel::kernel_tick(); + +// The curve decides when and how many; where they appear is this object's +// business. Spawning happens at a room edge, away from the player, so a +// wave never lands on top of them. +if (::wave::wave_step(curve)) { + repeat (::wave::wave_budget(curve, 1)) { + var _edge = irandom(3); + var _sx = _edge == 0 ? 60 : (_edge == 1 ? room_width - 60 : irandom_range(60, room_width - 60)); + var _sy = _edge == 2 ? 60 : (_edge == 3 ? room_height - 60 : irandom_range(60, room_height - 60)); + instance_create_depth(_sx, _sy, 0, obj_enemy); + } + + // One coin per wave, away from the edges the enemies arrive at. + // + // Nothing kills an enemy in this game — there is no weapon, and survival + // is the whole loop — so a coin "dropped by a dead enemy" could never + // appear. Without this the only `kernel_score_add` in the recipe is + // unreachable and every run ends "score 0", with the `pickup` dependency + // and the `coin_score` knob along for the ride. + instance_create_depth( + irandom_range(120, room_width - 120), + irandom_range(120, room_height - 120), + 0, obj_coin); +} + +// Once per enemy. `ai_separate` pushes the *calling* instance away from its +// peers, so running it from a single instance separated that one enemy from +// the pack and left every other pair fully overlapped — the group rendered as +// one blob. The library's own `_share = 0.5` is what stops the per-pair +// double-count this looks like it would cause. +with (obj_enemy) ::enemy_ai::ai_separate(brain, obj_enemy, 30); diff --git a/prefabs/top_down_arena/objects/obj_arena/object.toml b/prefabs/top_down_arena/objects/obj_arena/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_arena/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/top_down_arena/objects/obj_coin/Create.gml b/prefabs/top_down_arena/objects/obj_coin/Create.gml new file mode 100644 index 00000000..53ecbaaa --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_coin/Create.gml @@ -0,0 +1 @@ +loot = ::pickup::pickup_make(16, 130, 3.5); diff --git a/prefabs/top_down_arena/objects/obj_coin/Draw.gml b/prefabs/top_down_arena/objects/obj_coin/Draw.gml new file mode 100644 index 00000000..a403469b --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_coin/Draw.gml @@ -0,0 +1 @@ +draw_sprite(sprite_index, 0, x, y + ::pickup::pickup_bob(3, 4)); diff --git a/prefabs/top_down_arena/objects/obj_coin/Step.gml b/prefabs/top_down_arena/objects/obj_coin/Step.gml new file mode 100644 index 00000000..4ae6ab5d --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_coin/Step.gml @@ -0,0 +1,11 @@ +if (!::kernel::kernel_playing()) exit; + +var _who = ::pickup::pickup_step(loot, obj_player); +if (_who == noone) exit; + +// Score, feedback, then destroy — in that order, because the popup wants +// the coin's position and the instance is about to stop having one. +var _t = top_down_arena_tuning(); +::kernel::kernel_score_add(_t.coin_score); +::feel::feel_pop(x, y, "+" + string(_t.coin_score), c_yellow); +instance_destroy(); diff --git a/prefabs/top_down_arena/objects/obj_coin/object.toml b/prefabs/top_down_arena/objects/obj_coin/object.toml new file mode 100644 index 00000000..96e96a4d --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_coin/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_coin" diff --git a/prefabs/top_down_arena/objects/obj_enemy/Create.gml b/prefabs/top_down_arena/objects/obj_enemy/Create.gml new file mode 100644 index 00000000..2abc933b --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_enemy/Create.gml @@ -0,0 +1,2 @@ +var _t = top_down_arena_tuning(); +brain = ::enemy_ai::ai_make(_t.enemy_speed, _t.enemy_sight); diff --git a/prefabs/top_down_arena/objects/obj_enemy/Step.gml b/prefabs/top_down_arena/objects/obj_enemy/Step.gml new file mode 100644 index 00000000..52259616 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_enemy/Step.gml @@ -0,0 +1,10 @@ +if (!::kernel::kernel_playing()) exit; + +var _p = instance_find(obj_player, 0); +if (_p == noone) exit; + +if (::enemy_ai::ai_sees(brain, _p.x, _p.y)) { + ::enemy_ai::ai_chase(brain, _p.x, _p.y); +} else { + ::enemy_ai::ai_wander(brain, 1.5); +} diff --git a/prefabs/top_down_arena/objects/obj_enemy/object.toml b/prefabs/top_down_arena/objects/obj_enemy/object.toml new file mode 100644 index 00000000..26963bc4 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_enemy/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_enemy" diff --git a/prefabs/top_down_arena/objects/obj_player/Create.gml b/prefabs/top_down_arena/objects/obj_player/Create.gml new file mode 100644 index 00000000..3499bb08 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_player/Create.gml @@ -0,0 +1,3 @@ +var _t = top_down_arena_tuning(); +move_speed = _t.player_speed; +vitals = ::health::health_make(_t.player_hp, _t.iframe_seconds); diff --git a/prefabs/top_down_arena/objects/obj_player/Draw.gml b/prefabs/top_down_arena/objects/obj_player/Draw.gml new file mode 100644 index 00000000..19741c85 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_player/Draw.gml @@ -0,0 +1,4 @@ +// Flashing while invulnerable is what tells the player why nothing is +// hurting them. +if (::health::health_invulnerable(vitals) && (current_time div 90) mod 2 == 0) exit; +draw_self(); diff --git a/prefabs/top_down_arena/objects/obj_player/Step.gml b/prefabs/top_down_arena/objects/obj_player/Step.gml new file mode 100644 index 00000000..ac99afed --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_player/Step.gml @@ -0,0 +1,24 @@ +if (!::kernel::kernel_playing()) exit; + +::health::health_step(vitals); + +var _dx = keyboard_check(vk_right) - keyboard_check(vk_left); +var _dy = keyboard_check(vk_down) - keyboard_check(vk_up); +::motion::motion_topdown(obj_wall, _dx, _dy, move_speed); + +// Touching an enemy costs health, but only once per i-frame window — that +// window is what stops a chaser that stays overlapping ending the run in +// three frames. +if (place_meeting(x, y, obj_enemy)) { + var _t = top_down_arena_tuning(); + if (::health::health_hurt(vitals, _t.enemy_touch_damage)) { + ::feel::feel_shake(0.2, 7); + ::feel::feel_hitstop(0.06); + } +} + +if (::health::health_dead(vitals)) { + ::kernel::kernel_game_over("overrun"); + ::kernel::kernel_save_high_score(::kernel::kernel_score()); + room_goto(rm_results); +} diff --git a/prefabs/top_down_arena/objects/obj_player/object.toml b/prefabs/top_down_arena/objects/obj_player/object.toml new file mode 100644 index 00000000..297e1266 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_player/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_player" diff --git a/prefabs/top_down_arena/objects/obj_results/Draw_GUI.gml b/prefabs/top_down_arena/objects/obj_results/Draw_GUI.gml new file mode 100644 index 00000000..5854bb91 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_results/Draw_GUI.gml @@ -0,0 +1,9 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 190, "RUN OVER", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 240, + "score " + string(::kernel::kernel_score()), c_yellow, fa_center); +::kernel::kernel_draw_text(_w / 2, 270, + "lasted " + string(floor(::kernel::kernel_elapsed())) + "s", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 300, + "best " + string(::kernel::kernel_save_get("high_score", 0)), c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 360, "SPACE for the title", c_ltgray, fa_center); diff --git a/prefabs/top_down_arena/objects/obj_results/Step.gml b/prefabs/top_down_arena/objects/obj_results/Step.gml new file mode 100644 index 00000000..17cb6d8e --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_results/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_title); diff --git a/prefabs/top_down_arena/objects/obj_results/object.toml b/prefabs/top_down_arena/objects/obj_results/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_results/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/top_down_arena/objects/obj_title/Create.gml b/prefabs/top_down_arena/objects/obj_title/Create.gml new file mode 100644 index 00000000..4d412ccb --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_title/Create.gml @@ -0,0 +1,5 @@ +// One boot for the whole run. The kernel is persistent, so this happens +// once and every later room inherits the state it set up. +::kernel::kernel_boot(); +::kernel::kernel_data_source(top_down_arena_data, top_down_arena_tuning()); +::kernel::kernel_state_set(::kernel::kernel_states().title); diff --git a/prefabs/top_down_arena/objects/obj_title/Draw_GUI.gml b/prefabs/top_down_arena/objects/obj_title/Draw_GUI.gml new file mode 100644 index 00000000..b98f773d --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_title/Draw_GUI.gml @@ -0,0 +1,4 @@ +var _w = ::kernel::kernel_gui_width(); +::kernel::kernel_draw_text(_w / 2, 200, "ARENA", c_white, fa_center); +::kernel::kernel_draw_text(_w / 2, 260, "survive the waves", c_ltgray, fa_center); +::kernel::kernel_draw_text(_w / 2, 320, "press SPACE to start", c_ltgray, fa_center); diff --git a/prefabs/top_down_arena/objects/obj_title/Step.gml b/prefabs/top_down_arena/objects/obj_title/Step.gml new file mode 100644 index 00000000..b846a5ef --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_title/Step.gml @@ -0,0 +1 @@ +if (::kernel::kernel_action_pressed("confirm")) room_goto(rm_arena); diff --git a/prefabs/top_down_arena/objects/obj_title/object.toml b/prefabs/top_down_arena/objects/obj_title/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_title/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/top_down_arena/objects/obj_wall/object.toml b/prefabs/top_down_arena/objects/obj_wall/object.toml new file mode 100644 index 00000000..9e5bbee8 --- /dev/null +++ b/prefabs/top_down_arena/objects/obj_wall/object.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#object + +sprite = "spr_wall" diff --git a/prefabs/top_down_arena/prefab.toml b/prefabs/top_down_arena/prefab.toml new file mode 100644 index 00000000..1e0d07b0 --- /dev/null +++ b/prefabs/top_down_arena/prefab.toml @@ -0,0 +1,50 @@ +schema_version = 1 +name = "Top-down arena" +description = "Survive waves in one room: move, dodge, collect, and last as long as you can." +category = "game" +tags = ["genre:action", "role:template"] + +install = "copy" +requires = ["kernel", "feel", "motion", "health", "enemy_ai", "wave", "pickup"] + +seam = "scripts/scr_top_down_arena_data/scr_top_down_arena_data.gml" + +steps = [ + "`top_down_arena.gametest.json` came with the prefab and already passes: the player is in rm_arena, walks under the arrow keys, the wave curve spawns enemies, and the run survives five seconds. Run it with `gmx test top_down_arena.gametest.json --project .` before and after a change, and *extend* it for whatever you add rather than authoring a new one. Never delete a failing assert to get a pass — a test that had to lose its check to go green has verified nothing.", + "The whole game is one room, so difficulty comes from the wave curve rather than from new levels — `spawn_interval` and `spawn_scale` are the two numbers that decide whether it is playable at minute five.", + "The placeholder art is flat colour. `gmx prefab add iconic_animals/...` swaps in real sprites without touching any code.", +] + +[roles] +obj_arena = "runs the wave curve, spawns enemies, and ends the run" +obj_player = "eight-way movement, takes damage with an i-frame window" +obj_enemy = "chases the player, separated from its neighbours" +obj_coin = "one drops each wave; collected with a magnet, and the only thing that scores" +obj_wall = "the arena edge — what movement stops against" +rm_title = "boot room — its size is the window size" +rm_arena = "the whole game" +rm_results = "how long you lasted, and the best so far" + +[art.player] +catalog = "iconic_animals/sprites/spr_cartoon_cat_ginger" +placeholder = "flat rectangle" + +[art.enemy] +catalog = "iconic_animals/sprites/spr_cartoon_fox" +placeholder = "flat circle" + +[tuning] +player_speed = 4 +player_hp = 5 +iframe_seconds = 1.0 +enemy_speed = 1.8 +enemy_sight = 900 +enemy_touch_damage = 1 +# Seconds between spawns at wave one, multiplied by `spawn_scale` each wave +# and never below `spawn_floor`. These three decide whether minute five is +# playable, and they are the first thing to change. +spawn_interval = 2.2 +spawn_scale = 0.85 +spawn_floor = 0.35 +wave_seconds = 15 +coin_score = 25 diff --git a/prefabs/top_down_arena/project.toml b/prefabs/top_down_arena/project.toml new file mode 100644 index 00000000..bbdbe54f --- /dev/null +++ b/prefabs/top_down_arena/project.toml @@ -0,0 +1,8 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "top_down_arena" + +# The order here is the flow: each controller sends you to the next room. +[room_order] +order = ["rm_title", "rm_arena", "rm_results"] diff --git a/prefabs/top_down_arena/rooms/rm_arena/layers/Background.toml b/prefabs/top_down_arena/rooms/rm_arena/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_arena/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/top_down_arena/rooms/rm_arena/layers/Instances.toml b/prefabs/top_down_arena/rooms/rm_arena/layers/Instances.toml new file mode 100644 index 00000000..7bad6ab4 --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_arena/layers/Instances.toml @@ -0,0 +1,447 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_arena" +object = "obj_arena" +x = 0.0 +y = 0.0 + +[[entries]] +id = "inst_player" +object = "obj_player" +x = 480.0 +y = 270.0 + +[[entries]] +id = "inst_wall_0" +object = "obj_wall" +x = 20.0 +y = 20.0 + +[[entries]] +id = "inst_wall_1" +object = "obj_wall" +x = 20.0 +y = 520.0 + +[[entries]] +id = "inst_wall_2" +object = "obj_wall" +x = 60.0 +y = 20.0 + +[[entries]] +id = "inst_wall_3" +object = "obj_wall" +x = 60.0 +y = 520.0 + +[[entries]] +id = "inst_wall_4" +object = "obj_wall" +x = 100.0 +y = 20.0 + +[[entries]] +id = "inst_wall_5" +object = "obj_wall" +x = 100.0 +y = 520.0 + +[[entries]] +id = "inst_wall_6" +object = "obj_wall" +x = 140.0 +y = 20.0 + +[[entries]] +id = "inst_wall_7" +object = "obj_wall" +x = 140.0 +y = 520.0 + +[[entries]] +id = "inst_wall_8" +object = "obj_wall" +x = 180.0 +y = 20.0 + +[[entries]] +id = "inst_wall_9" +object = "obj_wall" +x = 180.0 +y = 520.0 + +[[entries]] +id = "inst_wall_10" +object = "obj_wall" +x = 220.0 +y = 20.0 + +[[entries]] +id = "inst_wall_11" +object = "obj_wall" +x = 220.0 +y = 520.0 + +[[entries]] +id = "inst_wall_12" +object = "obj_wall" +x = 260.0 +y = 20.0 + +[[entries]] +id = "inst_wall_13" +object = "obj_wall" +x = 260.0 +y = 520.0 + +[[entries]] +id = "inst_wall_14" +object = "obj_wall" +x = 300.0 +y = 20.0 + +[[entries]] +id = "inst_wall_15" +object = "obj_wall" +x = 300.0 +y = 520.0 + +[[entries]] +id = "inst_wall_16" +object = "obj_wall" +x = 340.0 +y = 20.0 + +[[entries]] +id = "inst_wall_17" +object = "obj_wall" +x = 340.0 +y = 520.0 + +[[entries]] +id = "inst_wall_18" +object = "obj_wall" +x = 380.0 +y = 20.0 + +[[entries]] +id = "inst_wall_19" +object = "obj_wall" +x = 380.0 +y = 520.0 + +[[entries]] +id = "inst_wall_20" +object = "obj_wall" +x = 420.0 +y = 20.0 + +[[entries]] +id = "inst_wall_21" +object = "obj_wall" +x = 420.0 +y = 520.0 + +[[entries]] +id = "inst_wall_22" +object = "obj_wall" +x = 460.0 +y = 20.0 + +[[entries]] +id = "inst_wall_23" +object = "obj_wall" +x = 460.0 +y = 520.0 + +[[entries]] +id = "inst_wall_24" +object = "obj_wall" +x = 500.0 +y = 20.0 + +[[entries]] +id = "inst_wall_25" +object = "obj_wall" +x = 500.0 +y = 520.0 + +[[entries]] +id = "inst_wall_26" +object = "obj_wall" +x = 540.0 +y = 20.0 + +[[entries]] +id = "inst_wall_27" +object = "obj_wall" +x = 540.0 +y = 520.0 + +[[entries]] +id = "inst_wall_28" +object = "obj_wall" +x = 580.0 +y = 20.0 + +[[entries]] +id = "inst_wall_29" +object = "obj_wall" +x = 580.0 +y = 520.0 + +[[entries]] +id = "inst_wall_30" +object = "obj_wall" +x = 620.0 +y = 20.0 + +[[entries]] +id = "inst_wall_31" +object = "obj_wall" +x = 620.0 +y = 520.0 + +[[entries]] +id = "inst_wall_32" +object = "obj_wall" +x = 660.0 +y = 20.0 + +[[entries]] +id = "inst_wall_33" +object = "obj_wall" +x = 660.0 +y = 520.0 + +[[entries]] +id = "inst_wall_34" +object = "obj_wall" +x = 700.0 +y = 20.0 + +[[entries]] +id = "inst_wall_35" +object = "obj_wall" +x = 700.0 +y = 520.0 + +[[entries]] +id = "inst_wall_36" +object = "obj_wall" +x = 740.0 +y = 20.0 + +[[entries]] +id = "inst_wall_37" +object = "obj_wall" +x = 740.0 +y = 520.0 + +[[entries]] +id = "inst_wall_38" +object = "obj_wall" +x = 780.0 +y = 20.0 + +[[entries]] +id = "inst_wall_39" +object = "obj_wall" +x = 780.0 +y = 520.0 + +[[entries]] +id = "inst_wall_40" +object = "obj_wall" +x = 820.0 +y = 20.0 + +[[entries]] +id = "inst_wall_41" +object = "obj_wall" +x = 820.0 +y = 520.0 + +[[entries]] +id = "inst_wall_42" +object = "obj_wall" +x = 860.0 +y = 20.0 + +[[entries]] +id = "inst_wall_43" +object = "obj_wall" +x = 860.0 +y = 520.0 + +[[entries]] +id = "inst_wall_44" +object = "obj_wall" +x = 900.0 +y = 20.0 + +[[entries]] +id = "inst_wall_45" +object = "obj_wall" +x = 900.0 +y = 520.0 + +[[entries]] +id = "inst_wall_46" +object = "obj_wall" +x = 940.0 +y = 20.0 + +[[entries]] +id = "inst_wall_47" +object = "obj_wall" +x = 940.0 +y = 520.0 + +[[entries]] +id = "inst_wall_48" +object = "obj_wall" +x = 20.0 +y = 60.0 + +[[entries]] +id = "inst_wall_49" +object = "obj_wall" +x = 940.0 +y = 60.0 + +[[entries]] +id = "inst_wall_50" +object = "obj_wall" +x = 20.0 +y = 100.0 + +[[entries]] +id = "inst_wall_51" +object = "obj_wall" +x = 940.0 +y = 100.0 + +[[entries]] +id = "inst_wall_52" +object = "obj_wall" +x = 20.0 +y = 140.0 + +[[entries]] +id = "inst_wall_53" +object = "obj_wall" +x = 940.0 +y = 140.0 + +[[entries]] +id = "inst_wall_54" +object = "obj_wall" +x = 20.0 +y = 180.0 + +[[entries]] +id = "inst_wall_55" +object = "obj_wall" +x = 940.0 +y = 180.0 + +[[entries]] +id = "inst_wall_56" +object = "obj_wall" +x = 20.0 +y = 220.0 + +[[entries]] +id = "inst_wall_57" +object = "obj_wall" +x = 940.0 +y = 220.0 + +[[entries]] +id = "inst_wall_58" +object = "obj_wall" +x = 20.0 +y = 260.0 + +[[entries]] +id = "inst_wall_59" +object = "obj_wall" +x = 940.0 +y = 260.0 + +[[entries]] +id = "inst_wall_60" +object = "obj_wall" +x = 20.0 +y = 300.0 + +[[entries]] +id = "inst_wall_61" +object = "obj_wall" +x = 940.0 +y = 300.0 + +[[entries]] +id = "inst_wall_62" +object = "obj_wall" +x = 20.0 +y = 340.0 + +[[entries]] +id = "inst_wall_63" +object = "obj_wall" +x = 940.0 +y = 340.0 + +[[entries]] +id = "inst_wall_64" +object = "obj_wall" +x = 20.0 +y = 380.0 + +[[entries]] +id = "inst_wall_65" +object = "obj_wall" +x = 940.0 +y = 380.0 + +[[entries]] +id = "inst_wall_66" +object = "obj_wall" +x = 20.0 +y = 420.0 + +[[entries]] +id = "inst_wall_67" +object = "obj_wall" +x = 940.0 +y = 420.0 + +[[entries]] +id = "inst_wall_68" +object = "obj_wall" +x = 20.0 +y = 460.0 + +[[entries]] +id = "inst_wall_69" +object = "obj_wall" +x = 940.0 +y = 460.0 + +[[entries]] +id = "inst_wall_70" +object = "obj_wall" +x = 20.0 +y = 500.0 + +[[entries]] +id = "inst_wall_71" +object = "obj_wall" +x = 940.0 +y = 500.0 diff --git a/prefabs/top_down_arena/rooms/rm_arena/room.toml b/prefabs/top_down_arena/rooms/rm_arena/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_arena/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/top_down_arena/rooms/rm_results/layers/Background.toml b/prefabs/top_down_arena/rooms/rm_results/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_results/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/top_down_arena/rooms/rm_results/layers/Instances.toml b/prefabs/top_down_arena/rooms/rm_results/layers/Instances.toml new file mode 100644 index 00000000..64f9d60f --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_results/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_results" +object = "obj_results" +x = 0.0 +y = 0.0 diff --git a/prefabs/top_down_arena/rooms/rm_results/room.toml b/prefabs/top_down_arena/rooms/rm_results/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_results/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/top_down_arena/rooms/rm_title/layers/Background.toml b/prefabs/top_down_arena/rooms/rm_title/layers/Background.toml new file mode 100644 index 00000000..138bf4ee --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_title/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#141824" diff --git a/prefabs/top_down_arena/rooms/rm_title/layers/Instances.toml b/prefabs/top_down_arena/rooms/rm_title/layers/Instances.toml new file mode 100644 index 00000000..4c4719d2 --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_title/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_title" +object = "obj_title" +x = 0.0 +y = 0.0 diff --git a/prefabs/top_down_arena/rooms/rm_title/room.toml b/prefabs/top_down_arena/rooms/rm_title/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/top_down_arena/rooms/rm_title/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/top_down_arena/sprites/spr_coin/frame_000.png b/prefabs/top_down_arena/sprites/spr_coin/frame_000.png new file mode 100644 index 00000000..33f02581 Binary files /dev/null and b/prefabs/top_down_arena/sprites/spr_coin/frame_000.png differ diff --git a/prefabs/top_down_arena/sprites/spr_coin/sprite.toml b/prefabs/top_down_arena/sprites/spr_coin/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/top_down_arena/sprites/spr_coin/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/top_down_arena/sprites/spr_enemy/frame_000.png b/prefabs/top_down_arena/sprites/spr_enemy/frame_000.png new file mode 100644 index 00000000..7494116e Binary files /dev/null and b/prefabs/top_down_arena/sprites/spr_enemy/frame_000.png differ diff --git a/prefabs/top_down_arena/sprites/spr_enemy/sprite.toml b/prefabs/top_down_arena/sprites/spr_enemy/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/top_down_arena/sprites/spr_enemy/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/top_down_arena/sprites/spr_player/frame_000.png b/prefabs/top_down_arena/sprites/spr_player/frame_000.png new file mode 100644 index 00000000..c6fdbe40 Binary files /dev/null and b/prefabs/top_down_arena/sprites/spr_player/frame_000.png differ diff --git a/prefabs/top_down_arena/sprites/spr_player/sprite.toml b/prefabs/top_down_arena/sprites/spr_player/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/top_down_arena/sprites/spr_player/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/top_down_arena/sprites/spr_wall/frame_000.png b/prefabs/top_down_arena/sprites/spr_wall/frame_000.png new file mode 100644 index 00000000..96066b9b Binary files /dev/null and b/prefabs/top_down_arena/sprites/spr_wall/frame_000.png differ diff --git a/prefabs/top_down_arena/sprites/spr_wall/sprite.toml b/prefabs/top_down_arena/sprites/spr_wall/sprite.toml new file mode 100644 index 00000000..70110f74 --- /dev/null +++ b/prefabs/top_down_arena/sprites/spr_wall/sprite.toml @@ -0,0 +1,3 @@ +#:schema ../../.gm-schema/v1.json#sprite + +origin = "centre" diff --git a/prefabs/top_down_arena/thumbnail.png b/prefabs/top_down_arena/thumbnail.png new file mode 100644 index 00000000..ba52ca7c Binary files /dev/null and b/prefabs/top_down_arena/thumbnail.png differ diff --git a/prefabs/transition/LICENSE b/prefabs/transition/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/transition/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/transition/README.md b/prefabs/transition/README.md new file mode 100644 index 00000000..4a6e357c --- /dev/null +++ b/prefabs/transition/README.md @@ -0,0 +1,3 @@ +# transition + +Cover the screen, change room, uncover — in that order. diff --git a/prefabs/transition/objects/obj_demo/Create.gml b/prefabs/transition/objects/obj_demo/Create.gml new file mode 100644 index 00000000..06b5c2ad --- /dev/null +++ b/prefabs/transition/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +// Persistent so the transition survives the room change it is performing. +fade = transition_make(0.45); +style = 0; diff --git a/prefabs/transition/objects/obj_demo/Draw_GUI.gml b/prefabs/transition/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..893ef82c --- /dev/null +++ b/prefabs/transition/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,10 @@ +var _names = ["fade", "wipe", "bars"]; +draw_text(40, 30, "transition demo — SPACE changes room, ENTER changes style"); +draw_text(40, 50, "style: " + _names[style]); +draw_text(40, 90, room == rm_demo ? "room one" : "room two"); +draw_text(40, 110, transition_busy(fade) ? "busy — input is ignored" : "ready"); + +// Drawn last so it covers this text too. +if (style == 0) transition_draw_fade(fade, c_black); +else if (style == 1) transition_draw_wipe(fade, c_black, true); +else transition_draw_bars(fade, c_black); diff --git a/prefabs/transition/objects/obj_demo/Step.gml b/prefabs/transition/objects/obj_demo/Step.gml new file mode 100644 index 00000000..ea1b5fbb --- /dev/null +++ b/prefabs/transition/objects/obj_demo/Step.gml @@ -0,0 +1,10 @@ +transition_step(fade); + +// Input is gated on `busy`, so the player stops playing a room they are +// already leaving. +if (transition_busy(fade)) exit; + +if (keyboard_check_pressed(vk_space)) { + transition_go(fade, room == rm_demo ? rm_demo_two : rm_demo); +} +if (keyboard_check_pressed(vk_enter)) style = (style + 1) mod 3; diff --git a/prefabs/transition/objects/obj_demo/object.toml b/prefabs/transition/objects/obj_demo/object.toml new file mode 100644 index 00000000..0b0c188d --- /dev/null +++ b/prefabs/transition/objects/obj_demo/object.toml @@ -0,0 +1,2 @@ +#:schema ../../.gm-schema/v1.json#object + diff --git a/prefabs/transition/prefab.toml b/prefabs/transition/prefab.toml new file mode 100644 index 00000000..9fc9ec35 --- /dev/null +++ b/prefabs/transition/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Room transitions" + +# A library: installed under `prefabs//` in the target project and +# called as `::transition::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Fades, wipes and closing bars, with the room change performed on the frame the screen is fully covered. Going first and fading after shows the new room for a frame, which is worse than no transition at all. Includes a busy flag to gate input on, so the player stops playing a room they are leaving." +category = "library" +tags = ["capability:transitions", "capability:rooms", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo", "rooms/rm_demo_two"] + +steps = [ + "Draw the overlay last, in a Draw GUI event — a transition the score sits on top of is not a transition.", +] diff --git a/prefabs/transition/project.toml b/prefabs/transition/project.toml new file mode 100644 index 00000000..c015e351 --- /dev/null +++ b/prefabs/transition/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "transition" + +[room_order] +order = ["rm_demo", "rm_demo_two"] diff --git a/prefabs/transition/rooms/rm_demo/layers/Background.toml b/prefabs/transition/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/transition/rooms/rm_demo/layers/Instances.toml b/prefabs/transition/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/transition/rooms/rm_demo/room.toml b/prefabs/transition/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/transition/rooms/rm_demo_two/layers/Background.toml b/prefabs/transition/rooms/rm_demo_two/layers/Background.toml new file mode 100644 index 00000000..29b097c4 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo_two/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#241826" diff --git a/prefabs/transition/rooms/rm_demo_two/layers/Instances.toml b/prefabs/transition/rooms/rm_demo_two/layers/Instances.toml new file mode 100644 index 00000000..03772da5 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo_two/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo_two" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/transition/rooms/rm_demo_two/room.toml b/prefabs/transition/rooms/rm_demo_two/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/transition/rooms/rm_demo_two/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/transition/scripts/scr_transition/scr_transition.gml b/prefabs/transition/scripts/scr_transition/scr_transition.gml new file mode 100644 index 00000000..6d57a4ca --- /dev/null +++ b/prefabs/transition/scripts/scr_transition/scr_transition.gml @@ -0,0 +1,65 @@ +/// Covering the screen while the room changes. +/// +/// A room change is instant, and instant reads as a glitch. The other half +/// is that `room_goto` must happen while the screen is covered — going first +/// and fading after shows the new room for a frame, which is worse than no +/// transition at all. + +/// @function transition_make(seconds) +/// @description Transition state, idle, with each half taking `seconds`. +function transition_make(seconds) { + return { phase: "idle", t: 0, span: max(0.01, seconds), target: -1 }; +} + +/// @function transition_go(state, target_room) +/// @description Start covering the screen, then enter `target_room` once it +/// is fully covered. Ignored while one is already running, so a held key +/// cannot stack two transitions. +function transition_go(state, target_room) { + if (state.phase != "idle") return false; + state.phase = "out"; + state.t = 0; + state.target = target_room; + return true; +} + +/// @function transition_step(state) +/// @description Advance one frame and perform the room change at the covered +/// moment. Returns true on the frame the transition finishes. +function transition_step(state) { + if (state.phase == "idle") return false; + state.t += delta_time / 1000000; + + if (state.phase == "out") { + if (state.t < state.span) return false; + // Fully covered: the only safe frame to change room on. + if (state.target >= 0 && room_exists(state.target)) { + room_goto(state.target); + } + state.phase = "in"; + state.t = 0; + return false; + } + + if (state.t < state.span) return false; + state.phase = "idle"; + state.t = 0; + state.target = -1; + return true; +} + +/// @function transition_busy(state) +/// @description Whether one is running. Gate input on this, or the player +/// keeps playing a room they are leaving. +function transition_busy(state) { + return state.phase != "idle"; +} + +/// @function transition_cover(state) +/// @description How covered the screen is, 0..1 — 0 clear, 1 opaque. +function transition_cover(state) { + if (state.phase == "idle") return 0; + var _f = clamp(state.t / state.span, 0, 1); + return state.phase == "out" ? _f : 1 - _f; +} + diff --git a/prefabs/transition/scripts/scr_transition_draw/scr_transition_draw.gml b/prefabs/transition/scripts/scr_transition_draw/scr_transition_draw.gml new file mode 100644 index 00000000..7d3a088f --- /dev/null +++ b/prefabs/transition/scripts/scr_transition_draw/scr_transition_draw.gml @@ -0,0 +1,42 @@ +/// @function transition_draw_fade(state, colour) +/// @description A flat fade. Draw it in a GUI event, last, so it covers +/// the HUD as well — a transition the score sits on top of is not a +/// transition. +function transition_draw_fade(state, colour) { + var _a = transition_cover(state); + if (_a <= 0) return; + draw_set_colour(colour); + draw_set_alpha(_a); + draw_rectangle(0, 0, display_get_gui_width(), display_get_gui_height(), false); + draw_set_alpha(1); + draw_set_colour(c_white); +} + +/// @function transition_draw_wipe(state, colour, from_left) +/// @description A hard edge crossing the screen instead of a fade. +function transition_draw_wipe(state, colour, from_left) { + var _f = transition_cover(state); + if (_f <= 0) return; + var _w = display_get_gui_width(); + var _h = display_get_gui_height(); + draw_set_colour(colour); + if (from_left) { + draw_rectangle(0, 0, _w * _f, _h, false); + } else { + draw_rectangle(_w * (1 - _f), 0, _w, _h, false); + } + draw_set_colour(c_white); +} + +/// @function transition_draw_bars(state, colour) +/// @description Two bars closing from top and bottom. +function transition_draw_bars(state, colour) { + var _f = transition_cover(state); + if (_f <= 0) return; + var _w = display_get_gui_width(); + var _h = display_get_gui_height(); + draw_set_colour(colour); + draw_rectangle(0, 0, _w, _h / 2 * _f, false); + draw_rectangle(0, _h - _h / 2 * _f, _w, _h, false); + draw_set_colour(c_white); +} diff --git a/prefabs/transition/thumbnail.png b/prefabs/transition/thumbnail.png new file mode 100644 index 00000000..1286b20a Binary files /dev/null and b/prefabs/transition/thumbnail.png differ diff --git a/prefabs/ui_board/LICENSE b/prefabs/ui_board/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/ui_board/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/ui_board/README.md b/prefabs/ui_board/README.md new file mode 100644 index 00000000..a7201542 --- /dev/null +++ b/prefabs/ui_board/README.md @@ -0,0 +1,30 @@ +# ui-board + +A screenful of things you can click, with hover and layout. + +Pointer-driven hotspots with hover, press and disabled states, grid and row layouts, selection and answer slots. The catalog holds roughly 1,500 UI and icon sprites with no gameplay layer that can use them; this is that layer. + +## Quick usage + +Once added, the prefab is namespaced under its folder: + +```gml +board = ui_board_make(); +::ui_board::ui_add(board, "play", 0, 0, 0, 0, "Play"); +::ui_board::ui_layout_grid(board, 1, 320, 200, 320, 64); +``` + +Called from inside the prefab's own scripts the names are unqualified; +from your project they take the `::ui_board::` prefix. + +## Requires + +- `kernel` +- `feel` + +These install alongside rather than being folded in, so several +prefabs requiring the same one share a single copy. + +## Attribution & license + +© Opera Software — MIT (see `LICENSE`). diff --git a/prefabs/ui_board/objects/obj_demo/Create.gml b/prefabs/ui_board/objects/obj_demo/Create.gml new file mode 100644 index 00000000..ca9d3d59 --- /dev/null +++ b/prefabs/ui_board/objects/obj_demo/Create.gml @@ -0,0 +1,7 @@ +// Standalone demo. ui-board's requirements are not installed here, so +// `gmx validate` warns about the ::kernel:: and ::feel:: calls inside +// the pack — that is the intended "declared but absent" signal. +board = ui_board_make(); +for (var i = 0; i < 6; i++) ui_add(board, "spot" + string(i), 0, 0, 0, 0, "Option " + string(i + 1), i); +ui_layout_grid(board, 3, 80, 120, 220, 90); +last = "none"; diff --git a/prefabs/ui_board/objects/obj_demo/Draw_GUI.gml b/prefabs/ui_board/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..424a23e9 --- /dev/null +++ b/prefabs/ui_board/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,2 @@ +ui_draw(board); +draw_text(80, 60, "ui-board demo — hover and click; last: " + last); diff --git a/prefabs/ui_board/objects/obj_demo/Step.gml b/prefabs/ui_board/objects/obj_demo/Step.gml new file mode 100644 index 00000000..9c10baec --- /dev/null +++ b/prefabs/ui_board/objects/obj_demo/Step.gml @@ -0,0 +1,2 @@ +var _hit = ui_update(board); +if (_hit != "") last = _hit; diff --git a/prefabs/ui_board/objects/obj_demo/object.toml b/prefabs/ui_board/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/ui_board/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/ui_board/prefab.toml b/prefabs/ui_board/prefab.toml new file mode 100644 index 00000000..e67352f0 --- /dev/null +++ b/prefabs/ui_board/prefab.toml @@ -0,0 +1,13 @@ +schema_version = 1 +name = "Interactive UI board" + +# A library: installed under `prefabs//` in the target project and +# called as `::ui_board::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" +requires = ["kernel", "feel"] + +description = "Pointer-driven hotspots with hover, press and disabled states, grid and row layouts, selection and answer slots. The catalog holds roughly 1,500 UI and icon sprites with no gameplay layer that can use them; this is that layer." +category = "library" +tags = ["capability:ui", "capability:pointer", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] diff --git a/prefabs/ui_board/project.toml b/prefabs/ui_board/project.toml new file mode 100644 index 00000000..2c40462f --- /dev/null +++ b/prefabs/ui_board/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "ui-board" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/ui_board/rooms/rm_demo/layers/Background.toml b/prefabs/ui_board/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/ui_board/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/ui_board/rooms/rm_demo/layers/Instances.toml b/prefabs/ui_board/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..2c5f18a7 --- /dev/null +++ b/prefabs/ui_board/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,10 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/ui_board/rooms/rm_demo/room.toml b/prefabs/ui_board/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/ui_board/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/ui_board/scripts/scr_ui_board/scr_ui_board.gml b/prefabs/ui_board/scripts/scr_ui_board/scr_ui_board.gml new file mode 100644 index 00000000..cefe480f --- /dev/null +++ b/prefabs/ui_board/scripts/scr_ui_board/scr_ui_board.gml @@ -0,0 +1,56 @@ +/// A screenful of interactive objects. +/// +/// Everything is in GUI space, because that is where the pointer is +/// reported and where a HUD is drawn — mixing the two spaces is the +/// bug this pack exists to stop each template rediscovering. +/// +/// Hover animation runs through ::feel::, and the pointer through +/// ::kernel::, so this pack owns layout and hit-testing and nothing else. + +/// @function ui_board_make() +/// @description An empty board. +function ui_board_make() { + return { spots: [], hot: "", pressed: "", selected: "" }; +} + +/// @function ui_add(board, id, x, y, w, h, label, payload) +/// @description Add a hotspot. `payload` is whatever the caller wants +/// back when it is clicked — an answer index, an item id, a callback. +function ui_add(board, id, x, y, w, h, label = "", payload = undefined) { + var _spot = { + id: id, x: x, y: y, w: w, h: h, + label: label, payload: payload, + enabled: true, hover: 0, tint: c_white + }; + array_push(board.spots, _spot); + return _spot; +} + +/// @function ui_get(board, id) +function ui_get(board, id) { + for (var i = 0; i < array_length(board.spots); i++) { + if (board.spots[i].id == id) return board.spots[i]; + } + return undefined; +} + +/// @function ui_clear(board) +function ui_clear(board) { + board.spots = []; + board.hot = ""; + board.pressed = ""; + board.selected = ""; +} + +/// @function ui_set_enabled(board, id, enabled) +function ui_set_enabled(board, id, enabled) { + var _s = ui_get(board, id); + if (_s != undefined) _s.enabled = enabled; +} + +/// @function ui_set_tint(board, id, colour) +function ui_set_tint(board, id, colour) { + var _s = ui_get(board, id); + if (_s != undefined) _s.tint = colour; +} + diff --git a/prefabs/ui_board/scripts/scr_ui_board_draw/scr_ui_board_draw.gml b/prefabs/ui_board/scripts/scr_ui_board_draw/scr_ui_board_draw.gml new file mode 100644 index 00000000..e4cf4f8f --- /dev/null +++ b/prefabs/ui_board/scripts/scr_ui_board_draw/scr_ui_board_draw.gml @@ -0,0 +1,61 @@ +/// @function ui_update(board) +/// @description Advance hover animation and hit-test the pointer. +/// Returns the id of the hotspot clicked this frame, or "" — so the +/// caller's Step reads as `var hit = ui_update(board); if (hit != "")`. +function ui_update(board) { + var _p = ::kernel::kernel_pointer(); + var _dt = delta_time / 1000000; + var _clicked = ""; + + board.hot = ""; + for (var i = 0; i < array_length(board.spots); i++) { + var _s = board.spots[i]; + var _over = _s.enabled && ui_contains(_s, _p.x, _p.y); + if (_over) board.hot = _s.id; + + // Hover eases in faster than it eases out; a menu that decays + // slowly feels responsive, one that snaps back feels twitchy. + var _target = _over ? 1 : 0; + var _rate = _over ? 8 : 5; + _s.hover = _s.hover + (_target - _s.hover) * min(1, _dt * _rate); + + if (_over && ::kernel::kernel_action_pressed("confirm")) { + _clicked = _s.id; + board.pressed = _s.id; + ::feel::feel_hitstop(0.04); + } + } + return _clicked; +} + +/// @function ui_draw(board) +/// @description Draw every hotspot. Plain rectangles by design: a +/// template overrides the look by drawing its own sprites over the same +/// rects, and gets working hit-testing either way. +function ui_draw(board) { + for (var i = 0; i < array_length(board.spots); i++) { + var _s = board.spots[i]; + var _lift = ::feel::feel_tween(0, 4, _s.hover, "out_quad"); + var _x = _s.x; + var _y = _s.y - _lift; + + var _fill = _s.enabled ? merge_colour(c_dkgray, _s.tint, 0.25 + 0.35 * _s.hover) + : c_dkgray; + draw_set_alpha(_s.enabled ? 1 : 0.4); + draw_set_colour(_fill); + draw_rectangle(_x, _y, _x + _s.w, _y + _s.h, false); + draw_set_colour(_s.enabled ? _s.tint : c_gray); + draw_rectangle(_x, _y, _x + _s.w, _y + _s.h, true); + + if (_s.label != "") { + draw_set_halign(fa_center); + draw_set_valign(fa_middle); + draw_set_colour(c_white); + draw_text_ext(_x + _s.w / 2, _y + _s.h / 2, _s.label, 20, _s.w - 16); + draw_set_halign(fa_left); + draw_set_valign(fa_top); + } + draw_set_alpha(1); + draw_set_colour(c_white); + } +} diff --git a/prefabs/ui_board/scripts/scr_ui_board_layout/scr_ui_board_layout.gml b/prefabs/ui_board/scripts/scr_ui_board_layout/scr_ui_board_layout.gml new file mode 100644 index 00000000..459a8253 --- /dev/null +++ b/prefabs/ui_board/scripts/scr_ui_board_layout/scr_ui_board_layout.gml @@ -0,0 +1,22 @@ +/// @function ui_layout_grid(board, cols, x, y, w, h, gap) +/// @description Re-place every hotspot on a `cols`-wide grid starting at +/// (`x`, `y`). Called after adding, so a caller never computes a +/// position — which is what makes "four answers" and "nine answers" the +/// same code. +function ui_layout_grid(board, cols, x, y, w, h, gap = 12) { + for (var i = 0; i < array_length(board.spots); i++) { + var _s = board.spots[i]; + var _col = i % cols; + var _row = i div cols; + _s.x = x + _col * (w + gap); + _s.y = y + _row * (h + gap); + _s.w = w; + _s.h = h; + } +} + +/// @function ui_contains(spot, px, py) +function ui_contains(spot, px, py) { + return (px >= spot.x && py >= spot.y && px <= spot.x + spot.w && py <= spot.y + spot.h); +} + diff --git a/prefabs/ui_board/thumbnail.png b/prefabs/ui_board/thumbnail.png new file mode 100644 index 00000000..26db8dc2 Binary files /dev/null and b/prefabs/ui_board/thumbnail.png differ diff --git a/prefabs/wave/LICENSE b/prefabs/wave/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/wave/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/wave/README.md b/prefabs/wave/README.md new file mode 100644 index 00000000..699b8643 --- /dev/null +++ b/prefabs/wave/README.md @@ -0,0 +1,3 @@ +# wave + +How often things arrive, and how that gets worse. diff --git a/prefabs/wave/objects/obj_demo/Create.gml b/prefabs/wave/objects/obj_demo/Create.gml new file mode 100644 index 00000000..eba12963 --- /dev/null +++ b/prefabs/wave/objects/obj_demo/Create.gml @@ -0,0 +1,4 @@ +// A curve you can watch tighten: the marks are spawns, the bar is the +// current wave, and the interval keeps falling until it hits the floor. +curve = wave_make(1.2, 0.82, 0.15, 6); +marks = []; diff --git a/prefabs/wave/objects/obj_demo/Draw_GUI.gml b/prefabs/wave/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..93d01ff1 --- /dev/null +++ b/prefabs/wave/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,19 @@ +draw_text(40, 30, "wave demo — SPACE resets the curve"); +draw_text(40, 70, "wave " + string(wave_number(curve)) + + " every " + string_format(wave_interval(curve), 1, 2) + "s" + + " batch " + string(wave_budget(curve, 2))); + +draw_rectangle(40, 100, 40 + 400 * wave_progress(curve), 116, false); +draw_rectangle(40, 100, 440, 116, true); + +// Every spawn of the last six seconds, as a tick on a timeline. The ticks +// visibly crowd together as the interval falls. +var _now = current_time; +draw_rectangle(40, 200, 440, 202, false); +for (var i = 0; i < array_length(marks); i++) { + var _age = (_now - marks[i]) / 1000; + if (_age > 6) continue; + var _px = 440 - (_age / 6) * 400; + draw_rectangle(_px - 1, 186, _px + 1, 216, false); +} +draw_text(40, 240, "spawns, last six seconds"); diff --git a/prefabs/wave/objects/obj_demo/Step.gml b/prefabs/wave/objects/obj_demo/Step.gml new file mode 100644 index 00000000..44d42ebc --- /dev/null +++ b/prefabs/wave/objects/obj_demo/Step.gml @@ -0,0 +1,8 @@ +if (wave_step(curve)) { + array_push(marks, current_time); + if (array_length(marks) > 60) array_delete(marks, 0, 1); +} +if (keyboard_check_pressed(vk_space)) { + wave_reset(curve); + marks = []; +} diff --git a/prefabs/wave/objects/obj_demo/object.toml b/prefabs/wave/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/wave/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/wave/prefab.toml b/prefabs/wave/prefab.toml new file mode 100644 index 00000000..a98d6fa6 --- /dev/null +++ b/prefabs/wave/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Waves and difficulty" + +# A library: installed under `prefabs//` in the target project and +# called as `::wave::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "A spawn interval that tightens each wave down to a floor, a wave counter and progress through the current one, and a per-wave batch size so the curve keeps climbing after the interval bottoms out. Deliberately knows nothing about what spawns: the same curve drives enemies, obstacles or cards." +category = "library" +tags = ["capability:difficulty", "capability:waves", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "`wave_reset` on a new run, or the second run starts at the difficulty the first one ended at.", +] diff --git a/prefabs/wave/project.toml b/prefabs/wave/project.toml new file mode 100644 index 00000000..0bd7e63c --- /dev/null +++ b/prefabs/wave/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "wave" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/wave/rooms/rm_demo/layers/Background.toml b/prefabs/wave/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/wave/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/wave/rooms/rm_demo/layers/Instances.toml b/prefabs/wave/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/wave/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/wave/rooms/rm_demo/room.toml b/prefabs/wave/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/wave/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/wave/scripts/scr_wave/scr_wave.gml b/prefabs/wave/scripts/scr_wave/scr_wave.gml new file mode 100644 index 00000000..8ce9c87f --- /dev/null +++ b/prefabs/wave/scripts/scr_wave/scr_wave.gml @@ -0,0 +1,52 @@ +/// The difficulty curve: how often things arrive, and how that changes. +/// +/// Separate from spawning on purpose. This decides *when* and *how many*; +/// what appears is the game's business, and keeping the two apart is what +/// lets the same curve drive enemies, obstacles or cards. + +/// @function wave_make(interval, scale, floor, wave_seconds) +/// @description A curve starting at `interval` seconds between spawns, +/// multiplied by `scale` each wave, never below `floor`, with a new wave +/// every `wave_seconds`. `scale` below 1 gets harder; above 1 gets easier. +function wave_make(interval, scale, floor, wave_seconds) { + return { + base: interval, + scale: scale, + floor: max(0.016, floor), + wave_seconds: wave_seconds, + wave: 1, + next: interval, + wave_left: wave_seconds, + elapsed: 0, + }; +} + +/// @function wave_step(state) +/// @description Advance one frame. Returns true on the frames a spawn is +/// due — at most once per frame, so a caller can spawn unconditionally on +/// a true without checking anything else. +function wave_step(state) { + var _dt = delta_time / 1000000; + state.elapsed += _dt; + + state.wave_left -= _dt; + if (state.wave_left <= 0) { + state.wave += 1; + state.wave_left += state.wave_seconds; + } + + state.next -= _dt; + if (state.next > 0) return false; + state.next += wave_interval(state); + return true; +} + +/// @function wave_reset(state) +/// @description Back to wave 1. What a new run needs — without it the +/// second run starts at the difficulty the first one ended at. +function wave_reset(state) { + state.wave = 1; + state.next = state.base; + state.wave_left = state.wave_seconds; + state.elapsed = 0; +} diff --git a/prefabs/wave/scripts/scr_wave_read/scr_wave_read.gml b/prefabs/wave/scripts/scr_wave_read/scr_wave_read.gml new file mode 100644 index 00000000..1cc792af --- /dev/null +++ b/prefabs/wave/scripts/scr_wave_read/scr_wave_read.gml @@ -0,0 +1,27 @@ +/// @function wave_interval(state) +/// @description Seconds between spawns at the current wave, floor applied. +function wave_interval(state) { + return max(state.floor, state.base * power(state.scale, state.wave - 1)); +} + +/// @function wave_number(state) +/// @description Which wave it is, counting from 1 — the number to show. +function wave_number(state) { + return state.wave; +} + +/// @function wave_progress(state) +/// @description How far through the current wave, 0..1. For a bar, or to +/// hold a boss back until the wave is nearly over. +function wave_progress(state) { + return 1 - state.wave_left / state.wave_seconds; +} + +/// @function wave_budget(state, base_count) +/// @description How many to spawn at once at this wave — `base_count` +/// growing with the wave number. Raising the count as well as the rate is +/// what stops a curve flattening the moment it hits the interval floor. +function wave_budget(state, base_count) { + return base_count + floor((state.wave - 1) / 3); +} + diff --git a/prefabs/wave/thumbnail.png b/prefabs/wave/thumbnail.png new file mode 100644 index 00000000..5687772d Binary files /dev/null and b/prefabs/wave/thumbnail.png differ diff --git a/prefabs/zone/LICENSE b/prefabs/zone/LICENSE new file mode 100644 index 00000000..4bee0fbe --- /dev/null +++ b/prefabs/zone/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Opera Software + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/prefabs/zone/README.md b/prefabs/zone/README.md new file mode 100644 index 00000000..3c75a8bf --- /dev/null +++ b/prefabs/zone/README.md @@ -0,0 +1,3 @@ +# zone + +Rectangles and circles that notice the frame you enter or leave. diff --git a/prefabs/zone/objects/obj_demo/Create.gml b/prefabs/zone/objects/obj_demo/Create.gml new file mode 100644 index 00000000..70ba0284 --- /dev/null +++ b/prefabs/zone/objects/obj_demo/Create.gml @@ -0,0 +1,3 @@ +door = zone_rect(120, 140, 380, 300); +well = zone_circle(680, 300, 110); +log_lines = []; diff --git a/prefabs/zone/objects/obj_demo/Draw.gml b/prefabs/zone/objects/obj_demo/Draw.gml new file mode 100644 index 00000000..c12a23cf --- /dev/null +++ b/prefabs/zone/objects/obj_demo/Draw.gml @@ -0,0 +1,5 @@ +draw_set_colour(door.inside ? c_lime : c_white); +zone_draw(door); +draw_set_colour(well.inside ? c_lime : c_white); +zone_draw(well); +draw_set_colour(c_white); diff --git a/prefabs/zone/objects/obj_demo/Draw_GUI.gml b/prefabs/zone/objects/obj_demo/Draw_GUI.gml new file mode 100644 index 00000000..df606d90 --- /dev/null +++ b/prefabs/zone/objects/obj_demo/Draw_GUI.gml @@ -0,0 +1,6 @@ +draw_text(40, 30, "zone demo — move the mouse in and out; SPACE forgets what was inside"); +draw_text(40, 60, "distance to the door: " + + string(round(zone_nearest_edge(door, mouse_x, mouse_y)))); +for (var i = 0; i < array_length(log_lines); i++) { + draw_text(700, 60 + i * 20, log_lines[i]); +} diff --git a/prefabs/zone/objects/obj_demo/Step.gml b/prefabs/zone/objects/obj_demo/Step.gml new file mode 100644 index 00000000..596cb778 --- /dev/null +++ b/prefabs/zone/objects/obj_demo/Step.gml @@ -0,0 +1,14 @@ +// One call per zone per frame: `zone_track` advances state, so asking +// twice in a frame would eat the transition. +var _d = zone_track(door, mouse_x, mouse_y); +var _w = zone_track(well, mouse_x, mouse_y); + +if (_d == "enter" || _d == "exit") array_push(log_lines, "door " + _d); +if (_w == "enter" || _w == "exit") array_push(log_lines, "well " + _w); +while (array_length(log_lines) > 8) array_delete(log_lines, 0, 1); + +if (keyboard_check_pressed(vk_space)) { + zone_reset(door); + zone_reset(well); + array_push(log_lines, "reset"); +} diff --git a/prefabs/zone/objects/obj_demo/object.toml b/prefabs/zone/objects/obj_demo/object.toml new file mode 100644 index 00000000..e79994fb --- /dev/null +++ b/prefabs/zone/objects/obj_demo/object.toml @@ -0,0 +1 @@ +#:schema ../../.gm-schema/v1.json#object diff --git a/prefabs/zone/prefab.toml b/prefabs/zone/prefab.toml new file mode 100644 index 00000000..c30fec65 --- /dev/null +++ b/prefabs/zone/prefab.toml @@ -0,0 +1,16 @@ +schema_version = 1 +name = "Trigger regions" + +# A library: installed under `prefabs//` in the target project and +# called as `::zone::`, rather than emitted as source you +# edit. Namespacing is what stops two libraries colliding on a name. +install = "namespace" + +description = "Rectangular and circular regions with enter/exit/inside/outside tracking, so a trigger fires on the single frame the answer changed rather than every frame the player stands in it. Plus distance to the edge, for a prompt that fades in on approach, and a reset so a player who died inside a trigger can set it off again." +category = "library" +tags = ["capability:triggers", "capability:regions", "role:library"] +exclude = ["objects/obj_demo", "rooms/rm_demo"] + +steps = [ + "Call `zone_track` once per frame per zone — it advances state, so calling it twice in a frame eats the transition.", +] diff --git a/prefabs/zone/project.toml b/prefabs/zone/project.toml new file mode 100644 index 00000000..07a26515 --- /dev/null +++ b/prefabs/zone/project.toml @@ -0,0 +1,7 @@ +#:schema .gm-schema/v1.json#project + +schema_version = 1 +name = "zone" + +[room_order] +order = ["rm_demo"] diff --git a/prefabs/zone/rooms/rm_demo/layers/Background.toml b/prefabs/zone/rooms/rm_demo/layers/Background.toml new file mode 100644 index 00000000..ca03e442 --- /dev/null +++ b/prefabs/zone/rooms/rm_demo/layers/Background.toml @@ -0,0 +1,7 @@ +#:schema ../../../.gm-schema/v1.json#layer + +kind = "background" +depth = 100 + +[background] +colour = "#12141C" diff --git a/prefabs/zone/rooms/rm_demo/layers/Instances.toml b/prefabs/zone/rooms/rm_demo/layers/Instances.toml new file mode 100644 index 00000000..e8ef8358 --- /dev/null +++ b/prefabs/zone/rooms/rm_demo/layers/Instances.toml @@ -0,0 +1,9 @@ +#:schema ../../../.gm-schema/v1.json#layer +kind = "instances" +depth = 0 + +[[entries]] +id = "inst_demo" +object = "obj_demo" +x = 0.0 +y = 0.0 diff --git a/prefabs/zone/rooms/rm_demo/room.toml b/prefabs/zone/rooms/rm_demo/room.toml new file mode 100644 index 00000000..47f5e9f6 --- /dev/null +++ b/prefabs/zone/rooms/rm_demo/room.toml @@ -0,0 +1,4 @@ +#:schema ../../.gm-schema/v1.json#room + +size = { width = 960, height = 540 } +view = { width = 960, height = 540 } diff --git a/prefabs/zone/scripts/scr_zone/scr_zone.gml b/prefabs/zone/scripts/scr_zone/scr_zone.gml new file mode 100644 index 00000000..4518f517 --- /dev/null +++ b/prefabs/zone/scripts/scr_zone/scr_zone.gml @@ -0,0 +1,56 @@ +/// Regions that notice when something enters or leaves them. +/// +/// The tracking is the point. Testing "is the player inside" every frame is +/// easy and gives you a door that reopens sixty times a second; what a +/// trigger needs is the single frame the answer *changed*. + +/// @function zone_rect(x1, y1, x2, y2) +/// @description A rectangular region, corners in any order. +function zone_rect(x1, y1, x2, y2) { + return { + kind: "rect", + x1: min(x1, x2), y1: min(y1, y2), + x2: max(x1, x2), y2: max(y1, y2), + inside: false, + }; +} + +/// @function zone_circle(x, y, radius) +/// @description A circular region centred on a point. +function zone_circle(x, y, radius) { + return { kind: "circle", cx: x, cy: y, r: radius, inside: false }; +} + +/// @function zone_contains(zone, px, py) +/// @description Whether the point is in the region right now. Stateless — +/// use `zone_track` when you care about the transition. +function zone_contains(zone, px, py) { + if (zone.kind == "circle") { + return point_distance(px, py, zone.cx, zone.cy) <= zone.r; + } + return px >= zone.x1 && px <= zone.x2 && py >= zone.y1 && py <= zone.y2; +} + +/// @function zone_any_inside(zone, obj) +/// @description Whether any instance of `obj` is in the region, and which. +/// Returns the instance or `noone`. +function zone_any_inside(zone, obj) { + with (obj) { + if (zone_contains(zone, x, y)) return id; + } + return noone; +} + +/// @function zone_nearest_edge(zone, px, py) +/// @description Distance from a point to the region — 0 inside it. For a +/// prompt that fades in as the player approaches. +function zone_nearest_edge(zone, px, py) { + if (zone_contains(zone, px, py)) return 0; + if (zone.kind == "circle") { + return point_distance(px, py, zone.cx, zone.cy) - zone.r; + } + var _dx = max(zone.x1 - px, 0, px - zone.x2); + var _dy = max(zone.y1 - py, 0, py - zone.y2); + return point_distance(0, 0, _dx, _dy); +} + diff --git a/prefabs/zone/scripts/scr_zone_draw/scr_zone_draw.gml b/prefabs/zone/scripts/scr_zone_draw/scr_zone_draw.gml new file mode 100644 index 00000000..ce5ed851 --- /dev/null +++ b/prefabs/zone/scripts/scr_zone_draw/scr_zone_draw.gml @@ -0,0 +1,10 @@ +/// @function zone_draw(zone) +/// @description Outline it. Debug only — a trigger you cannot see is a +/// trigger you will misplace. +function zone_draw(zone) { + if (zone.kind == "circle") { + draw_circle(zone.cx, zone.cy, zone.r, true); + return; + } + draw_rectangle(zone.x1, zone.y1, zone.x2, zone.y2, true); +} diff --git a/prefabs/zone/scripts/scr_zone_track/scr_zone_track.gml b/prefabs/zone/scripts/scr_zone_track/scr_zone_track.gml new file mode 100644 index 00000000..b87d35b7 --- /dev/null +++ b/prefabs/zone/scripts/scr_zone_track/scr_zone_track.gml @@ -0,0 +1,28 @@ +/// @function zone_track(zone, px, py) +/// @description Advance the region by one frame and return what happened: +/// "enter" and "exit" on the single frame each occurs, otherwise "inside" or +/// "outside". Call once per frame per zone, and switch on the result. +function zone_track(zone, px, py) { + var _now = zone_contains(zone, px, py); + var _was = zone.inside; + zone.inside = _now; + if (_now && !_was) return "enter"; + if (!_now && _was) return "exit"; + return _now ? "inside" : "outside"; +} + +/// @function zone_entered(zone, px, py) +/// @description `zone_track` reduced to the one question most triggers ask. +/// Still advances the zone, so do not call both in the same frame. +function zone_entered(zone, px, py) { + return zone_track(zone, px, py) == "enter"; +} + +/// @function zone_reset(zone) +/// @description Forget whether anything was inside, so the next frame inside +/// counts as an entry again. What a room restart needs — otherwise a player +/// who died inside a trigger never re-triggers it. +function zone_reset(zone) { + zone.inside = false; +} + diff --git a/prefabs/zone/thumbnail.png b/prefabs/zone/thumbnail.png new file mode 100644 index 00000000..01fc2f5c Binary files /dev/null and b/prefabs/zone/thumbnail.png differ