oriverse-engine 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,25 @@
1
1
 
2
2
  >>>>>>>>>>>>
3
3
  Weave World Description Language Documentation
4
- Version:29Jul26-23:56-13158
4
+ Version:19Aug26-09:37-59549
5
5
  >>>>>>>>>>>>
6
6
 
7
7
  <weave_language>
8
8
  - Weave is used to code on Oriverse (syntax similar to Swift for logic, plus scene declarations at top level).
9
9
  - A Weave file has two kinds of top-level content:
10
- - scene declarations: `object`, `saved`, `physics`, `crowd`, `display`, `region` — describe what exists in the world.
10
+ - scene declarations: `object`, `saved`, `physics`, `crowd`, `display`, `region`, and lights `pointLight`/`spotLight`/`areaLight` — describe what exists in the world.
11
11
  - class code: `#class <Name> ... #end` blocks of logic; each block compiles to bytecode attached to a game object.
12
- - Compiler directives (top-level only): #class, #shader (body is WGSL), #texture (CPU-baked procedural PBR texture; auto-creates `material <Name>` usable as `material = "<Name>"`; defaults to triplanar/repeat/opaque, supports `mapping = uv;`, `wrap = clamp;`, `render_mode = cutout;`, and scalar `out.alpha`; see <texture_dsl_usage>; body authoring in the texture_dsl skill), #mesh (local CPU-baked procedural static mesh model `mesh.<Name>`; see <mesh_dsl_usage>; body authoring in the mesh_dsl skill), #tree (procedural tree species `tree.<Name>`; see <tree_dsl_usage>), #forest (scattered-tree region `forest.<Name>`; see <forest_dsl_usage>), #particles (local particle effect JSON asset `effect.<Name>`; `particleTextureName` takes `"flipbook.<word>"` for a shared-library animated atlas, `"texture.<Texture>.color"` for a local #texture, or `"image.<User>.<Name>"` for a known ori image asset; see particle_vfx skill), #adjuster (user-tunable look knobs; see <adjuster_usage>), #rules (see <world_rules>), #end, #set <Name> = <Number> (TileSize, Realistic), #doc_version <N> (authoring doc version; see <updating_old_worlds>).
12
+ - Compiler directives (top-level only): #class, #shader (custom WGSL on the engine lit material — lighting/shadows/IBL stay engine-owned; `@vertex { }` mutates ctx.world_pos for wind/sway, fragment writes out.base_color/world_normal/orm; bind via `material M { shader = "S" }`; live knobs via SetMaterialParameterNumber; body authoring in the shader_dsl skill), #texture (CPU-baked procedural PBR texture; auto-creates `material <Name>` usable as `material = "<Name>"`; defaults to triplanar/repeat/opaque, supports `mapping = uv;`, `wrap = clamp;`, `render_mode = cutout;`, and scalar `out.alpha`; see <texture_dsl_usage>; body authoring in the texture_dsl skill), #mesh (local CPU-baked procedural static mesh model `mesh.<Name>`; see <mesh_dsl_usage>; body authoring in the mesh_dsl skill), #tree (procedural tree species `tree.<Name>`; see <tree_dsl_usage>), #forest (scattered-tree region `forest.<Name>`; see <forest_dsl_usage>), #scatter (scattered-model region `scatter.<Name>`, display-only, no collision; see <scatter_dsl_usage>), #particles (local particle effect JSON asset `effect.<Name>`; `particleTextureName` takes `"flipbook.<word>"` for a shared-library animated atlas, `"texture.<Texture>.color"` for a local #texture, or `"image.<User>.<Name>"` for a known ori image asset; see particle_vfx skill), #sound (local CPU-baked procedural sound asset `sound.<name>`; see <sound_dsl_usage>), #adjuster (user-tunable look knobs; see <adjuster_usage>), #rules (see <world_rules>), #end, #set <Name> = <Number> (TileSize, Realistic; texture policy, display-only: `#set TextureMaxResolution 1024` caps every market texture this world downloads to <=1024px [picks the market's smaller tier], `#set TextureFullResolutionAssets "model.a.x, model.b.y"` always loads listed assets at full resolution, `#set TextureFullResolutionUpgrade 1` enables the desktop post-load full-res texture re-fetch [default off: the world keeps its load-time texture tier — project-load assets land quarter-res; wasm/mobile never run the upgrade]), #doc_version <N> (authoring doc version; see <updating_old_worlds>).
13
13
  - Scene numeric fields support constant arithmetic: `+`, `-`, `*`, `/`, unary sign, parentheses.
14
14
  - One statement per line; separate statements on the same line with `;`. An empty list literal `[]` needs a List-typed context, e.g. `var H: List<Number> = []`.
15
15
  - Numbers are deterministic 48.16 fixed-point; tiny fractions lose precision — track small quantities in a smaller unit (e.g. hundredths of a cm as integers).
16
16
  - No recursion: direct or mutual func call cycles are a compile error; rewrite as loops (a List can serve as an explicit stack).
17
17
  - Scene object declarations can set position, rotation, size, height, model; model is an Oriverse URL like `ori.username.modelname`; attach a class with `:` (e.g. `object Foo : MyClass { ... }`).
18
18
  - Default mesh-backed `object`/`physics`/`crowd`/`display`: `position=(x,y,0)` means bottom-center pivot on ground Z=0. Effects/lights/regions/terrain/groups/canvases use center placement.
19
- - For axis-aligned cube walls/floors/platforms, prefer `bounds_min=(x1,y1,z1)` + `bounds_max=(x2,y2,z2)` over `position`+`size`; it names exact world-space box corners and avoids center/half-size mistakes. Example: `object NorthWall { model="cube" bounds_min=(-1500,1100,0) bounds_max=(1500,1180,280) }`. Do not combine with position/rotation/size/height/tile fields.
20
- - Grass on boxes: `object`/`display` clauses with model = "cube" accept grass = true (blades on the top face; display-only, no collision; global SetGrass* look knobs apply). Optional: grass_border = round|noise (edge falloff, default full face), grass_border_noise_scale = 220 (cm, noise feature size). Yaw rotation only - tilted boxes grow nothing.
19
+ - Dynamic lights: `pointLight Lamp { position=(x,y,z) radius=600 intensity=5 color=#ffd9a0 }`; `spotLight` shines along local +X (aim with rotation), `areaLight` emits from its `size` rectangle. `radius` = falloff range in cm; `size` does NOT change a point/spot light (editor gizmo only); `cast_shadow = true` on point/spot only. Nest in `saved` templates to spawn lights from code; for lamps built into a model prefer the mesh `emitter(...)` socket.
20
+ - For axis-aligned cube walls/floors/platforms, prefer `bounds_min=(x1,y1,z1)` + `bounds_max=(x2,y2,z2)` over `position`+`size`; it names exact world-space box corners and avoids center/half-size mistakes. Example: `object NorthWall { model="cube" bounds_min=(-1500,1100,0) bounds_max=(1500,1180,280) }`. Cube boxes only (`model="cube"`; `region_cube` for regions) other models must use position+size. Do not combine with position/rotation/size/height/tile fields.
21
+ - Object size clamps per axis at load: 500m static-collidable, 100m movable-physics, 1km with collision off — tile bigger floors/walls from 500m pieces.
22
+ - Grass on objects: static mesh `object`/`display` clauses accept grass = true (blades on upward faces; steep faces stay bare; display-only, no collision; global SetGrass* look knobs apply; skinned models grow nothing). On model = "cube" blades cover the top face (yaw rotation only) and grass_border = round|noise (edge falloff, default full face) + grass_border_noise_scale = 220 (cm, noise feature size) shape the edge.
21
23
  - Prefer `height = ...` for imported/custom models to preserve proportions; use `ScaleToHeight(...)` at runtime; use `size = ...` only when intentionally stretching X/Y/Z.
22
24
  - "world description" = the serialized text of a world (scene + classes); saved from the editor and pasted back to reload. AI builds these; users paste them in.
23
25
  - `#class player` is auto-attached to every PlayerObject. Do not declare the player in scene. Declaring it REPLACES the built-in default player: call SetDefaultPlayerControlsEnabled(true) in its OnSpawned to keep built-in WASD move + jump, or code a movement loop — with neither, players cannot move (see <camera>).
@@ -34,7 +36,7 @@ Weave World Description Language Documentation
34
36
  - Name aliases by TYPE: `alias.<type>.<name>` with <type> in {model, effect, image, sound}, e.g. `model.goblin`, `effect.explosion`, `image.gold_icon`, `sound.hit`. A typed alias must resolve to a matching asset type.
35
37
  - You usually do NOT know real ori urls. Prefer typed alias placeholders and let the system/human bind real assets later:
36
38
  - All four types may be referenced UNDECLARED or declared bare (`alias sound.hit`, no `=` target); they resolve from the generated libraries below, show a safe placeholder until generation lands (cube / default explosion / placeholder sprite / placeholder clip), and a human can rebind them.
37
- - MODEL aliases resolve from a generated-mesh library: a bare declaration `alias model.tree` (no `=` target, auto-added for undeclared refs too) binds a matching generated mesh by name at load; on a miss it shows a cube while a mesh is generated in the background (available on a later load). Name model aliases with plain singular nouns (`model.tree`, `model.market_stall`, not `model.trees`/`model.bigTreeA`) so library matching works. Want a specific existing mesh to start from or adapt -> the `mesh_library_search` tool returns library `#mesh` source to inline.
39
+ - MODEL aliases resolve from a generated-mesh library: a bare declaration `alias model.tree` (no `=` target, auto-added for undeclared refs too) binds a matching generated mesh by name at load; on a miss it shows a placeholder (cube; the player a mannequin stand-in) while a mesh is generated in the background, swapping in live when it lands. Name model aliases with plain singular nouns (`model.tree`, `model.market_stall`, not `model.trees`/`model.bigTreeA`) so library matching works. Want a specific existing mesh to start from or adapt -> the `mesh_library_search` tool returns library `#mesh` source to inline. A bound word's source is already in the world text (`#mesh gen_<word>` + its `material`/`#texture` clauses) — recolor/restyle by editing those directly. Character-noun words mint as rigged humanoids that auto-play idle/walk/run; the alias status marks minted words `rigged humanoid`.
38
40
  - IMAGE aliases resolve the same way from a generated-UI-image library: bare `alias image.gold_icon` binds a generated icon by name at load; on a miss it shows a placeholder sprite while an icon is generated in the background. Prefer bare image aliases for UI icons; name them by subject (`image.gold_icon`, `image.energy_icon`).
39
41
  - SOUND aliases resolve the same way from a generated-sound library: bare `alias sound.coin_pickup` binds a generated clip by name at load; on a miss it plays a placeholder clip while a sound is generated in the background. Name sound aliases by the event they accompany (`sound.coin_pickup`, `sound.sword_hit`, `sound.door_creak`) so generation prompts come out right.
40
42
  - ANIMATION: referencing an undefined `Ori.anim.canon.<verb>` appends the canonical-clip library's `#clip <verb> on canon` block at load (plain action verbs: `anim.canon.cheer`, `anim.canon.stomp`); it plays on any rigged humanoid via engine retarget. On a library miss the character keeps its current animation; the verb is queued for house minting and binds on a later load. A world-authored `#clip <verb> on canon` always wins.
@@ -46,58 +48,62 @@ Weave World Description Language Documentation
46
48
  </asset_aliases>
47
49
 
48
50
  <asset_word_library>
49
- - Established approved library words per alias namespace, plus `flipbook.<word>` particle textures. PREFER an existing word when one matches the need — it binds instantly with no generation wait or spend. Other words still work: misses show a placeholder and auto-generate in the background (flipbook words are minted deliberately via the flipbook_gen tool instead). Model words are not baked here — query them live with the `mesh_library_search` tool.
50
- image: (none approved yet)
51
- sound: (none approved yet)
52
- effect: (none approved yet)
53
- flipbook: blue_flame bubble_float dust_puff electric_spark ember_glow explosion_burst gray_smoke heart_pulse leaf_flutter magic_sparkle orange_flame poison_cloud snowflake_spin star_twinkle water_splash white_smoke
51
+ - Established library words per alias namespace, plus `flipbook.<word>` particle textures. PREFER an existing word when one matches the need — it binds instantly with no generation wait or spend. Other words still work: misses show a placeholder and auto-generate in the background (flipbook words are minted deliberately via the flipbook_gen tool instead).
52
+ model: abandoned_car alchemist alien_bug alien_hive alley_monster amphora anchor aqua_penguin assembler athlete aunt auntie axe ball banana banana_peel bandage bandit barn barracks barrel barrel_2 base_core baseball_bat battle_car bed bee beehive belt bench bench_2 berry_bush billboard blacksmith boar boat boat_parts bomb bone book boomerang boulder boulder_2 box bread_slice bread_slice_2 brick bridge buddhist_temple bush bush_2 bush_a butter cabbage camp_tent camp_tent_2 campfire canvas_tent cargo_truck carrot cart carved_owl_statue cash_bundle cat cat_blacksmith cat_cart_keeper cat_chef cat_engineer cat_factory_worker cat_merchant cat_rickshaw chair chair_2 checkpoint cheese cheese_2 chest chicken chicken_coop chime_lantern chunk_floor circular_saw clay_buddy cleric cloud clue_item clue_item_2 coin command_center cottage cousin cow crate crate_2 crop crop_plot crossbow crystal crystal_cluster crystal_shard cup customer customer_2 cutting_board cutting_board_2 cyclops dog dragon drill druid dual_blades durian elegant_woman ember_fox enemy_spawn energy_drink evidence_item exp_gem fan farmhouse farmhouse_floor fence finish_arch fish fish_2 fishing_rod flag flag_2 flamethrower flashlight flat_rock_slab flower food_crate forager_hut foundation fountain fox fridge fridge_2 frog frying_pan frying_pan_2 fuel_pump furniture_bookshelf furniture_table furniture_table_2 furniture_wardrobe gargoyle gate ghoul ghoul_2 gilded_hourglass glimmer_crate goblin godfather golden_beak_shaman golden_coin grandma grandpa grape grape_2 grass_rock grass_rock_cliff great_sword green_maple_tree greenhouse grocery_box ground_item guard guard_tower hammer hamster harpy hay_bale hay_bale_2 heal_pickup held_item horned_beast horse hospital_assistant house hungry_mouse hunter_bow hunting_hammer hut ice_shard idle_character invoker_mage iron_node item_crate jam_jar kid killer knife knight ladder lamp lamp_2 lane lantern lantern_2 light_pole log_pile log_pile_2 lumber_camp machine_gun mage magnet market_stall mecha_chameleon medic medicine_crate miner minotaur moss_boar mushroom nephew niece night_watchman ninja_runner npc nurse oak_tree officer owl paddle palm_tree patient pearl_bush pebble person pickaxe pickup_orb pig pine_tree pistol plate plate_2 player_char police_car pond poop projectile pumpkin rabbit rabbit_runner ragdoll_fighter rain_mill rain_totem ranger requiem_gun resident revolver rifle road_sign robot_grunt robot_runner rock rocket rocket_launcher rocksample rope rope_bundle runic_obelisk sandbag sea_mine sedan sentinel_drone shark shield shipping_container shop_counter shop_floor shopkeeper shotgun shuriken signpost signpost_2 signpost_3 skull slime small_rock smg smoke_bomb snail sniper_rifle soil_tile soldier spider sports_car squid stair star star_core statue stepmother stilt_house stone_pile stonecutter_lodge storehouse storm_hearth storm_shrine surgeon survivor sword table tank_hull tent timber_bundle toaster toaster_2 tool_chest torch torch_2 tower training_dummy training_dummy_2 travel_pack tree tribal_cat_elder tribal_cat_villager trophy trunk turret uncle villager volleyball wagon wall warehouse watchtower water_barrel water_well watercan watering_can weapon_rack weapon_rack_2 weapon_shop_sign well wheat wheelbarrow white_tribal_cat_player wizard_staff wood_fence wood_fence_2 wooden_stool worker_robot zebrite_totem zombie
53
+ image: airtank_icon alien_bug angled_player_banner axe_icon bandage_icon bean_icon bean_seeds_icon berries_icon book_icon boss_skull bread_icon bullet_icon cabbage_icon campfire_icon carrot_icon cheese_icon clock_icon coin_icon crop_icon crossed_swords crossed_swords_2 crosshair cute_paw_cursor dash_icon deck_icon deck_icon_2 energy_icon fish_icon fishing_rod_icon flamethrower_icon flint_icon fluffy_sheep_party_portrait forest_ancient_tree_spirit_wide_splash forest_ent gold_coin_icon gold_icon gold_icon_2 hand_icon health_icon health_icon_2 hoe_icon horse_party_portrait hunger_icon inverse_trapezoid_top_bar iron_ore_icon knight_helmet loading_splash log_icon machine_gun_icon mackerel_icon monster_head moon_icon orange_cat_party_portrait panel_frame parsnip_icon parsnip_seeds_icon pickaxe_icon poison_icon potato_icon potato_seeds_icon purple_hive_mind_insect_warrior_wide_splash qa_probe_gem radish_icon revolver_icon rifle_icon robot_face robot_vending_mech_champion_wide_splash rocket_launcher_icon round_table_knight_wide_splash ruby_gem_icon salvage_icon sanity_icon sardine_icon scythe_icon seed_bag_icon seed_icon shotgun_icon skill_dagger_icon skill_fireaura_icon skill_hammer_icon skill_shield_icon skill_stealth_icon snapper_icon snowflake_spin squid_icon steak_icon stone_icon sun_icon tft_alien_portrait_black tft_boss_skull_black tft_clock_black tft_crossed_swords_black tft_forest_portrait_black tft_monster_head_black tft_robot_portrait_black tft_yokai_portrait_black torch_icon tuna_icon twigs_icon undead_death_knight_wide_splash undead_skull water_drop_icon watering_can_icon white_chicken_party_portrait wood_icon yokai_mask yokai_samurai_oni_wide_splash
54
+ sound: ambient_horror apartment_ambience arena_warning baby_raptor_step banana_boomerang baton_hit baton_swing bear_shotgun bite blast body_thump body_thump_2 boost boost_burst bow_hit branch_snap brass_gong cannon_blast cannon_fire car_horn card_draw card_play cash_pickup cash_pickup_2 character_select checkpoint chomp coin_pickup coin_pickup_2 corn_burst countdown countdown_beep countdown_tick crab_blaster crab_hit crab_skill crash crash_burst critical_impact critical_impact_2 croc_machinegun crow_call crowd_cheer crystal_pickup dash_burst dodge dodge_dust dodge_dust_2 dog_bark dog_pistol door_close door_open door_open_2 door_unlock dual_hit earthquake_rumble earthquake_rumble_2 electric_burst electric_shock eliminated energy_empty engine_acceleration engine_idle exit_unlock explosion finish finish_chime fish_caught fishing_cast fishing_reel flamethrower_burst food_complete food_complete_2 front_weapon_activation game_check game_end game_start gameplay_music gameplay_music_2 glimmer_bell gold_pickup golf_cup golf_swing grenade_launcher gunshot hammer_hit hammer_hit_2 heartbeat hit hook_impact hook_impact_2 hook_throw hook_throw_2 item_pickup item_pickup_2 laser_shot laser_shot_2 lasgun_shot level_up level_up_2 level_up_3 level_up_4 level_up_5 line_clear machine_gun machine_gun_reload machine_gun_shot magic_blast medkit_pickup medkit_use menu_music metal_hit metal_hit_2 mine missile_launch mole_sniper monster_attack monster_attack_2 monster_roar monster_roar_2 owl_hoot pickup piece_capture piece_lock piece_move pistol_reload pistol_shot place player_hit player_hurt player_hurt_2 police_siren punch rain_ambience raptor_step ready_up rear_ability_activation repair resource_pickup respawn revive rifle_reload rifle_shot rifle_shot_2 ring_out ringout robot_beam robot_blast robot_charge robot_laser rock_riff rocket_shot rot_loop royal_capture score_boost shield shield_block ship_destroyed shotgun_shot signal_ping siren slab_turn slap slap_hit stone_rotate survivor_step sword_clang sword_clang_2 sword_hit sword_hit_2 tire_skid toast_pop toy_pop trex_step turn_alert ui_click ui_click_2 ui_click_3 ui_click_4 ui_click_5 vehicle_damaged vehicle_destroyed victory victory_fanfare victory_fanfare_2 victory_sting victory_sting_2 vortex_hum wall_deploy wall_pickup warning_siren water_splash wave_crash weapon_switch whistle wind_gust wind_gust_2 wolf_howl wood_impact zebrite_chime zombie_attack zombie_growl zombie_roar
55
+ effect: acid_splash arrow_hit bite_splash blood_hit blood_splash boost_burst cannon_blast capture_burst cartoon_impact cash_sparkle cash_sparkle_2 coin_sparkle collapse_dust crash_burst critical_smoke damage_smoke damage_sparks damage_sparks_2 damage_sparks_3 dash_trail dash_trail_2 dismember dodge_dust dust_puff dust_puff_2 dust_puff_3 electric_burst eliminated_burst explosion explosion_burst finish_confetti fire_burst fireball fireball_burst gold_rush golf_cup golf_hit grenade_blast hammer_impact heal_glow healing_glow hit_burst hook_hit hook_hit_2 impact_flash_critical impact_flash_small impact_shockwave impact_shockwave_2 impact_shockwave_3 impact_sparks ink_cloud item_glow magic_blast magic_sparkle magic_vortex metal_hit meteor_fire mine_burst muzzle_flash objective_pickup player_hit poison_cloud rage_burst rainbow_burst rainbow_burst_2 rapid_slash respawn_burst revive_burst revive_glow rickshaw_burn ringout_explosion rocket_explosion rot_pulse rune_pickup shield_burst shield_pop slap_hit slash_burst slash_burst_2 slash_burst_3 smoke_puff smoke_puff_2 sonar_pulse sparkle_burst sparkle_burst_2 sparkle_burst_3 sparkler speed_burst stun_stars stun_stars_2 stun_stars_3 sword_slash tank_burning tank_explosion terrain_blast top_collision_impact water_burst water_splash wind_leaves
56
+ flipbook: blood_mist blue_flame bubble_float dust_puff electric_spark ember_glow explosion_burst gray_smoke heart_pulse hot_spark_burst leaf_flutter magic_sparkle muzzle_flash_star orange_flame poison_cloud smoke_puff snowflake_spin star_twinkle tracer_streak water_splash white_smoke
54
57
  </asset_word_library>
55
58
 
56
59
  <texture_dsl_usage>
57
60
  - For new worlds using primitive meshes, use texture dsl to paint them.
58
61
  - `#texture Name ... #end` auto-creates triplanar `material Name`. Use it as `material = "Name"` in scene objects, `Material.Name` in Weave code, `terrain.SetTerrainMaterial(Material.Name)`, `terrain.SetTerrainLayers(...)`, `batch.SetBatchMaterial(Material.Name)`, or as a local `#mesh` slot name (`material: "Name"` / `paint(..., material: "Name")`).
59
- - Flat color shorthand: `#texture Wood color=#8B5A2B` creates a non-emissive triplanar `material Wood`; useful for low-poly color-blocking and quick placeholders. Optional `roughness=0..1` and `metallic=0..1` (e.g. `#texture Asphalt color=#1E2024 roughness=0.95`) without a high roughness, dark ground colors read pale from sky reflection.
60
- - Derived material (color variant, no extra texture bake): `material WoodRed { base = "Wood" tint = "#8a2b1e" }` copies shader/textures/params from the base material and multiplies its color by tint (quoted hex). `base` may also name a built-in tintable detail family - WoodDetail, StoneDetail, MetalDetail, FabricDetail, LeatherDetail, PlasterDetail (near-white; tint sets the color) - or a built-in authored surface: StylizedBricks, OldTilesWhite, StylizedGrass, PolishedConcrete. Prefer this over writing a `#texture` body when only the color differs.
61
- - Use #texture directly on primitives, terrain, batches, and local `#mesh` slots. For #mesh material precedence: object/batch override > explicit submesh override > authored local mesh slot material > intrinsic GLB PBR. Imported GLB material names are not auto-bound to world materials. For large terrain/floors, set `tile_m = 2.0;`..`4.0;` so repeats are not too tiny.
62
+ - Masonry patterns (pattern.brick/tile) under the default triplanar mapping project world-space joints; on carved/round geometry (columns, moldings, statues) use a coursing-free variant or `mapping = uv;`.
63
+ - Flat color shorthand: `#texture TeamRed color=#B03A2E` creates a non-emissive triplanar `material TeamRed` for stylized color-blocking and quick placeholders; surfaces meant to read as real should use the built-in library (next clause). Optional `roughness=0..1` and `metallic=0..1` (e.g. `#texture RubberFloor color=#1E2024 roughness=0.95`) without a high roughness, dark ground colors read pale from sky reflection.
64
+ - Built-in material library real textured PBR with zero declarations: `material = "Wood"` on any object/batch/terrain (and `Material.Wood` in code) just works; the engine copies the one-line library clause into the world on save, and a world-declared material of the same name always wins. Library: Wood, WoodDark, Plank, Bark, Stone, Rock, Brick, Dirt, Sand, Metal, MetalDark, Gold, Fabric, Cloth, Rope, Straw, Leather, Foliage, Leaf, Grass, Snow, Ceramic, Bone, Glass, GlassTransparent, Water, Default; authored surfaces StylizedBricks, OldTilesWhite, StylizedGrass, PolishedConcrete, Asphalt. Default to these for anything meant to read as a real surface.
65
+ - Derived material (color variant, no extra texture bake): `material WoodRed { base = "Wood" tint = "#8a2b1e" }` copies shader/textures/params from the base material and multiplies its color by tint (quoted hex). `base` may name any built-in above or a tintable near-white detail family (WoodDetail, StoneDetail, MetalDetail, FabricDetail, LeatherDetail, PlasterDetail). Prefer this over writing a `#texture` body when only the color differs.
66
+ Material clauses also accept `emission = N` (self-lit glow in the material's tinted color; linear: 1 soft, 4 lamp, 16+ neon bloom) and `roughness = 0..1` / `metallic = 0..1` (uniform overrides of the baked surface response) — e.g. `material CyanGlow { base = "MetalDetail" tint = "#19D9FF" emission = 5 }`.
67
+ - Use #texture directly on primitives, terrain, batches, and local `#mesh` slots. For #mesh material precedence: object/batch override > explicit submesh override > authored local mesh slot material > intrinsic GLB PBR. Imported GLB material names are not auto-bound to world materials. `tile_m` = meters per pattern repeat (default 1.0, prop scale): RAISE to `2.0;`..`4.0;` for large terrain/floors so repeats are not too tiny; LOWER to `0.3;`..`0.5;` for small props/panels so the pattern does not print larger than the object. Material clauses accept it too (`material X { base = "Fabric" tileM = 0.4 }`).
62
68
  - Authoring or editing a `#texture` BODY (anything beyond the color=/base= one-liners above): read the `texture_dsl` skill first — statement syntax, builtins (noise/pattern/sdf/filter families), recipes, worked examples. Do not guess builtin names or signatures from memory.
63
69
  - Terrain layer sets: assign scalar `out.height` on every layer of the set (or none) — higher-height texels win at SetTerrainLayers blend borders (rock pushing through grass).
64
70
  - `#terrain_splat Name ... #end` + `terrain.SetTerrainSplat("Name")`: same expression language with terrain inputs `height` (world m), `slope01` (0 flat..1 vertical), `convex` (-1..1), `ao` (0..1), `moist` (0..1), `user` (0..1, fed by `terrain.SetTerrainSplatMask(mask, sx, sy)`, else 0); `out.color` RGB = SetTerrainLayers Steep/High/Low weights (base = leftover). Baked per-terrain; replaces the slope/height auto-bands for hand-authored placement (e.g. `out.color = vec3(smoothstep(0.4, 0.6, slope01), smoothstep(18, 24, height), sat(moist * 2 - convex));`).
65
71
  - Optional scalar `out.grass` in `#terrain_splat` = grass spawn density 0..1 (0 = bare; spawn saturates around 0.4, so mid values still read as full cover) - keeps grass off beaches/cliffs/dry zones from the same rules that place the textures, and thins the auto ground tint with it. Unassigned = full density; an explicit SetTerrainGrassWeightmap overrides it.
66
72
  - Colormap mode: `out.albedo` (vec3, mutually exclusive with `out.color`) makes the bake the FINAL surface color instead of layer weights - full per-pixel control (satellite-style coloring, stylized palettes, drainage streaks from `user`). The base SetTerrainMaterial still supplies normal/ORM detail; SetTerrainLayers weights, band blending, macro tint and the auto grass ground tint are all bypassed. Set `size = 2048;` (up to 4096) for crisp color; `out.grass` still works alongside.
67
- - `#road_network Name ... #end`: a road GRAPH the engine meshes into junction-merged road tiles at load (carriageway, miter-blended intersections, curbs, sidewalks, center dashes, crosswalks). Body statements end with `;`: `cell = 32000;` (tile grid cm, optional), `class = width_cm, sidewalk_cm, curb_w_cm, curb_h_cm, "CarriagewayMat", "CurbMat", "SidewalkMat", "MarkingMat";` (repeatable; edges index classes in order; empty marking name = no markings), `nodes = x, y, x, y, ...;` (cm, repeatable/appends), `edges = nodeA, nodeB, classIndex, ...;` (triplets, repeatable). Pair it with ONE object `object Roads_o { model = "roadnet.<Name>" position = (0, 0, 0) }` the loader expands it into per-tile objects automatically (thin walkable slabs). Prefer this over hand-built per-segment road boxes: junctions come out clean and one graph edit reflows everything.
68
- - `#terrain_bytes "terrain.url" ... #end`: embedded uploaded-terrain heightfield payload (base64 of zlib-compressed terrain bytes) satisfying `terrain X { model = "terrain.url" }` with zero network fetches, so the world is self-contained (headless validation included). Blocks are machine-generated (desktop `embed_terrain` CLI), not hand-authored. Prefer procedural terrain (`SpawnTerrainFromHeights` + `TerrainHeights*`) for new worlds.
69
- - Avoid SetEmissionColor/SetEmissionIntensity/SetAssetSubmeshEmissionIntensity for normal world materials for now; they can make generated scenes too bright. Prefer non-emissive #texture/material colors, and use only tiny low-intensity glow accents when explicitly requested.
73
+ - `#road_network Name ... #end`: a road GRAPH the engine meshes into junction-merged road tiles at load (carriageway, miter-blended intersections, curbs, sidewalks, center dashes, crosswalks). Body statements end with `;`: `cell = 32000;` (tile grid cm, optional), `class = width_cm, sidewalk_cm, curb_w_cm, curb_h_cm, "CarriagewayMat", "CurbMat", "SidewalkMat", "MarkingMat";` (repeatable; edges index classes in order; empty marking name = no markings), `nodes = x, y, x, y, ...;` (cm, repeatable/appends), `edges = nodeA, nodeB, classIndex, ...;` (triplets, repeatable), `curve = a, b, c1x, c1y[, c2x, c2y];` (optional per a→b edge: quadratic/cubic bezier control points in cm; omit = straight; junctions meet curves at their end tangents; too-tight curvature for the class width is a load error). The block spawns the network itself: tiled road meshes plus walkable per-edge slabs (no companion object needed); optional `position = x, y, z;` offsets the whole network. Editor edits fold back into this block on save — re-read it before patching. Prefer this over hand-built per-segment road boxes: junctions come out clean and one graph edit reflows everything.
74
+ - `#terrain_bytes "terrain.url" ... #end`: embedded uploaded-terrain heightfield payload (base64 of zlib-compressed terrain bytes) satisfying `terrain X { model = "terrain.url" }` with zero network fetches, so the world is self-contained offline. Blocks are machine-generated (desktop `embed_terrain` CLI), not hand-authored - and never required to submit: validation resolves uploaded `terrain.` assets itself. Prefer procedural terrain (`SpawnTerrainFromHeights` + `TerrainHeights*`) for new worlds.
75
+ - Emission is linear (1 soft self-lit, 4 lamp, 16+ neon bloom): put it on small accents - lamps, signs, runes, windows; large emissive surfaces wash out the scene, keep those <= 1.
70
76
  </texture_dsl_usage>
71
77
 
72
78
  <adjuster_usage>
73
- - `#adjuster ... #end` (top-level): declares hand-tunable global look knobs shown in the editor's adjuster panel (top-right sliders icon; auto-opens after load). Values apply on load; slider edits fold back into this block on save.
79
+ - `#adjuster ... #end` (top-level): declares hand-tunable global look knobs. Values apply on load; slider edits fold back into this block on save. Sliders: grass/ground knobs in the Terrain inspector (click the terrain), SetWater* in Find > Water, the rest in the Adjust panel (top-right sliders icon; links to both inspectors).
74
80
  - Body lines: `section "Title"` groups knobs after it; `knob <SimCallName> = <value> [range a..b]`; value is a number, or `#rrggbb` for color knobs; `range` overrides the slider range of number knobs. `//` comments ok.
75
81
  - Only whitelisted global sim calls (below) are valid knob names; each at most once. Texture params and ranged #mesh params expose per-texture/per-mesh panel knobs on their own; no #adjuster entry for those.
76
- - Add an #adjuster block whenever the look is a matter of taste the user may want to hand-tune (grass, sun, sky, post color grading).
82
+ - Add an #adjuster block whenever the look is a matter of taste the user may want to hand-tune (grass, ground, water, wind, tree LOD).
83
+ - Lighting/sky/grade knobs (sun color/intensity, skybox tint/saturation/exposure, SSAO, vignette, scene contrast/saturation/exposure) are USER-owned: never write or retune them, as knobs or calls; lighting/mood requests -> Adjust panel.
77
84
  - SetWater* knobs set the INITIAL ocean state: they replay after load-time code, so a `knob SetWaterEnabled = 0` line keeps water off across reloads (the editor's Find > Water delete writes it). Code that changes water after load (OnSpawned, timers, events) still wins at runtime.
78
- - Knobs: SetGrassRootColor (color), SetGrassTipColor (color), SetGrassBrightnessVariationPercent (0..300), SetGrassSizeVariationPercent (0..300), SetGrassHeightMinCm (0..300), SetGrassHeightMaxCm (0..300), SetGrassWavePeriodCm (100..3000), SetGrassWindStrengthCm (0..50), SetGrassWindSpeedPercent (0..300), SetGrassBladeWidthPercent (50..200), SetGrassSpacingCm (0..100), SetTerrainErosionDetailPercent (0..300), SetSunColor (color), SetSunIntensity (0..20), SetSkyboxTint (color), SetSkyboxSaturation (0..3), SetSkyboxExposure (0..3), SetSsaoIntensity (0..3), SetTerrainNormalStrengthPercent (0..300), SetContrast (0..4), SetSaturation (0..4), SetExposure (0..3), SetWaterEnabled (0..1), SetWaterSeaLevelCm (-2000..10000), SetWaterAmplitudeCm (0..500), SetWaterWavelengthCm (100..20000), SetWaterSteepness (0..1), SetWaterDirectionDeg (0..360), SetWaterSpeedPercent (0..400), SetWaterShallowColor (color), SetWaterDeepColor (color), SetWaterColorMixRangeCm (1..2000), SetWaterTransparency (0..1), SetWaterReflectionStrength (0..2), SetWaterReflectionDistortion (0..2)
85
+ - Knobs: SetGrassRootColor (color), SetGrassTipColor (color), SetGrassBrightnessVariationPercent (0..300), SetGrassSizeVariationPercent (0..300), SetGrassHeightMinCm (0..300), SetGrassHeightMaxCm (0..300), SetGrassWavePeriodCm (100..3000), SetGrassWindStrengthCm (0..50), SetGrassWindSpeedPercent (0..300), SetGrassBladeWidthPercent (50..200), SetGrassSpacingCm (0..100), SetGrassTerrainActiveDistanceM (0..500), SetGrassCullDistanceM (0..500), SetGrassFadeDistancePercent (0..100), SetGrassFarThinningPower (0..8), SetTerrainErosionDetailPercent (0..300), SetSunColor (color), SetSunIntensity (0..20), SetSunRotation (0..360), SetSunTilt (0..90), SetSkyLightIntensity (0..8), SetSkyLightColor (color), SetSkyLightSaturation (0..3), SetSkyboxTint (color), SetSkyboxSaturation (0..3), SetSkyboxExposure (0..3), SetSkyboxHaze (0..8), SetSkyboxContrast (0..3), SetSkyboxGroundColor (color), SetSkyboxColor (color), SetSkyboxCloudCoverage (0..1), SetSkyboxStarIntensity (0..3), SetShadowBlurRadius (0..20), SetShadowOpacity (0..1), SetShadowLift (0..3), SetShadowLiftThreshold (0.02..0.3), SetShadowDistance (0..500), SetFogColor (color), SetFogStartDistance (0..2000), SetFogEndDistance (0..2000), SetFogSkyboxAffect (0..100), SetHeightFogEnabled (0..1), SetHeightFogTop (0..200), SetHeightFogBottom (-100..200), SetHeightFogSoftness (0..1), SetSsaoIntensity (0..1), SetSsaoDistance (0..10), SetBloomThreshold (0..3), SetBloomIntensity (0..2), SetTerrainNormalStrengthPercent (0..300), SetContrast (0..4), SetSaturation (0..4), SetExposure (-3..3), SetVignetteStrength (0..1), SetVignetteRadius (0..1), SetVignetteSoftness (0..1), SetVignetteColor (color), SetWaterEnabled (0..1), SetWaterSeaLevelCm (-2000..10000), SetWaterAmplitudeCm (0..500), SetWaterWavelengthCm (100..20000), SetWaterSteepness (0..1), SetWaterDirectionDeg (0..360), SetWaterSpeedPercent (0..400), SetWaterShallowColor (color), SetWaterDeepColor (color), SetWaterColorMixRangeCm (1..2000), SetWaterTransparency (0..1), SetWaterReflectionStrength (0..2), SetWaterReflectionDistortion (0..2), SetWindDirectionDeg (0..360), SetWindStrength (0..1), SetWindGustiness (0..1), SetTreeLodSwitchDistanceM (0..300), SetTreeMeshDistanceM (0..1000), SetTreeCullDistanceM (0..3000), SetTreeFadeDistancePercent (5..80), SetTreeFarLeafDensityPercent (10..300)
79
86
  Example:
80
87
  #adjuster
81
88
  section "Grass"
82
89
  knob SetGrassTipColor = #8fbf4a
83
- knob SetGrassHeightMaxCm = 45
84
- knob SetSunIntensity = 4.5 range 2..8
90
+ knob SetGrassHeightMaxCm = 45 range 0..120
85
91
  #end
86
92
  </adjuster_usage>
87
93
 
88
94
  <particles_json_usage>
89
95
  - `#particles Name ... #end` defines local effect asset `effect.Name`; `#particles effect.Name` also works, but prefer short `Name`. Body is a JSON array; each object is one emitter component.
90
96
  - Every local component needs a top-level `"particleTextureName"`: `"flipbook.<word>"` for an animated sprite-sheet atlas from the shared flipbook library (best for fire/smoke/magic; lowercase subject words like `flipbook.orange_flame`; misses render a glow placeholder until the word is minted), `"texture.TextureName.color"` with a matching `#texture TextureName`, or `"image.<User>.<Name>"` referencing a real ori image asset you actually know (never invent one). Use `texture.*.color` (`baked.*` addresses material-slot maps, not particle textures; `.normal`/`.orm` are not particle flipbooks).
91
- - Spawn with scene `effect Spark { effect = "effect.Name" }` or Weave effect APIs.
92
- - Sizing: declare `"boundingBoxSize": [x, y, z]` (meters, Z-up; per-component, max wins) as the effect's authored real-world footprint, and author emitters (`radius`, `sizeSequence`, speeds) at that real scale. The object's `size` (cm, like any object) then scales the effect by `size / boundingBoxSize` — e.g. a campfire authored with `boundingBoxSize [1, 1, 1.5]` placed with `size = 100` renders at its authored 1m footprint. Without `boundingBoxSize` the base is a legacy 10cm box, so `size = 100` blows the effect up 10x — always declare it. Size effects with `size`, not `scale`.
97
+ - Spawn with scene `effect Spark { effect = "effect.Name" }` - or, for prop-attached fire/smoke, prefer a `particles("<word>", at: ...)` socket inside the `#mesh` (see <mesh_dsl_usage>). From code pass the ASSET as a static import: `SpawnEffectWithAutoDestroy(Ori.effect.Name, pos)` (returns the spawned object — `.SetSizeUniform(n)` to size it), or `Ori.alias.effect.<word>` for aliases. A `Scene.<object>` reference is not an effect asset (W2C0405).
98
+ - Sizing: declare `"boundingBoxSize": [x, y, z]` (meters, Z-up; per-component, max wins) as the effect's authored real-world footprint, and author emitters (`radius`, `sizeSequence`, speeds) at that real scale. The object's `size` (cm, like any object) then scales the effect by `size / boundingBoxSize` — e.g. a campfire authored with `boundingBoxSize [1, 1, 1.5]` placed with `size = 100` renders at its authored 1m footprint. Without `boundingBoxSize` the base is a legacy 10cm box, so `size = 100` blows the effect up 10x — always declare it. Size effects with `size`, not `scale`. Author emitters freely around your origin; when the content is not origin-centered, add `"boundingBoxCenter": [x, y, z]` (meters; where the box CENTER sits relative to that origin; default [0,0,0]) so the box covers the content — e.g. a ground flame rising 0..1.5m declares [0, 0, 0.75]. Placement matches meshes: the object's `position` rests the declared box's bottom on the point (`transform_pivot = center` centers it); code spawns (`SpawnEffect*`) put the box CENTER at the given position.
93
99
  - JSON values: scalars are numbers, vecs are `[x,y]`/`[x,y,z]`, curves are up to 8 `[[life01, value], ...]`; `colorSequence` uses `[[life01, [r,g,b]], ...]`. Prefer numeric `0/1` for booleans.
94
- - CPU/import keys: `_particleName_` ignored label; `particleCount` live-particle capacity override, not spawn count; `emitterPosition`/`emitterRotation` component-local offset/XYZ degrees; `boundingBoxSize` authored footprint in meters (see sizing bullet above).
100
+ - CPU/import keys: `_particleName_` ignored label; `particleCount` live-particle capacity override, not spawn count; `emitterPosition`/`emitterRotation` component-local offset/XYZ degrees; `boundingBoxSize`/`boundingBoxCenter` authored footprint + box placement in meters (see sizing bullet above).
95
101
  - Flipbook keys: `framesX`, `framesY`, `totalFrames`, `flipbookAnimationSpeed`, `flipbookStartRandom`. For `flipbook.<word>` refs these auto-fill from the library at load; set them only to override.
96
- - Spawn keys: `spawnStartDelay`, `spawnRate` per sec, `isLooping`, `duration` seconds (`0` means no shader stop; use finite duration for one-shots), `burstParticleCount`, `burstInterval`.
97
- - Shape keys: `emitterShapeType` = `"sphere"|"box"|"cylinder"|"disc"` or `0..3`; `shapeStyle` = `"volume"|"surface"`; `shapeInOut` = `"in"|"out"|"both"` (omitted defaults inward); `radius`, `cylinderHeight`, `boxSize`, `shapePartial`, `lifeTimeRange`, `speedRange`, `spreadAngle` `[yaw,pitch]` degrees, `lockedToEmitter`. Box surface emits from +Z face; cylinder height is local X; disc uses `cylinderHeight` as thickness.
98
- - Visual/motion keys: `sizeConstant` or `sizeSequence`; `opacityConstant` or `opacitySequence`; `colorConstant` or `colorSequence`; `accelerationVector` authored Z-up; `drag` seconds to halve velocity; `rotationRange`, `rotationSpeedRange`; `orientation` 0 facing camera / 1 world-up / 2 velocity-parallel / 3 velocity-perpendicular; `additiveBlend`, `isHidden`, `softParticleFadeDisabled`, `softParticleFadeDistance`, `squashConstant` or `squashSequence` (positive stretches up, negative sideways).
102
+ - Spawn keys: `spawnStartDelay`, `spawnRate` per sec, `isLooping`, `duration` seconds (`0` means no shader stop; use finite duration for one-shots), `burstParticleCount` (fires at t=0, then every `burstInterval` seconds while emitting; `burstInterval` unset = the t=0 burst only).
103
+ - Shape keys: `emitterShapeType` = `"sphere"|"box"|"cylinder"|"disc"` or `0..3`; `shapeStyle` = `"volume"|"surface"`; `shapeInOut` = `"in"|"out"|"both"` (omitted defaults inward); `radius`, `cylinderHeight`, `boxSize`, `shapePartial`, `lifeTimeRange`, `speedRange`, `spreadAngle` `[yaw,pitch]` degrees, `lockedToEmitter` (`0` default = particles keep world position once spawned - trails stay behind a moving emitter; `1` = particles ride it, e.g. torch flame). Box surface emits from +Z face; cylinder height is local X; disc uses `cylinderHeight` as thickness. Emission direction: each shape emits along its own radial direction; `shapeInOut` omitted = INWARD, `"out"` flips outward, `"both"` = 50/50. `spreadAngle [yaw, pitch]` then jitters that direction by uniform +/-yaw degrees about the emitter's local Y axis and +/-pitch degrees about its X axis (fixed local axes, not a cone around the direction).
104
+ - Visual/motion keys: `sizeConstant` or `sizeSequence`; `opacityConstant` or `opacitySequence`; `colorConstant` or `colorSequence`; `accelerationVector` authored Z-up; `drag` seconds to halve velocity; `rotationRange`, `rotationSpeedRange`; `orientation` 0 facing camera / 1 world-up / 2 velocity-parallel / 3 velocity-perpendicular; `additiveBlend`, `isHidden`, `softParticleFadeDisabled`, `softParticleFadeDistance`, `squashConstant` or `squashSequence` (positive stretches up, negative sideways); `lit` 1 = sun+sky shading (smoke/dust/debris read as lit volumes; leave 0 for emissive fire/glow).
99
105
  - Resolution key: components render at full (native) resolution by default (crisp edges, no distance shimmer). Set `"renderResolution": "half"` on soft/volumetric high-overdraw components (smoke, fog, glow) — cuts GPU fill cost on weak GPUs; avoid on hard-edged/crisp content (`"full"` is accepted as an explicit no-op).
100
- - Ribbon/trail components (boost trails, sword arcs, tracers, dash streaks): `"type": "ribbon"` renders one connected camera-facing strip through the emitter's recent world path instead of billboards; layer with billboard components in the same effect (ideal boost = ribbon streak + sparse additive orbs). Keys: `trailSeconds` (point max age = trail length, default 0.5), `pointsPerSecond` (path sampling rate, default 30), `ribbonUv` = `"stretch"` (default) | `"repeat_per_meter"` (tiles by real distance), `ribbonPoints` (ring cap; defaults to fit trailSeconds*pointsPerSecond, max 256); `sizeConstant`/`sizeSequence` = strip WIDTH in meters over point age; texture/flipbook, color/opacity, `additiveBlend`, `renderResolution`, `boundingBoxSize`, `emitterPosition`/`emitterRotation`, soft-fade keys keep their exact meaning. Emission/shape/motion keys (`spawnRate`, `lifeTimeRange`, `emitterShapeType`, `speedRange`, `spreadAngle`, `drag`, `orientation`, `duration`, ...) are load ERRORS on a ribbon component. Ribbon effects are always continuous; the strip exists only while the emitter MOVES (attach to a moving object; `preview_effect` sweeps the emitter for you). Object `size` scales strip width — trail length is temporal (`trailSeconds` x actual speed).
106
+ - Ribbon/trail components (boost trails, sword arcs, tracers, dash streaks): `"type": "ribbon"` renders one connected camera-facing strip through the emitter's recent world path instead of billboards; layer with billboard components in the same effect (ideal boost = ribbon streak + sparse additive orbs). Keys: `trailSeconds` (point max age = trail length, default 0.5), `pointsPerSecond` (path sampling rate, default 30), `ribbonUv` = `"stretch"` (default) | `"repeat_per_meter"` (tiles by real distance), `ribbonPoints` (ring cap; defaults to fit trailSeconds*pointsPerSecond, max 256); `sizeConstant`/`sizeSequence` = strip WIDTH in meters over point age; texture/flipbook, color/opacity, `additiveBlend`, `renderResolution`, `boundingBoxSize`, `emitterPosition`/`emitterRotation`, soft-fade keys keep their exact meaning. Emission/shape/motion keys (`spawnRate`, `lifeTimeRange`, `emitterShapeType`, `speedRange`, `spreadAngle`, `drag`, `orientation`, `duration`, ...) are load ERRORS on a ribbon component. Ribbon effects are always continuous; the strip exists only while the emitter MOVES (attach to a moving object; `preview_effect` sweeps the emitter for you). Object `size` scales strip width — trail length is temporal (`trailSeconds` x actual speed). A trail that looks unchanged after resizing its object wants `trailSeconds`/`sizeSequence` edits, not more `size`.
101
107
  - Mesh components (debris, coins, leaves, shards, casings): `"type": "mesh"` renders lit opaque 3D instances of a real mesh instead of sprites; layer with billboard/ribbon components in one effect (rock burst = mesh chunks + dust billboards + one flash). `"particleMeshName"`: a local `"mesh.Name"` or a bare-alias `"alias.model.<noun>"` (library-bound or background-generated; placeholder cube until it lands) — or an array of up to 4, picked per particle (identical chunks read fake). Meshes keep their own materials and receive scene lighting: `colorConstant`/`colorSequence` tints (omit = untinted), `opacityConstant`/`opacitySequence` dither-fades, `sizeConstant`/`sizeSequence` = largest mesh dimension in meters. `"meshAlign"`: `"tumble"` (default; `"tumbleSpeedRange": [min,max]` deg/s random-axis, default [90, 360]) | `"velocity"` (mesh +Z along travel: arrows, shards) | `"world"` (authored orientation). Emission/shape/motion keys keep their exact billboard meaning. Texture/flipbook keys, `orientation`, `rotationRange`/`rotationSpeedRange`, `squash*`, `additiveBlend`, `softParticleFade*`, `renderResolution` are load ERRORS on a mesh component. `"castShadow": 1` enables shadow casting (default off; a fading particle stops casting, like skinned deaths). `"groundBounce": 0..1` bounces world-space particles off terrain/static ground with that restitution and lets them settle until life ends (visual bounce, not physics; `lockedToEmitter` particles skip it). HDR `colorConstant`/`colorSequence` excess over 1.0 becomes emissive glow. Each particle is a real lit draw — keep `spawnRate * lifeTime` under ~100 and meshes low-poly.
102
108
  - Runtime-managed fields are not useful in JSON: `time`, `delta_time`, `world_matrix`, `world_matrix_inv`, `emitter_time`, `emitter_id`, `system_scale`, `instance_tint`, `instantClearOnRemove`, padding fields. Unknown keys only log warnings.
103
109
  Example:
@@ -123,32 +129,48 @@ effect ChimneySmoke {
123
129
  }
124
130
  </particles_json_usage>
125
131
 
132
+ <sound_dsl_usage>
133
+ - `#sound Name ... #end` bakes a local procedural sound asset `sound.name` at load (name lowercased; reference case-insensitive, no creator segment). Play like any sound url: `PlaySound(Ori.sound.name, 1.0)`.
134
+ - Body: `duration = <sec>` (max 8, default 1), `let x = <expr>` chains, final `out = <expr>`. Combine with `+ - * /` and parens; scalar constants broadcast, shorter signals zero-pad.
135
+ - Ops: `osc(sin/saw/square/tri, freq, phase)` (freq may be a signal for sweeps/FM), `noise(white/pink, seed)`, `biquad(lp/hp/bp, input, cutoff, q)` (cutoff may be a signal), `env_adsr(attack, decay, sustain, release)`, `env_exp(start, end, time)` (freq or amp ramps), `mix(a, b, t)`, `layer(a, b, ...)`, `delay(input, time, feedback, mix)`, `pitch(input, ratio)`, `normalize(input, peak)`, `limit(input, ceiling)`.
136
+ - Edits rebake on reload. Keep one-shots short; looping ambience via PlaySoundLooped works on any baked sound.
137
+ - Music/BGM: this synth is SFX-grade (8s cap, no instruments) and cannot produce music — say so up front and use a marketplace/uploaded sound url with PlaySoundLooped instead of iterating synth attempts.
138
+ #sound laser
139
+ duration = 0.35
140
+ let sweep = osc(saw, freq = env_exp(1800, 250))
141
+ out = normalize(biquad(lp, sweep, cutoff = 2200, q = 1.2) * env_adsr(0.005, 0.1, 0.0, 0.08), 0.9)
142
+ #end
143
+ </sound_dsl_usage>
144
+
126
145
  <mesh_dsl_usage>
127
- - `#mesh <Name> ... #end` defines a local CPU-baked procedural static mesh; reference it as `model = "mesh.Name"` in scene declarations or `Ori.model.mesh.Name` in Weave code (SpawnObject, CreateInstanceBatch). Authoring units are simulation centimeters, Z-up; baked meshes are bottom-center anchored at object origin and their deterministic cm AABB supplies box-collider bounds.
146
+ - `#mesh <Name> ... #end` defines a local CPU-baked procedural static mesh; reference it as `model = "mesh.Name"` in scene declarations or `Ori.model.mesh.Name` in Weave code (SpawnObject, CreateStaticInstanceBatch). Authoring units are simulation centimeters, Z-up; baked meshes are bottom-center anchored at object origin and their deterministic cm AABB supplies box-collider bounds.
147
+ - `#mesh <Name> destructible: masonry|wood|rock|concrete` bakes a hidden debris chunk set for the model; `obj.ShatterAndDestroy()` then bursts the object into tumbling chunks that bounce, settle, and fade (display-only, no sim collision; without the tag it acts as plain Destroy()). Preview with the preview_destruction tool; destructible mesh names cap at 46 chars.
128
148
  - For standard props (tree, crate, market_stall, ...), PREFER a bare `alias model.<noun>` (see <asset_aliases>): it binds a matching generated mesh from the library at load, or background-generates one.
129
149
  - Authoring or editing a `#mesh` body: read the `mesh_dsl` skill first — full language + builtin reference and the preview workflow. Do not guess builtin names or signatures from memory; unknown, duplicate, or wrong-type arguments fail loudly.
150
+ - Lights on meshes: an `emitter("Bulb", at:, color:, intensity:, kind: "point"|"spot", ...)` line in the mesh body bakes a light socket; every placed instance shines from it (lamps, signs, projectors) - no separate light object needed. Syntax in the `mesh_dsl` skill; per-object toggle `Object.SetEmitterLightsEnabled(false)`.
151
+ - Effects on meshes: `particles("<effect word>", at: (x,y,z)[, axis:, scale:])` in the mesh body embeds a particle effect the way `emitter()` embeds a light - mesh-local, riding every instance/rotation/saved object. The word must be a quoted literal and resolves like `effect.<word>` (local `#particles` block name, case-sensitive, or lowercase library word). Rest-pose like emitter(); prefer loops (fire, smoke, drips). For prop-attached fire/smoke prefer this over hand-placed effect objects.
130
152
  - Material slots: `material: "Name"` on generators or `paint(g, material: "Name")`. A slot name resolves to world `material.Name` when it exists (pair slots with `#texture Name` for low-poly color-blocking). Precedence: object/batch material override > explicit submesh override > authored local slot material > intrinsic GLB PBR.
131
- - Canonical slot names auto-fill from the built-in material library when the world doesn't declare them (library meshes only use these): Wood, WoodDark, Plank, Bark, Stone, Rock, Brick, Dirt, Sand, Metal, MetalDark, Gold, Fabric, Cloth, Rope, Straw, Leather, Foliage, Leaf, Grass, Snow, Ceramic, Bone, Glass, GlassTransparent, Water, Default. Recolor one via a derived material, e.g. `material Wood { base = "WoodDetail" tint = "#8a2b1e" }` (see <texture_dsl_usage>).
153
+ - Canonical slot names auto-fill from the built-in material library when the world doesn't declare them (library meshes only use these; name list in <texture_dsl_usage>). Recolor one for every library mesh by declaring it, e.g. `material Wood { base = "WoodDetail" tint = "#8a2b1e" }`.
132
154
  - GlassTransparent is the real translucent glass (fresnel reflection + grime; casts no shadow) - only for panes with real geometry behind them (balustrades, canopies, display cases), NOT hollow building window walls (blend reveals the empty shell; use `interior_window_pbr`, next clause). Derive tints in one line - `material PoolGlass { base = "GlassTransparent" tint = "#3A7E8C" }` - and override `opacity = 0..1` (transmission) / `glassRoughness = 0..1` (reflection blur) only to tune.
133
- - Hollow building window walls (where GlassTransparent would expose the empty shell): `material TowerWin { base = "InteriorRooms" shader = "interior_window_pbr" tint = "#D8CFC0" glassTint = "#3A4E4A" }` - opaque fake-parallax rooms, night-lit cells automatic. `tint` = mullions; `roomW/roomH/roomDepth/bayW` m override 4.41/3.3/4.0/1.47.
155
+ - Hollow building window walls (where GlassTransparent would expose the empty shell): `material TowerWin { base = "InteriorRooms" shader = "interior_window_pbr" tint = "#D8CFC0" glassTint = "#3A4E4A" }` - opaque fake-parallax rooms, night-lit cells automatic. Base picks the interior per building: `"InteriorRooms"` homes, `"InteriorOffice"` workplaces. Homes get side drapes by default (`drapeSides` = per-side width 0..0.35, 0 disables; offices default 0). `tint` = mullions; `roomW/roomH/roomDepth/bayW` m override 4.41/3.3/4.0/1.47 - repeat PITCHES of the fake-room grid (set to the real floor/bay pitch), not opening sizes; `roomOffsetX/roomOffsetZ` m shift the grid datum (default 0).
134
156
  </mesh_dsl_usage>
135
157
 
136
158
  <tree_dsl_usage>
137
159
  - `#tree Name ... #end` (top-level): procedural tree species `tree.Name` for scene objects (`model = "tree.Name"`). Deterministic: trunk + branches become the sim collider; the renderer adds detail, leaf cards, LODs, impostors, and wind sway automatically.
138
- - Body is integer-only `key: value` lines (all optional; defaults are a curated broadleaf oak, so `seed:` alone gives a good tree). Keys: seed, levels (1-4), children (1-16), rings (whorl only: 0-40 branch rings along the trunk, decoupled from segments; 0 = one per segment - reference firs want ~15-20; members auto-stagger vertically), fill (whorl only: 0-16 short branches between rings - dense conifers), segments (2-8), collider_levels (dense conifers should author 1 - collide with the trunk, not hundreds of branchlets), trunk_height (cm), trunk_radius (cm), length_pct, radius_pct, angle_deg (child pitch from the parent axis; >90 emits below horizontal - with up_bias_pct the branch swoops down then up, the fir/spruce shelf), angle_jitter_deg, up_bias_pct, curve_jitter_pct, trunk_lean_deg (whole-trunk tilt, default 2; the trunk-shape low band), trunk_sweep_pct (one C- or S-arc over the trunk height, default 10; the mid band), trunk_noise_pct (trunk wobble amplitude; 0 = dead-straight leader with curve_jitter_pct branches) + trunk_noise_scale_pct (its wavelength as % of trunk height, 8-100, default 35; small = ragged bark-beat, large = slow bends - the high band), trunk_gnarl_pct (0-100; display-only radial lobe noise on the thick lower trunk - buttress/character silhouette for hero close-ups; collider stays smooth), leaves (frond stations per end branch), leaf_size (cm; mean size of one leaf-cluster card - each card is a baked spray of many leaves, not a single leaf; default 150), leaf_base (cm; no leaf cards below this height - bare forks/scaffolds; leaf_base_fade cm band above it ramps density to full, default 120), leaf_shell_pct (0-100; blend toward smooth per-pixel crown-shell shading - 0 = flat per-card facing, 100 = crown shades as a smooth rounded volume; default 100), leaf_clump_pct (0-100; shifts that shell reference from the whole-crown center toward each level-1 branch clump's own center, so big canopies shade as separate puffy lobes instead of one ball; default 40), leaf_detail_pct (0-300; leafy normal-detail sparkle bumped from the frond texture itself; 0 = smooth shell only; default 200), leaf_ao_pct (0-200; canopy AO strength darkening cards near the trunk column and canopy bottom; 0 = off; default 40), leaf_curl_pct (0-300; how strongly leaf cards bow out of their plane for varied, non-flat silhouettes; 0 = flat cards; default 150), leaf_far_shade_pct (0-150; far-billboard brightness; billboards lose cast shadows so <100 darkens them to match the near tree; default 60), droop_pct (tips sag), length_taper_pct (shorter branches higher up; ~70 = conifer cone), root_flare_pct (wider trunk base).
160
+ - Body is integer-only `key: value` lines (all optional; defaults are a curated broadleaf oak, so `seed:` alone gives a good tree). Keys: seed, levels (1-4), children (1-16), rings (whorl only: 0-40 branch rings along the trunk, decoupled from segments; 0 = one per segment - reference firs want ~15-20; members auto-stagger vertically), fill (whorl only: 0-16 short branches between rings - dense conifers), segments (2-8), collider_levels (dense conifers should author 1 - collide with the trunk, not hundreds of branchlets), trunk_height (cm), trunk_radius (cm), length_pct, radius_pct, angle_deg (child pitch from the parent axis; >90 emits below horizontal - with up_bias_pct the branch swoops down then up, the fir/spruce shelf), angle_jitter_deg, up_bias_pct, curve_jitter_pct, trunk_lean_deg (whole-trunk tilt, default 2; the trunk-shape low band), trunk_sweep_pct (one C- or S-arc over the trunk height, default 10; the mid band), trunk_noise_pct (trunk wobble amplitude; 0 = dead-straight leader with curve_jitter_pct branches) + trunk_noise_scale_pct (its wavelength as % of trunk height, 8-100, default 35; small = ragged bark-beat, large = slow bends - the high band), trunk_gnarl_pct (0-100; display-only radial lobe noise on the thick lower trunk - buttress/character silhouette for hero close-ups; collider stays smooth), leaves (frond stations per end branch), leaf_size (cm; mean size of one leaf-cluster card - each card is a baked spray of many leaves, not a single leaf; default 150), leaf_base (cm; no leaf cards below this height - bare forks/scaffolds; leaf_base_fade cm band above it ramps density to full, default 120), leaf_shell_pct (0-100; blend toward smooth per-pixel crown-shell shading - 0 = flat per-card facing, 100 = crown shades as a smooth rounded volume; default 100), leaf_clump_pct (0-100; shifts that shell reference from the whole-crown center toward each level-1 branch clump's own center, so big canopies shade as separate puffy lobes instead of one ball; default 40), leaf_detail_pct (0-300; leafy normal-detail sparkle bumped from the frond texture itself; 0 = smooth shell only; default 200), leaf_ao_pct (0-200; canopy AO strength darkening cards near the trunk column and canopy bottom; 0 = off; default 40), leaf_curl_pct (0-300; how strongly leaf cards bow out of their plane for varied, non-flat silhouettes; 0 = flat cards; default 150), leaf_far_shade_pct (0-150; far-billboard brightness; billboards lose cast shadows so <100 darkens them to match the near tree; default 60), droop_pct (tips sag), length_taper_pct (shorter branches higher up; ~70 = conifer cone), root_flare_pct (wider trunk base), wind_pct (0-300; sway response to the global wind - trunk, branches and leaf cards; 0 = rigid, default 100).
139
161
  - Word keys: `mode: spiral|whorl|colonize` (whorl = rings of branches off a straight central leader - the pine/fir/spruce mode; colonize = space-colonization crown grown to fill a shape), `crown_shape: round|cone|vase` (colonize only), `leaf_mode: broadleaf|needle|weeping|mat|frond` (foliage style; weeping = hanging strand curtain, pair with high droop_pct for willows; frond = one whole-branch ribbon card per branch replacing the branch wood - THE fir/spruce mode, pair with mode: whorl and angle_deg ~95-115; mat = scattered flat shelf mats, a lighter conifer fill).
140
162
  - Colonize-only integer keys: points (crown attraction point count; auto-scales with crown size up to 1024, override for extra fine/coarse branching), leader (0-100: how far up the crown the central trunk persists before dissolving into forks - 0 = open broadleaf habit, oaks/beeches fork early; ~50-70 = beech/maple with a trunk partway into the crown; 100 = excurrent conifer/poplar spire), crown_width (cm, 0 = auto), crown_base (crown bottom height cm, 0 = auto; whorl mode also honors it as the lowest branch ring - ground-sweeping firs author ~150), crown_follow_pct (0-100, default 75: how much the crown envelope rides a leaning/swept trunk's top - 100 = crown centered on the bole, 0 = crown stays over the root and limbs reach back, the "leaning toward a clearing" habit).
141
163
  - Colonize authored crown shapes: `crown_sphere: x, y, z, r` / `crown_ellipsoid: x, y, z, rx, ry, rz` / `crown_dome: x, y, z_cut, rx, ry, rise` lines (cm, up to 8) grow branches to fill the union of those volumes, overriding crown_shape/crown_width/crown_base. A dome is the upper half of an ellipsoid with a flat cut at z_cut (umbrella/mushroom caps). Keep volumes near/above the trunk; disconnected lobes get bridged by a reaching branch. Colonize branches stay inside their envelope, so droop_pct barely bends them - weeping/cascading silhouettes (willows) need mode: spiral with high droop_pct instead.
142
164
  - Authored trunk path (any mode): `trunk_point: x, y, z` lines (cm, up to 8, z strictly ascending) draw the trunk's xy course as a smooth spline through the knots, replacing trunk_lean_deg/trunk_sweep_pct (trunk_noise still layers on top). Above the top knot the offset holds, so put the top knot at trunk_height (or crown_base) for a fully drawn bole; pair with crown volumes + crown_follow_pct 100 for a hero leaning tree. Note: trees in a #forest keep a straight vertical collision capsule - keep heavy lean/sweep/trunk_point species out of forests or accept coarse trunk collision there.
143
- - Optional `#texture TreeLeaf_<Name> ... #end` (with `mapping = uv;`, `render_mode = cutout;` + `out.alpha`) replaces the auto-generated leaf frond card. `mapping = uv;` is required - cards are UV quads; the triplanar default renders them invisible. Author the card as a spray of MANY leaves via `sdf.pinnate` (thin leaflets for needle/frond conifers) or `pattern.foliage`, never one big leaf. Cluster cards consume only color+alpha; authored `out.normal`/`out.orm` matter only for `leaf_mode: frond` ribbon cards.
144
- - Optional `#texture TreeBark ... #end` (or `material TreeBark { ... }`) replaces the built-in bark material shared by all tree species.
145
- - Global wind: `SetWind(DirectionDeg, Strength01, Gustiness01)`.
165
+ - Leaf card: `leaf: <TextureName>` body key (or naming the texture `TreeLeaf_<Name>`) replaces the auto-generated frond card; the texture needs `mapping = uv;`, `render_mode = cutout;` + `out.alpha`. `mapping = uv;` is required - cards are UV quads; the triplanar default renders them invisible (unknown or non-uv `leaf:` refs = load error). Author the card as a spray of MANY leaves via `sdf.pinnate` (thin leaflets for needle/frond conifers) or `pattern.foliage`, never one big leaf, with the stem at the bottom edge (v=0) - the card's base sits on the branch. Cluster cards consume only color+alpha; authored `out.normal`/`out.orm` matter only for `leaf_mode: frond` ribbon cards.
166
+ - Bark: `bark: <TextureOrMaterialName>` body key per species (unknown name = load error); bare `TreeBark` (#texture or material) is the shared default for species without one (else built-in bark). Cheap per-species tint: `material PineBark { base = "TreeBark" tint = "#5a4630" }` + `bark: PineBark`.
167
+ - Global wind: `SetWind(DirectionDeg, Strength01, Gustiness01)`; per-species response: `wind_pct`.
146
168
  - Authored integer keys expose editor adjuster-panel sliders (live rebake; the sim collider updates on save) - author the look-defining ones. When the look matters, iterate with the `preview_tree` tool before submitting.
147
169
  Species recipes: see <example_tree_species> in <world_description_examples>.
148
170
  </tree_dsl_usage>
149
171
 
150
172
  <forest_dsl_usage>
151
- - `#forest Name ... #end` (top-level): scatters a `#tree` species over an object's box region (`model = "forest.Name"`; set the object's `size` (cm) to shape the region, default 20x20x10 m). Each tree stands on the terrain surface under its XY; where that surface lies outside the region's z-span the tree is skipped, so the z-span doubles as an elevation band (e.g. bottom above the waterline = no beach/underwater trees, top below the peaks = a tree line). An XY with no terrain under it is skipped too when the world has terrain (a region overhanging the terrain edge grows no drowned/floating trees); only fully terrain-less worlds anchor on the box bottom (flat-ground worlds just work; deliberate floating platforms use on_terrain: 0). Placements stay stable when the region is resized/moved. THE way to put woods on terrain - one region object, no spawn loops.
173
+ - `#forest Name ... #end` (top-level): scatters a `#tree` species over an object's box region (`model = "forest.Name"`; set the object's `size` (cm) to shape the region, default 20x20x10 m; the region object is bottom-anchored like any object - z-span = [position.z, position.z + size.z]). Each tree stands on the terrain surface under its XY; where that surface lies outside the region's z-span the tree is skipped, so the z-span doubles as an elevation band (e.g. bottom above the waterline = no beach/underwater trees, top below the peaks = a tree line). An XY with no terrain under it is skipped too when the world has terrain (a region overhanging the terrain edge grows no drowned/floating trees); only fully terrain-less worlds anchor on the box bottom (flat-ground worlds just work; deliberate floating platforms use on_terrain: 0). Placements stay stable when the region is resized/moved. THE way to put woods on terrain - one region object, no spawn loops.
152
174
  - Body is `key: value` lines. `species: <TreeName>` (required, a `#tree` in this world); optional integers: seed, spacing (cm cell size, >= 100, default 700), density_pct (0-100 chance per cell), scale_min_pct / scale_max_pct (per-tree size range, default 85-125), on_terrain (default 1; 0 = always stand on the box bottom, e.g. floating platform forests).
153
175
  - Terrain filters (the precise control; the z-span is the coarse bracket): height_min_cm / height_max_cm keep trees inside a world-z band at their terrain anchor (waterline, tree line); slope_max_pct (0-100 on the #terrain_splat slope01 scale: 50 = 45deg, 35 ~ 30deg) skips steeper ground (no cliff trees). Filters gate terrain-anchored trees only and require on_terrain: 1. Prefer one big region + filters over hand-fitted boxes.
154
176
  - Collision is per trunk only (a vertical capsule from the species' trunk), never the region box - players walk between trees and brush through leaves.
@@ -163,6 +185,21 @@ Example:
163
185
  object Woods1 { model = "forest.PineWoods" position = (0, 0, 500) size = (12000, 8000, 1000) }
164
186
  </forest_dsl_usage>
165
187
 
188
+ <scatter_dsl_usage>
189
+ - `#scatter Name ... #end` (top-level): scatters ANY model over an object's box region (`model = "scatter.Name"`), exactly like `#forest` placement-wise (same region object, terrain anchoring, z-span band, filters) but display-only: NO collision, plus per-instance color variation. Use for rocks, bushes, props, debris; use `#forest` when trees should collide.
190
+ - Body: `model: <url>` (required; `mesh.<Name>`, `Ori.model.*`, or a market model url) + the `#forest` keys minus `species`; optional integers hue_jitter_deg (0-180), sat_jitter_pct / val_jitter_pct (0-100) tint each instance (multiplies the region object's color; a tint on textured models, not true hue rotation).
191
+ Example:
192
+ #scatter Boulders
193
+ model: "mesh.Rock"
194
+ spacing: 900
195
+ density_pct: 40
196
+ scale_min_pct: 60
197
+ scale_max_pct: 160
198
+ hue_jitter_deg: 12
199
+ #end
200
+ object RockField { model = "scatter.Boulders" position = (0, 0, 500) size = (12000, 8000, 1000) }
201
+ </scatter_dsl_usage>
202
+
166
203
  <room_building>
167
204
  - For simple rectangular rooms, prefer `BuildRoom(Center, RoomSize, Thickness, Height, GateSide, GateWidth)` instead of hand-placing wall boxes. It creates clean aligned walls and avoids crooked wall edges/gaps.
168
205
  - `Center` is the room center position. `RoomSize` is the INNER clear size in cm. Walls sit just outside that footprint, with bottoms at `Center.Z` and height `Height`.
@@ -196,13 +233,22 @@ event OnSpawned {
196
233
  - `TileGridCreate(TilesX, TilesY, DefaultWalkable)` returns an opaque compact List. `TileGridCreate(TilesX, TilesY, DefaultWalkable, EdgeBlockingEnabled)` enables optional edge-wall storage when the Bool is true. Do not call generic mutating List functions (`Add`, `Set`, `Resize`, `Clear`, `Pop`, etc.) on it; use TileGrid helpers. Padded grid capacity is capped at 1,048,576 tiles.
197
234
  - Edge-blocking grids can use `TileGridSetEdgeBlocked(Grid, X1, Y1, X2, Y2, Blocked)` for cardinal-adjacent tile pairs. Compact grids reject SetEdgeBlocked; `TileGridIsEdgeBlocked(...)` returns false for valid compact-grid edges. Edge-enabled diagonal pathfinding does not cross blocked cardinal edges, and a diagonal step requires all four cardinal edges around the crossed 2x2 tile corner to be clear.
198
235
  - `BuildTileBlockedGridFromTag(OriginTile, TilesX, TilesY, BlockerTag)` creates a walkable grid and blocks tiles whose world-center point overlaps an object with the tag. Build/rebuild grids when blockers change, not every frame.
199
- - `TileGridSetWalkableFromStaticColliders(Grid, OriginTile, ZMin, ZMax)` re-rasterizes an existing non-layered grid from the static world colliders: every tile resets, and a tile blocks when any static collider's world AABB overlaps its rect within the [ZMin, ZMax] cm band (rotated colliders use their conservative AABB; colliders covering the whole grid footprint - floors, world bounds - are ignored). Keep ZMin above the walk surface (e.g. 20) so partial floor slabs don't block their tiles. Call once after building/editing static geometry - walls and crates then block FindPath without hand-authored SetWalkable loops; re-call after edits.
200
- - `TileGridFindPath4/8(Grid, StartX, StartY, GoalX, GoalY)` returns a reversed path List: Goal ... Start. Empty means blocked start/goal, out of bounds, or no path. Compact TileGridFindPath8 preserves corner cutting through unwalkable side tiles; edge-enabled grids still require crossed cardinal edges to be clear.
236
+ - `TileGridSetWalkableFromStaticColliders(Grid, OriginTile, ZMin, ZMax)` re-rasterizes an existing grid from the static colliders: every tile resets, and a tile blocks when any static collider's world AABB overlaps its rect within the [ZMin, ZMax] cm band (rotated colliders use their conservative AABB; colliders covering the whole grid footprint - floors, world bounds - are ignored). Static world objects, static PhysicsObjects, and tilemap solid cells all block; keep ZMin above the walk surface AND above any solid ground tilemap's top (e.g. 20) so floor slabs don't block their tiles. Layered grids rasterize per layer ([ZMin, ZMax] relative to each layer plane). It RESETS every tile - call it BEFORE the FromTerrain pair; for runtime placements stamp instead of re-rastering.
237
+ - `TileGridSetWalkableFromTerrain(Grid, OriginTile, MaxSlope01)` marks tiles UNWALKABLE where the topmost terrain at the tile center is steeper than MaxSlope01 (slope01 = g/(1+g); 0.5 = 45deg; walkers wedge above ~0.5, so 0.4-0.45 is a safe unit ceiling) or absent. Subtractive only - never re-opens tiles.
238
+ - `TileGridSetEdgesFromTerrain(Grid, OriginTile, MaxStepCm)` (edge-blocking grids) blocks the edge between cardinal neighbors whose terrain height difference exceeds MaxStepCm: cliffs become impassable EDGES while rim tiles stay standable; graded ramps (small per-tile rise) stay open. Tile centers sample mid-cell, so a hard TierCm cliff reads as two TierCm/2 steps - MaxStepCm = TierCm/3 separates cliff jumps from ramp rise; size ramps >= 2 tiles of run per tier step so their slope stays below the ~45deg walk limit.
239
+ - Cliff-map load order (manager OnSpawned): collider raster -> FromTerrain -> EdgesFromTerrain -> BakeCrowdObstacles -> IsReachable asserts. Then placements only stamp.
240
+ - `TileGridStampRect(Grid, X1, Y1, X2, Y2, Blocked)` / `TileGridStampObject(Grid, OriginTile, Obj, Blocked)` stamp footprints incrementally (rect corners clamp to the grid; spawn: true, despawn: false re-opens the stamped rect - overlapping footprints must re-stamp their survivors) - cheaper than re-rastering after each placement.
241
+ - `TileGridIsReachable(Grid, X1, Y1, X2, Y2)` (layered: 7-arg form) = "does an 8-dir path exist" without building one - assert connectivity (spawn points, ramps) in a manager OnSpawned with `RaiseWorldError` so broken maps fail at load, not mid-game; also check before wall placements that could seal a region.
242
+ - `TileGridBakeCrowdObstacles(Grid, OriginTile)` bakes avoidance line obstacles from the grid's blocked edges and unwalkable borders so crowd units are never pushed off cliffs or into tilemap walls avoidance cannot see. It is a snapshot: re-bake after runtime walkability edits (opened gates, razed bridges); placed buildings already repel via their own colliders, no re-bake needed.
243
+ - One grid, many movers (RTS): build the grid ONCE on a manager scene object (`var Grid: List<Number>` field; `TileGridCreate` + `TileGridSetWalkableFromStaticColliders` in its OnSpawned) and let every unit reach it through a scene cast - `TileGridFindPath8((Scene.Manager as HQ).Grid, ...)`. Cross-class field access works for List fields; never rebuild the grid per unit.
244
+ - `TileGridFindPath4/8(Grid, StartX, StartY, GoalX, GoalY[, GoalToleranceTiles])` returns a reversed path List: Goal ... Start. Empty means blocked start/goal, out of bounds, or no path (a TRUE verdict - the search budget covers the whole grid, so a no-path query floods the goal's component; on big maps gate mass orders with one TileGridIsReachable, share flow fields, and cache no-path results). GoalToleranceTiles (0-16) accepts the cheapest walkable tile within that ring of the goal - set it when ordering onto colliders (buildings, resource nodes); empty then means the ring itself is unreachable. Compact TileGridFindPath8 preserves corner cutting through unwalkable side tiles; edge-enabled grids still require crossed cardinal edges to be clear.
201
245
  - `TileGridHasLineOfSight(Grid, StartX, StartY, GoalX, GoalY)` (layered: 7-arg form, same Z only) returns true when the straight tile-center corridor is fully walkable (crossed edges clear on edge grids; exact corner crossings also need both side tiles). Chasing a moving target: LOS true -> `MoveTowardContinuous` at the live position, false -> FindPath + follow.
202
246
  - `TileGridSmoothPath(Grid, Path)` returns a new reversed path with waypoints dropped wherever the straight corridor between the survivors is clear (string pulling; never smooths across a layer change). Walkers zigzagging tile centers -> smooth once after FindPath, before following.
203
- - To follow a path, call `MoveAlongTilePathContinuous(Path, Grid, OriginTile, Speed)` each frame: one call steps the mover, pops reached waypoints, and returns true once the path is consumed (false for an already-empty path). Prefer it over manual MoveToward + Pop loops; it is one sim call per mover per frame.
247
+ - To follow a path, call `MoveAlongTilePathContinuous(Path, Grid, OriginTile, Speed[, ArriveWithinCm[, OrientTurnRate]])` each frame: one call steps the mover, pops reached waypoints, and returns true once the path is consumed (false for an already-empty path). ArriveWithinCm (default 1) completes the FINAL waypoint within that distance - set it when the goal can be occupied (resource node, building) so contact does not stall arrival. OrientTurnRate faces the mover along its travel (deg/s; 0 = instant) - RTS vehicles read wrong without it. Prefer it over manual MoveToward + Pop loops; it is one sim call per mover per frame.
248
+ - Mover frozen after an early order switch: a half-consumed path re-followed later pushes at a stale waypoint - Clear() the path list whenever you leave a follow before it returns true.
204
249
  - For CROWDS (Weave2, hundreds+ of movers): `obj.StartTilePathFollow(Path, Grid, OriginTile, Speed)` registers the mover with the native segment follower (same movement semantics, zero per-mover calls while walking; returns false for an empty path). Start CONSUMES the path list (it is empty afterwards - the follower owns a copy); sim-visible mover positions update at waypoint granularity while walking. One manager then drains `TilePathFollowTakeArrivals()` once per frame (returns finished movers as `List<Object>`) to run arrival logic. `obj.StopTilePathFollow()` cancels; `obj.IsTilePathFollowDone()` polls instead of draining (an arrival stays set until taken/stopped/restarted). Starting again replaces the previous follow.
205
- - Layered grids (bridges/multi-floor): `TileGridCreate3D(TilesX, TilesY, Layers, DefaultWalkable)`; each layer is an independent walkable plane at OriginTile.z + Z. `TileGridSetWalkable/IsWalkable(Grid, X, Y, Z, ...)`, `TileGridSetLinkUp(Grid, X, Y, Z, Linked)` marks a stair cell connecting Z and Z+1 (both ends must also be walkable), `TileGridFindPath4/8(Grid, StartX, StartY, StartZ, GoalX, GoalY, GoalZ)`. On layered grids MoveAlongTilePathContinuous drives Z too (object bottom rests on the layer plane). No edge blocking on layered grids.
250
+ - For MANY movers sharing ONE destination set (group move / rally / harvest points): `TileGridFlowFieldBuild(Grid, GoalTiles)` bakes per-tile direction toward the nearest goal into a reusable opaque List (GoalTiles = flat X,Y,Z coordinate triples; unwalkable goal tiles are skipped; every goal blocked -> all-unreachable field). `obj.StartTileFlowFollow(Field, Grid, OriginTile, Speed)` walks it with the same native follower - the lists stay CALLER-OWNED, one field serves any number of movers; returns false when the mover's tile is off-grid or unreachable in the field. Fields are build-once snapshots: after `TileGridSetWalkable` edits affected movers HALT and surface in `TilePathFollowTakeBlocked()` (drains like TakeArrivals) - rebuild the field and restart them. TakeArrivals/Stop/IsDone work as for path follows.
251
+ - Layered grids (bridges/multi-floor): `TileGridCreate3D(TilesX, TilesY, Layers, DefaultWalkable)`; each layer is an independent walkable plane at OriginTile.z + Z. `TileGridSetWalkable/TileGridIsWalkable(Grid, X, Y, Z, ...)`, `TileGridSetLinkUp(Grid, X, Y, Z, Linked)` marks a stair cell connecting Z and Z+1 (both ends must also be walkable), `TileGridFindPath4/8(Grid, StartX, StartY, StartZ, GoalX, GoalY, GoalZ)`. On layered grids MoveAlongTilePathContinuous drives Z too (object bottom rests on the layer plane). No edge blocking on layered grids.
206
252
  </tile_pathfinding>
207
253
 
208
254
  <exported_params_v1>
@@ -218,7 +264,7 @@ event OnSpawned {
218
264
  - `@exportRange(...)` implies export
219
265
  - `@exportColor` implies export
220
266
  - local `var`, `let`, and other types are not supported in v1
221
- - exported params appear in the editor AI chat tune drawer when exactly one object with those vars is selected
267
+ - exported params appear in the editor AI Feed tune drawer when exactly one object with those vars is selected
222
268
  - for live tuning in the editor, apply exported values inside `event OnTuneChanged { ... }`
223
269
  - common pattern: initialize once in `OnPreSpawn`/`OnSpawned`, then mirror the same property updates in `OnTuneChanged`
224
270
  - user keeps nudging one number per message (offset/size/speed/color) -> declare it `@exportRange`, ask them to drag the slider, then bake the settled value back into the source instead of resubmitting per nudge
@@ -234,6 +280,17 @@ event OnTuneChanged {
234
280
  #end
235
281
  </exported_params_v1>
236
282
 
283
+ <persistent_state_v1>
284
+ - `@persist var Name = ...` (class-scope top-level var): value survives save/load and world reload
285
+ - supported types: Number, Bool, Text, Vector, Position (others are a compile error)
286
+ - scene objects rehydrate by object name; a standalone `@persistSpawned` line inside a class also recreates its runtime-spawned instances (saved template + position/rotation/size + tagged vars); crowd instances and multi-object templates are not recreated
287
+ - class edits migrate loudly: removed/retyped tagged vars are dropped and reported; new tagged vars start at their defaults
288
+ - a destroyed scene object reappears on load; persist a `@persist var Opened` style flag and react in OnSpawned
289
+ - saving is automatic per project; there is nothing to call
290
+ - PersistClear() wipes the project's saved state (pair with RestartGame() for a New Game button)
291
+ - prefer @persist over PersistGet/SetNumber for object state; the KV remains for per-player counters
292
+ </persistent_state_v1>
293
+
237
294
  <multiplayer_guidance>
238
295
  - Core model: multiplayer is deterministic lockstep with client prediction/rollback, NOT object replication.
239
296
  - All weave code, scene objects, and sim state run identically on every client/server (same world + inputs => same result; verifiable on-chain).
@@ -257,16 +314,22 @@ event OnTuneChanged {
257
314
  - Validation/simulate auto-joins a second mock player a few frames in when these per-player APIs are referenced, so per-player join/visibility/index logic is actually exercised.
258
315
  - Players/teams:
259
316
  - SetPlayerOwnerIndex/GetPlayerOwnerIndex, SetRenderVisibleOnlyToPlayerIndex/SetRenderVisibleToAllPlayers, SetTeam/GetTeam
317
+ - Scene field: `object X { team = 0 }` assigns the spawn team (same values as SetTeam; omit for teamless).
318
+ - Fog of war: SetFogOfWarEnabled(true) (needs terrain) then SetSightRadius on teamed objects - per-team grid; shroud, explored-memory, enemy-object hiding, and pick-through are built in (no per-object SetRenderVisible needed).
260
319
  - Iterate via GetAllPlayers()/GetAllPlayersWithTag(..) (existing players only, no None entries).
320
+ - A player who leaves stays in GetAllPlayers() for ~90s (reconnect window) before OnDestroy: gate rosters/win checks on p.IsConnected(), not Exists(), and react to the drop in `event OnPlayerLeft` / to a grace resume in `event OnPlayerReconnected` (both fire on that player's object). Bots are always connected.
321
+ - p.GetPlayerName() = the user's display name (not unique - key logic by player object/index, not name). The engine draws it above visible remote players (SetPlayerNamesVisible(false) hides that); obj.SetHoverText(text) puts a persistent label above any other body (hero units, NPCs).
261
322
  - Never assume a fixed player count or index order (players join/leave).
262
323
  - Determinism caveats:
263
324
  - Time is sim-time and pause-aware: TimeSecs/TimeTicks freeze on pause and may reset on world reset, so capture your own baseline (StartTime=TimeSecs()); RawTimeSecs/RawTimeTicks is unfrozen raw time.
264
- - RandomNumber/RandomPosition* are deterministic (seeded per tick + calling object + call index, so repeated or cross-object same-tick calls draw fresh values); inside player input events (key/mouse/action) all Random* calls are seeded by that input edge instead, so the predicted result matches the final one. RandomChancePercent outside input events returns false during client prediction, so there use it only for delay-tolerant side effects (e.g. drops), not moment-to-moment combat.
325
+ - RandomNumber/RandomPosition* are deterministic (seeded per tick + calling object + call index, so repeated or cross-object same-tick calls draw fresh values); inside player input events (key/mouse/action) all Random* calls are seeded by that input edge instead, so the predicted result matches the final one. RandomChancePercent outside input events returns false during client prediction, so there use it only for delay-tolerant side effects (e.g. drops), not moment-to-moment combat. A fresh load always starts at the same tick, so the FIRST draws repeat across sessions (identical opening deck/magazine every load) — mix a per-session value (e.g. TimeTicks() at game start) into anything that must differ between sessions.
265
326
  - Don't rely on cross-object event ordering (OnSpawned order across objects is unspecified).
266
- - Lobby/session APIs:
267
- - AdvertiseLobbySession()/ListLobbySessions()/JoinLobbySession are relay lobby discovery for menus; identity is automatic - sessions match only peers running this project with identical content (same saved version). LobbySessionStage(i) says which stage each session is on.
268
- - They do NOT create gameplay replication endpoints or change sim authority; once joined, the same shared deterministic simulation model applies.
269
- - Lobby UI lists real joined players via GetAllPlayers(); never simulate joins with a local counter/button.
327
+ - Session APIs (for CUSTOM in-game browsers only - the engine ships a built-in session browser + share/visibility panel for every project; most games need no session code):
328
+ - AdvertiseSession()/StopAdvertisingSession() toggle whether this session is listed in the project's public session list (same switch the host's engine panel sets; last write wins; play-mode entries auto-list - a game that wants private sessions calls StopAdvertisingSession()). Unlisted sessions stay joinable via share link.
329
+ - ListSessions()/JoinSession are relay session discovery; identity is automatic - listings match peers running this project with identical content (same saved version). SessionStage(i)/SessionPlayerCount(i) describe each listed session.
330
+ - OpenSessionBrowser() opens the built-in browser from a game menu.
331
+ - These do NOT create replication endpoints or change sim authority; once joined, the same shared deterministic sim applies.
332
+ - Session UI lists real joined players via GetAllPlayers(); never simulate joins with a local counter/button.
270
333
  - AI players (bots): var bot = SpawnAIPlayer("Rival") spawns a real PlayerObject: '#class player' code + OnSpawned run for it, it appears in GetAllPlayers(), has physics/animations, but no user/camera/UI. The Name arg becomes the bot's object name and its GetPlayerName().
271
334
  - Drive it from script: bot.SetAIMoveVector(dir) (held stick, persists until changed) and bot.PressAIAction(Action.Jump) (tap; Down next frame, auto-release after). The bot's own ...ByThisPlayer input reads and Action events then behave as if a user pressed them.
272
335
  - bot.IsAIPlayer() distinguishes bots; bot.DespawnAIPlayer() (or bot.Destroy()) removes them. Keyboard/mouse reads (IsKeyPressedByThisPlayer, GetMouseTarget*) return false/none for bots.
@@ -404,14 +467,18 @@ event OnSpawned {
404
467
 
405
468
  <object_types>
406
469
  - WorldObject: Game's objects, start off with static/CanCollide physics
470
+ - Scene `position = (x, y, z)` places the object's PIVOT; primitives and library meshes pivot at their base, so z is the bottom (like position_tiles). Effect objects render their authored local origin at `position`.
407
471
  - PlayerObject: Object representing player
408
472
  - RegionObject: Used in weave for detecting object overlap etc. cannot be seen in play-mode, cannot collide, or click
409
473
  - PhysicsObject: like WorldObject, but starts as dynamic/CanCollide physics
410
474
  - CrowdObject: Useful as creatures/walkable-NPCs. like WorldObject, but starts with crowd collision avoidance
411
- - it automatically avoids objects with CanCollide; no extra setup call is needed
475
+ - it steers around other crowd agents and obstacles it approaches at an angle; a blocker dead-ahead STOPS it (avoidance is anti-collision, not pathfinding) - route around structures with tile pathfinding
476
+ - RTS units: route with `MoveAlongTilePathContinuous` on a `crowd` receiver - the path sets the preferred velocity and avoidance shapes it every frame (global path + local separation). The zero-call `StartTilePathFollow` is waypoint-granular, so avoidance cannot shape it mid-segment - use it for overlap-tolerant swarms instead. Terrain cliffs and tilemap walls are invisible to avoidance until `TileGridBakeCrowdObstacles` bakes them from the nav grid.
412
477
  - Avoidance scope: XY-plane only; Z/height is ignored.
478
+ - Gravity is ON at spawn; crowd bodies stand on static geometry, terrain, and tilemaps (not on dynamic objects). ClimbingCrowdObject remains the climb/stack-capable crowd.
413
479
  - For grid-based or fixed-lane games (e.g., Tower Defense), avoid using CrowdObject, as its built-in avoidance can cause objects to move off their intended path.
414
480
  - Prefer it over plain WorldObject for free-roaming monster swarms in top-down games.
481
+ - Bone/pose simcalls (SetBoneRotation family, SetUpperBodyAnimationWeight, SetRenderOffset) reject crowd instances - use a regular object for featured NPCs needing bone-level control.
415
482
  - ClimbingCrowdObject: like CrowdObject, but simulated by the deterministic "climbing crowd" step (lean crowd bodies)
416
483
  - Prefer it over plain WorldObject for player-chasing enemies in flat first-/third-person worlds.
417
484
  - Always seeks the nearest player in XY each tick; players are never pushed (climbers always yield)
@@ -419,13 +486,15 @@ event OnSpawned {
419
486
  - Terrain + static primitive obstacles; supports elevator-like climbing
420
487
  - Can climb/stack on other climbers; large swarms may pile into a single moving blob
421
488
  - DisplayObject: like WorldObject but visual-only — no collision, no interaction events (Touched, ClickThisObject, etc. will never fire), doesn’t respond to forces
489
+ - When a display shell forms a player-reachable structure (building, wall), add a static collision core or invisible collider proxy behind it — display-only geometry is walked through.
422
490
  - EffectObject: visual effects
423
- - PointLightObject: point light
424
- - DiffuseOnlyLightObject: diffuse-only local light with flat core fade shell
425
- - SpotLightObject: spot light
426
- - AreaLightObject: area light
427
- - DirectionalLightObject: the world sun (editor-placed); control it with the SetSun* simcalls, not the generic light setters
491
+ - pointLight: local point light. `pointLight L { position = (0, 0, 150) radius = 500 intensity = 4 color = (1, 0.85, 0.6) }` - radius cm (default 100 = dim 1m pool); optional cast_shadow = true, light_diffuse_scale/light_specular_scale. For a bulb on a mesh prop, prefer an emitter(...) socket in the #mesh (see <mesh_dsl_usage>)
492
+ - spotLight: cone along the object's +X facing (aim with rotation); pointLight keys plus cos_inner/cos_outer = cosine of the half-angle, not degrees (defaults 24deg/30deg)
493
+ - diffuseOnlyLight: diffuse-only, flat core with fade_width (cm) fade shell; pointLight keys minus cast_shadow/light_specular_scale
494
+ - areaLight: area light; pointLight keys minus cast_shadow
495
+ - directionalLight: the world sun (editor-placed); control it with the SetSun* simcalls, not the generic light setters
428
496
  - PositionMarkerObject: position marker object useful as things like waypoints (easily manipulated by human visually in editor)
497
+ - DecalObject: projected surface overlay re-writing color/normal/roughness on the geometry inside its oriented box (bullet holes, stains, scorch marks). Spawn at runtime with SpawnDecal(Material, Position, RotationEuler, Size); scene keyword `decal Name { material = "M" ... }`. Globals: SetAllDecalsEnabled, SetDecalCullingDistanceM, SetAssetSubmeshReceiveDecal
429
498
  </object_types>
430
499
 
431
500
  <variable_types>
@@ -437,9 +506,11 @@ event OnSpawned {
437
506
  - Vector — (X,Y,Z) direction/offset; also used for RGB triples.
438
507
  - Position — world point (X,Y,Z). Alias of Vector; implicit cast both ways.
439
508
  - List<T> — homogeneous typed list; T is Number | Bool | Text | Object | Vector | Position | Timer | a class name (e.g. 'var Waypoints: List<Position>', 'var Minions: List<Monster>'). In List.* signatures below, T is the element type.
440
- - Class-scope 'var X: List<T>' needs no initializer; lists start empty.
509
+ - 'var X: List<T>' needs no initializer (class scope or local); lists start empty.
441
510
  - Bare 'var X: List' infers T from usage; annotate List<T> explicitly when inference reports conflicting evidence.
442
- - No List<List<T>> and no non-empty literals ([1,2,3]): start empty then Add, or Resize(n, default).
511
+ - No List<List<T>>. Non-empty literals ([1,2,3]) are const-only: runtime lists start empty then Add, or Resize(n, default).
512
+ - Class-scope 'const Ids = [10, 20, 30]' (Number/Bool/Text/Vector elements, type inferred) bakes a read-only table into the program: Get/Count/Contains/for-each work, writes are runtime errors. Const tables have zero per-frame snapshot/hash cost — item/monster/config databases belong here, not in runtime lists filled row-by-row at startup.
513
+ - Map<Number, T> — keyed store (T: Number | Bool | Text | Object): Set(K, V), Get(K) (missing key is a runtime error), GetOr(K, Default), Has(K), Remove(K)->Bool, Count(), IsEmpty(), Clear(). Keys compare exactly (no fractional truncation) — use integer keys. Replaces hand-rolled bucket/chain index lists for id -> row lookups (entity id -> row, structure id -> data).
443
514
  - Contains/IndexOf: List<Number|Bool|Vector|Position> and object lists only (not List<Text>/List<Timer>).
444
515
  - Sort()/SortDescending(): in-place numeric sort, List<Number> only.
445
516
  - List<struct>.SortByField("FieldName")/SortByFieldDescending: in-place sort of a record list by one of its Number fields (field name is a compile-checked string literal). Ties keep original order.
@@ -453,17 +524,19 @@ event OnSpawned {
453
524
  }
454
525
  - Fields: Number | Bool | Vector | Position | Object (plain or class-typed, e.g. 'var M: Monster' — member calls through class-typed fields are validated statically). Max 16 slots; Vector/Position count as 3, Object counts as 1. No Text/Timer/nested-struct fields yet. Object fields default to None (zeroed class-scope structs and Resize-grown list elements included).
455
526
  - Construct positionally with one arg per field: 'var S = Stat(100, Vector(1,0,0))'. Read/write fields ('S.Health += 1', 'S.Speed.X = 2') on locals, Self vars, and list elements ('Table.Get(i).Health = 5' writes back); copies are by value ('var C = S' snapshots), so fields on a function's returned struct need a var first.
456
- - Class-scope 'var Stats: Stat' starts zeroed (no struct initializers there; fill in OnSpawned). Structs work as function params/returns.
527
+ - 'var Stats: Stat' without an initializer starts zeroed in any scope, Object fields None (no struct literals at class scope; fill in OnSpawned). Structs work as function params/returns.
457
528
  - List<struct> is supported ('var Table: List<Stat>') with Add/Get/Set/RemoveAt/Swap/Shuffle/Resize(count)/Count/IsEmpty/Clear + for-each; not Contains/IndexOf/Pop/Last/GetRandomElement or Resize with fill.
458
529
  - Lists cap at ~1M elements: an every-frame Add with no Clear/dedupe (e.g. selections, hit logs) reaches the cap in minutes of play and stops the world with CapacityExceeded - clear or bound accumulating lists.
459
530
  - No struct operators ('==' included) — compare fields; no scene-clause overrides.
460
531
 
461
532
  Rules:
462
533
  - The literal None is valid **only** for Object (e.g., 'var o:Object = None').
534
+ - An annotated 'var' may omit its initializer in any scope and starts at the type default: 0 / false / "" / zero vector / None / done Timer / empty List / zeroed struct. Untyped locals still need '= value'.
535
+ - 'const Name = <value>' (class scope or local) folds at compile time with zero snapshot cost; values that never change (tuning numbers, names, data tables) belong in const, not var.
463
536
  </variable_types>
464
537
 
465
538
  <tags>
466
- - Tag is like enum, and can be made up by user without any declaration (Tag.None reserved for empty tag).
539
+ - Tag is like enum, and can be made up by user without any declaration. `Tag.None` is not writable in code; a Tag param always filters to that tag — there is no empty-tag value, so for an unfiltered query use the overload without a Tag param.
467
540
  - Use any made-up name (e.g. Tag.Monster) across classes; the same name resolves to the same tag everywhere.
468
541
  - Assign a tag directly in world description: object MySpawn { tag = Tag.Spawn }
469
542
  - AddTag/RemoveTag add and clear one tag; ClearTag() wipes all tags. Tag membership can be toggled at runtime (e.g. flag/unflag Tag.Burning) and HasTag/GetAllObjectsWithTag update immediately.
@@ -491,11 +564,13 @@ object test : MeleeEnemy { size = (70,70,100), override MaxHP = 100 }
491
564
  <world_description_information>
492
565
  - 1 world unit = 1 cm (centimeter)
493
566
  - Basic cube is 100x100x100 cm, sphere 100 cm diameter
494
- - Basic models: cube, sphere, cylinder, capsule, cone (apex +Z), wedge (slope rises toward +X)
567
+ - Basic models: cube, sphere, cylinder, capsule (long axis +Z), cone (apex +Z), wedge (slope rises toward +X)
495
568
  - Object position is the BOTTOM center: the object rests its base at position.z (top = position.z + size.z). A ground slab with its top at z=0 is position = (0, 0, -H) size = (X, Y, H).
496
569
  - Object size is clamped per axis: 50,000 cm for solid objects, 100,000 cm display-only (10,000 cm dynamic). Meshes scale to object size, so oversized objects render SQUASHED — tile large ground/water/road geometry into pieces under the cap.
497
570
  - cone/wedge collide as boxes. Walkable ramp = rotated thin cube, e.g. size = (630, 300, 20) rotation = (0, -18, 0) rises toward +X
498
- - Marketplace/#mesh models also collide as their AABB box: a boat deck, bridge, or house interior is NOT walkable/enterable by default. Declare `collision = mesh` on the object clause (object Boat { model = "..." collision = mesh }) to collide against the model's actual geometry. Skinned/animated models keep the box.
571
+ - Marketplace/#mesh models also collide as their AABB box: a boat deck, bridge, or house interior is NOT walkable/enterable by default, and interior furniture's box juts far past its real shape, blocking walkways. Declare `collision = mesh` on the object clause (object Boat { model = "..." collision = mesh }) to collide against the model's actual geometry — the right choice for enterable structures AND for furniture players move around. Skinned/animated models keep the box.
572
+ - `collision = off` spawns the object with no collider: nothing blocks on it and Touched/Click never fire (detect pickups by distance) — for movers/markers that must not block. Purely decorative geometry is cheaper as a `display` clause.
573
+ - Colliders are also the hit/UI anchor: Touched/Click/raycasts test the collider box, and hover bars/labels draw on the object they were set on. Size colliders to the visible model and set bars on the visible object, not a hidden collider proxy (symptoms: shots that visually touch but miss; bars floating beside the model).
499
574
  - Player is 40 cm diameter and 180 cm height, to fit inside a 100x100 cm sized tile with gap
500
575
  - Default player spawn: Origin() (0,0,0).
501
576
  - Calling an Object function without a caller implies 'Self.'
@@ -535,6 +610,7 @@ Applies to all physics types: Dynamic, Static, and Kinematic.
535
610
  - Dynamic objects have gravity on by default
536
611
  - Dynamic bodies have no feel knobs (no bounciness/friction/mass calls): use them for incidental 3D physics - debris, tumbling props, knockback - where the exact trajectory doesn't matter. Don't use them for 2D-style games.
537
612
  - For gameplay-tuned motion (sport balls, vehicles, designed arcs: basketball, tennis, racing), keep the object kinematic and integrate your own velocity var per frame (V.Z = V.Z - g*DeltaSecs(); MoveWithVelocityContinuously(V); reflect/steer V on contact - see #class Ball in <example_breakout>). Works because plain kinematic objects have gravity OFF; a gravity-affected character discards scripted Z (see <movement_rules>). Hand-rolled constants stay user-tweakable (@exportRange).
613
+ - Designed arcs that must land on an exact point at an exact time (serves, passes, lobs, throws): ArcToTarget/ArcToTargetWithPeak build the arc once (gravity captured with it), ArcFlightSeconds gives the contact time, SetPosition(ArcPositionAt(arc, t)) drives the object each frame - closed form, so no drift and no velocity kink between rally phases. Feed the remaining time to MoveToArriveIn and PlayAnimationHittingContactIn so mover and clip land on the same instant. Do not hand-author per-phase LerpVector segments.
538
614
  - if you move Static object with MoveTo etc., it becomes Kinematic (behaves like Kinematic platform)
539
615
  </defaults>
540
616
 
@@ -560,7 +636,7 @@ Applies to all physics types: Dynamic, Static, and Kinematic.
560
636
  <defaults>
561
637
  - PlayerObject defaults to kinematic with gravity
562
638
  - `collision = mesh` objects keep mesh collision while static or moved as a kinematic platform (players ride them); giving one gravity or PreventOverlap drops it back to box collision.
563
- - SetCharacterPreventOverlap give kinematic object fake physics avoiding CanCollide objects (turning PreventOverlap off for player with gravity will make it fall through ground)
639
+ - SetCharacterPreventOverlap(true) gives a kinematic object fake-physics separation from CanCollide objects and supplies a gravity character's ground support: off on a player with gravity = falls through the floor
564
640
  - SetCharacterOverlapIgnoreTag(Tag, IsOn) makes this character's overlap resolution skip objects with that tag (selective ghosting, e.g. walk through Tag.Enemy while charging) without touching ground collision; Touched/regions still fire. Prefer it over toggling SetCharacterPreventOverlap or SetCanCollide for pass-through-some-objects behavior.
565
641
  </defaults>
566
642
 
@@ -576,8 +652,11 @@ Applies to all physics types: Dynamic, Static, and Kinematic.
576
652
  - MoveTowardContinuousFacing
577
653
  - MoveTowardContinuousHorizontal
578
654
  - MoveTowardContinuousHorizontalFacing
655
+ - MoveToArriveIn (speed = remaining distance / remaining seconds; pair with ArcFlightSeconds)
656
+ - MoveToArriveInFacing
579
657
  </kinematic_functions>
580
- - On gravity-affected characters (the PlayerObject default; EnableCharacterGravity(true) objects) the Z component of these calls is silently DISCARDED - gravity/jump owns vertical velocity, only X/Y apply, and the mover never lifts. Flying/hovering: SetCharacterGravity(false) (players: SetPlayerModeHover); jumps: Jump(). Gravity-off and bodyless objects honor full 3D velocity.
658
+ - These continuous movers drive ONE frame of velocity then stop: call them every frame (a brain loop that Waits 0.2s between calls crawls at ~1/12 speed).
659
+ - On gravity-affected characters (the PlayerObject default; EnableCharacterGravity(true) objects) the Z component of these calls is silently DISCARDED - gravity/jump owns vertical velocity, only X/Y apply, and the mover never lifts. Flying/hovering: EnableCharacterGravity(false) (players: SetPlayerModeHover); jumps: Jump(). Gravity-off and bodyless objects honor full 3D velocity.
581
660
  - Gravity characters auto-step rises up to 45cm (capped at 1/4 body height for small NPCs); taller needs a ramp or Jump()
582
661
  - To move somewhere over N seconds, use an explicit per-frame loop instead of a fire-and-forget helper:
583
662
  <example_of_move_over_time>
@@ -641,19 +720,20 @@ Applies to all physics types: Dynamic, Static, and Kinematic.
641
720
  if T >= 1 { O.SetPosition(Target) break }
642
721
  O.SetPosition(LerpVector(Start, Target, EaseInOut(T)))
643
722
  }
644
- - SetPosition/SetCenterPosition/SetRotation are continuous "moved" setters: they keep velocity, contacts, and render interpolation, so they are the right per-frame drivers for tweens, movers, and platforms (bodiless, static, or kinematic objects). On force-affected dynamic bodies they fight physics integration (warned in the log) - use SetPhysicsVelocity/AddImpulse there, or SetAffectedByForce(false) first.
723
+ - SetPosition/SetCenterPosition/SetRotation are continuous "moved" setters: they keep velocity, contacts, and render interpolation, so they are the right per-frame drivers for tweens, movers, and platforms (bodiless or kinematic objects; a colliding STATIC re-carves its collider on every set - drive it with Move* movers instead, which auto-convert it to kinematic). On force-affected dynamic bodies they fight physics integration (warned in the log) - use SetPhysicsVelocity/AddImpulse there, or SetAffectedByForce(false) first.
645
724
  - TeleportTo is a discontinuity: it zeroes velocity and snaps interpolation. Use it for instant far moves, not per-frame motion.
646
725
  - EaseOutBack overshoots ~10% past the target near the end (snappy UI pops); EaseOutBounce settles with bounces. Both still end at exactly T=1 -> 1.
647
726
  </easing>
648
727
 
649
728
  <world_markers>
650
729
  - World-anchored HUD markers (lock-on reticles, waypoints, offscreen-target indicators, icon nameplates) are screen-space UI driven by projection. Do NOT build them from thin world cubes or hand-aimed meshes.
651
- - Recipe: a canvas image element + WorldToScreenPosition (call on a PlayerObject; returns base UI viewport coords 1600x900, or Vector(-1,-1,0) when behind/unprojectable) + SetCenterPosition per frame:
730
+ - Recipe: a canvas image element + WorldToScreenPosition (call on a PlayerObject; returns base UI viewport coords 1600x900, or Vector(-1,-1,0) when behind/unprojectable) + SetCenterPosition per frame. Call Marker.SetUiPivot(1, 1) ONCE first: without it, SetPosition/SetCenterPosition both place the element's TOP-LEFT at the point, so a 320x220 marker lands half its size off. The marker must be a direct child of a plain frame/canvas — inside a vStack/hStack/grid the parent's layout wins and the position is ignored.
731
+ Marker.SetUiPivot(1, 1) // once, e.g. OnSpawned
652
732
  var sp = WorldToScreenPosition(Target.GetCenterPosition())
653
- if sp.X >= 0 { Marker.SetVisible(true) Marker.SetCenterPosition(sp) } else { Marker.SetVisible(false) }
733
+ if sp.X >= 0 { Marker.SetVisible(true); Marker.SetCenterPosition(sp) } else { Marker.SetVisible(false) }
654
734
  - Per-player markers (each player sees their own lock-on): put the marker on a SpawnCanvas HUD owned via SetPlayerOwnerIndex and run the loop in '#class player' (see <multiplayer_guidance>).
655
735
  - Offscreen indicator: when the sentinel fires or sp leaves 0..1600 / 0..900, clamp to the screen edge yourself (direction from screen center 800,450 toward sp).
656
- - Before hand-rolling: SetHoverBar (bars/pills above objects, custom material supported), SetSaidText, and SpawnFloatingText already cover the common above-object UI.
736
+ - Before hand-rolling: SetHoverBar (bars/pills above objects, custom material supported), Say/SayTo (speech bubbles above the speaker), and SpawnFloatingText already cover the common above-object UI.
657
737
  </world_markers>
658
738
 
659
739
  <movement_laws>
@@ -661,10 +741,11 @@ Movement Laws (single-frame vs over-time)
661
741
  - Exactly one continuous translate per object per frame (and one continuous rotation). Calls whose names end with Continuous/Continuously are single-frame. If multiple are issued in one frame, last call wins (later calls overwrite earlier ones). Compose motion into one call.
662
742
  - See <time> for over-time/continuous rules.
663
743
  - TeleportTo* is absolute this frame. If a TeleportTo* and any continuous move happen in the same frame, teleport cancels motion and sets the final transform.
664
- - SnapToGround only when setting up an initial position; TeleportTo only when instantly moving far away (it zeroes velocity and snaps interpolation). For authored per-frame motion (tweens/movers) use SetPosition, which keeps velocity and interpolation; see <easing>.
744
+ - SnapToGround only when setting up an initial position; TeleportTo only when instantly moving far away (it zeroes velocity and snaps interpolation). For authored per-frame motion (tweens/movers) on bodiless/kinematic objects use SetPosition, which keeps velocity and interpolation; see <easing>.
745
+ - Arrival at an OBJECT: colliders stop the mover before centers meet, so DistanceTo(target) may never drop below both half-extents (reads as a freeze while Move* keeps firing). Gate on SurfaceGapTo(Target) <= margin instead.
665
746
  - Timing & readback: see <frame_and_scheduling_model> for tick order and GetPosition/GetRotation semantics.
666
747
 
667
- - Clarification: SnapToGround raycasts downward (-Z) from the object's bottom-center and snaps to the first WorldObject hit (ignores PhysicsObject/PlayerObject/CrowdObject etc.). It overrides any Z offset you set earlier in the same frame. For stacking on tiles/objects, compute Z = baseZ + baseSizeZ/2 + itemSizeZ/2 instead of using SnapToGround.
748
+ - Clarification: SnapToGround raycasts downward (-Z) from the object's bottom-center and snaps to the first WorldObject hit (ignores PhysicsObject/PlayerObject/CrowdObject etc.). It overrides any Z offset you set earlier in the same frame. For stacking on tiles/objects, compute Z = baseZ + baseSizeZ/2 + itemSizeZ/2 instead of using SnapToGround. Flat/plane items (rugs, road markings, decals): add ~1cm more — face-on-face at the same height shimmers (z-fights), worse at long view distances.
668
749
  </movement_laws>
669
750
 
670
751
  <inputs>
@@ -672,7 +753,7 @@ Movement Laws (single-frame vs over-time)
672
753
  - Builtin action constants: Action.Primary, Action.Secondary, Action.Interact, Action.Jump.
673
754
  - Custom actions (Weave2 worlds): declare a top-level scene clause `action dash { label="Dash" key=Key.LeftShift icon="image.<owner>.<name>" }` (all properties optional; icon is a QUOTED image url). The name then works everywhere builtins do: Action.dash, IsActionPressedByThisPlayer(Action.dash), event ActionDownByThisPlayer[dash]. Prefer custom actions over raw IsKeyPressed for gameplay verbs: they get a mobile touch button (label text) automatically when the world handles their events, and `key=` gives the desktop binding. Up to 27 per world; a key already bound (or used by another action) is a load error. Undeclared action names are a compile error.
674
755
  - PlayerObject.IsActionPressedByThisPlayer(Action.Primary) .. must be in player class; checks this player's held action.
675
- - PlayerObject.GetMoveVectorWorld() / GetMoveVectorCamera() .. this player's normalized move vector; diagonals clamp to length 1, thumbstick magnitudes below 1 are preserved.
756
+ - PlayerObject.GetMoveVectorWorld() / GetMoveVectorCamera() .. this player's normalized move vector; diagonals clamp to length 1, thumbstick magnitudes below 1 are preserved. World = raw input on fixed world axes (W = +Y), camera-independent; Camera = rotated by the player's orbit yaw, which per-frame camera overrides FREEZE - under SetCamera*ThisFrame, derive steering from your authored camera yaw instead.
676
757
  - event ActionDownByThisPlayer[Primary] / ActionUpByThisPlayer[Primary] .. must be in player class; options are the builtins plus declared action names.
677
758
  - Current builtin bindings: Left mouse -> Primary, Right mouse -> Secondary, E -> Interact, Space -> Jump.
678
759
  - Current movement bindings: WASD and mobile thumbstick feed the shared move vector.
@@ -691,6 +772,7 @@ Movement Laws (single-frame vs over-time)
691
772
  - Binding P in a Keyboard event overrides the built-in P sim-pause in game mode (editor P still pauses)
692
773
  - Delivery: ALL Weave input events (keyboard, mouse, action) fire in GAME mode only - editor mode delivers none of them (use IsKeyPressedByThisPlayerInEditor for editor tooling). Global KeyboardDown[..] reaches only live objects of the handler's class and skips objects currently selected in the editor (selection survives pressing Play) - put input handlers in `#class player`, where ByThisPlayer/Action events always land.
693
774
  - AI bots (SpawnAIPlayer) have NO keyboard at all and mobile sends only WASD + touch buttons: actions are the only input verb that works for every participant. Bare IsKeyPressed / KeyboardDown[..] are any-player and always false for bots/mobile.
775
+ - Hold-to-drain abilities (turbo/sprint): do not gate on `IsKeyPressedByThisPlayer(..) and Fuel > 0` — with passive regen that re-triggers every frame at empty and strobes FX/sound. Keep an Armed flag: disarm when fuel hits 0; re-arm only in KeyboardDownByThisPlayer (or the action's press event) after release + cooldown.
694
776
  - Object.GetMouseTargetPosition (caller must be a PlayerObject) returns a far point along the mouse ray when nothing is hit; use GetMouseTargetObject to test what was hit
695
777
  - Mobile/touch: pinch in/out maps to mouse scroll (event OnMouseScroll)
696
778
  </inputs>
@@ -717,34 +799,34 @@ Movement Laws (single-frame vs over-time)
717
799
  <colors>
718
800
  - colors are Vector rgb 0..1, e.g. SetColor(Vector(1, 0.5, 0)); ColorHex(Hex:Text) parses hex text like #FF8000
719
801
  - Default daylight leans slightly cool (sky ambient), most visibly in shadows - that is the intended look; author materials in their intended base colors rather than counter-tinting them for it.
720
- - Scene grade (SetContrast/SetSaturation/SetExposure/SetSkyboxHue) shifts every color in the scene at once - a fit for an explicitly requested mood, in small steps, rather than for correcting one object's color.
802
+ - Scene lighting/grade is user-tuned in the Adjust panel (see <adjuster_usage>): to fix one object's color, recolor that object/material - never with scene-wide grade.
721
803
  </colors>
722
804
 
723
805
  <time>
724
- - Wait(1) .. Wait 1 sec
806
+ - Wait(1) .. Wait 1 sec (event bodies only; not inside custom functions)
725
807
  - forever { ... } runs its scope once per frame (implicit WaitNextFrame() between iterations)
726
808
  - doesn't need WaitNextFrame() to work
727
809
  - use Wait(seconds) inside forever if you want slower than once-per-frame
728
810
  - while { ... } is same-frame (no implicit yield). Use only for bounded work that must finish this frame. For per-frame logic/conditions, prefer forever + if.
729
- - func in outer scope cannot use function that activate over time like Wait, WaitNextFrame
730
- - Clarification: "Continuous" functions (names ending in Continuous/Continuously) are single-frame and must be called every frame you want the effect. They ARE allowed inside funcs.
731
- - Clarification: "Over-time" functions (multi-frame, e.g. Wait, WaitNextFrame) run across multiple frames. These are NOT allowed inside funcs; use them in events instead.
811
+ - A same-frame loop stuck on movement/physics/time (e.g. while GetPosition().DistanceTo(t) > r { MoveTowardContinuous.. }) trips the SameFrameLoopBudgetExceeded guard - use forever + if, or Wait in the loop.
812
+ - Over-time calls (Wait, WaitNextFrame) work only in event bodies, never inside funcs. Continuous* calls are single-frame (call every frame you want the effect) and are fine inside funcs.
732
813
  </time>
733
814
 
734
815
  <camera>
735
816
  - default camera is third person with mouse-look (like fortnite).. mouse target will just be crosshair target
736
- - SetIsMouseLookOn(true) is the default mouse look like counter-strike and fortnite (SetCameraToFirstPerson also turns it on): the cursor is hidden and pinned to screen center, so players CANNOT click UI buttons — call SetIsMouseLookOn(false) while a menu/shop/end screen is open, restore true on close (touch players can still tap)
737
- - Camera always follows PlayerObject every frame unless overridden by SetCameraThisFrame()/SetCameraLookAtThisFrame()/SetChaseCameraThisFrame() (each is a one-frame override; call every frame to keep it).
817
+ - SetIsMouseLookOn(true) is the default mouse look like counter-strike and fortnite (SetCameraToFirstPerson also turns it on): the cursor is hidden and pinned to screen center, so players CANNOT click UI buttons — call SetCursorUiMode(true) while a menu/shop/end screen is open (releases look AND shows the cursor), SetCursorUiMode(false) on close (touch players can still tap). Spectator + per-frame camera overrides (SetCameraLookAtThisFrame) keep the cursor pinned+hidden while the script owns the camera angles: commander-camera/RTS worlds call SetCursorUiMode(true) once at player spawn
818
+ - Camera always follows PlayerObject every frame unless overridden by SetCameraThisFrame()/SetCameraLookAtThisFrame()/SetChaseCameraThisFrame() (each is a one-frame override; call every frame to keep it) — or parked with SetCameraFixed(Position, LookAt), the persistent one-call variant. While a per-frame override or fixed camera is active the mouse no longer steers the camera. The camera always views the caller's own PlayerObject; no simcall views through another player's object.
819
+ - SetCameraFixed(Position, LookAt) parks the camera (security cam / lobby / cinematic shot): persists across frames AND save/reload, camera collision is off while fixed, mouse-look is ignored; any camera-mode switch (SetCameraToThirdPerson(...) etc.) releases it — SetIsMouseLookOn/SetCursorUiMode do NOT. Prefer it over a per-frame SetCameraLookAtThisFrame loop for static shots.
738
820
  - Default camera yaw/rotation is 0 degrees.
739
821
  - At camera yaw 0: camera forward is +X, camera up is +Z, and camera left is +Y.
740
822
  - Camera yaw is right-hand (same as object yaw): `SetCameraRotation(0)` faces +X, `SetCameraRotation(90)` faces +Y, `SetCameraRotation(-90)` faces -Y.
741
823
  - Objects also face +X at rotation 0, with +Y their LEFT side (right-hand system): a character's right-side parts sit at negative y.
742
- - Angles in this API are degrees. For trig on degree angles use `SinDegree()`/`CosDegree()`/`TanDegree()` (and `Atan2Degree()` returns degrees); `Sin()`/`Cos()`/`Tan()`/`Atan2()` are radians.
824
+ - Angles in this API are degrees. Trig is Number methods on the angle: `Angle.SinDegree()`/`CosDegree()`/`TanDegree()`, `Y.Atan2Degree(X)` (degrees); `.Sin()`/`.Cos()`/`.Tan()`/`.Atan2(X)` are radians. There are no global trig functions.
743
825
  - SetCameraToTopDown() defaults to 70-degree tilt down, 900 units zoom, facing +Y.
744
- - Camera-mode switch signatures: `SetCameraToTopDown()`, `SetCameraToFirstPerson()`, `SetCameraToSideView()` take no args; only `SetCameraToThirdPerson(IsMouseLookOn)` takes one Bool. Collision is always restored on switch.
826
+ - Camera-mode switch signatures: `SetCameraToTopDown()`, `SetCameraToFirstPerson()`, `SetCameraToSideView()` take no args; only `SetCameraToThirdPerson(IsMouseLookOn)` takes one Bool. Switches restore collision+gravity, except an active SetPlayerModeHover/Spectator survives (SetPlayerModeGrounded exits it).
745
827
  - 2D-style games (top-down or side-view camera) are still 3D worlds: keep a ground/background object so the camera never shows bare skybox, and for side-view keep gameplay on a constant Y plane. A compact ~500x500 cm play area suits board/arcade layouts (chess, brick-breaker).
746
828
  - Third-person body faces its MOVEMENT direction, not the camera; GetCrosshairDirection() is camera-only. So a gun/item aimed via crosshair won't match the body when looking around while standing still. Fix: SetOrientingWithCameraYaw(true) makes the body track the camera yaw.
747
- - SetCameraZoom .. 0 will make it first person, 900 will make it third-person with camera 900 units away from character
829
+ - SetCameraZoom .. 0 will make it first person, 900 will make it third-person with camera 900 units away from character (zoom alone never enables mouse steering; use SetCameraToFirstPerson() for playable first person)
748
830
  - Third-person default framing: zoom 525 with pivot offset (0, -39, 87) = over-the-shoulder, body slightly left of screen center; override via SetCameraZoom()/SetCameraPivotOffset()
749
831
  - player movement must come from the player class: either SetDefaultPlayerControlsEnabled(true) in OnSpawned (attaches the built-in default controls, equivalent to the class below) or a coded movement loop — with neither, the player and thus the camera cannot move in play-mode. SetDefaultPlayerControlsEnabled(false) detaches them again (cutscene/stun freeze).
750
832
  - built-in default player controls (also the template for custom movement):
@@ -784,9 +866,6 @@ event OnSpawned {
784
866
  - player object always stand upright (lock rotation axis to z)
785
867
  </camera>
786
868
 
787
- <built_in_functions>
788
- - example: ObjectA.LookAt(ObjectB.GetPosition())
789
- </built_in_functions>
790
869
  <built_in_function_list>
791
870
  PrintNumber(Number: Number)
792
871
  Print(Text: Text)
@@ -800,8 +879,8 @@ SetAutoGameLogOn()
800
879
  PlayerPrint(PlayerIndex: Number, Text: Text)
801
880
  SetSunIntensity(Intensity: Number) // Default: 4
802
881
  SetSunSpecularScale(Scale: Number) // Default: 1
803
- SetSunRotation(Rotation: Number) // Default: 10
804
- SetSunTilt(Tilt: Number) // Default: 46
882
+ SetSunRotation(Rotation: Number) // Default: 10. Azimuth, degrees from +X toward +Y; shadows fall toward Rotation+180
883
+ SetSunTilt(Tilt: Number) // Default: 46. Elevation above the horizon, degrees (90 = overhead)
805
884
  SetSkybox(Skybox: Object)
806
885
  SetSkyboxHorizonTilt(AngleDegrees: Number) // Default: 0
807
886
  SetSkyboxRotation(RotationDegrees: Number) // Default: 0
@@ -831,11 +910,10 @@ SetShadowDepthBiasConstant(Units: Number) // Default: 1
831
910
  SetShadowDepthBiasSlopeScale(Scale: Number) // Default: 2.5
832
911
  SetShadowOpacity(Opacity01: Number) // Default: 1
833
912
  SetStaticLodTransitions(Lod1Size: Number, Lod2Size: Number, Lod3Size: Number) // Default: 0.18, 0.07, 0.025
834
- SetStaticLodShadowTransitions(Lod1Size: Number, Lod2Size: Number, Lod3Size: Number) // Default: 0.35, 0.18, 0.08
835
- SetStaticLodShadowSkipTexels(Cascade2SkipTexels: Number, Cascade3SkipTexels: Number, Lod0ExceptionTexels: Number) // Default: 24, 64, 768
836
- SetContrast(Contrast: Number) // Contrast multiplier around mid-gray. Default: 1 = neutral; 0 = flat gray, 2 = double
913
+ SetStaticLodShadowTransitions(Lod1Size: Number, Lod2Size: Number, Lod3Size: Number) // Same screen-size units as SetStaticLodTransitions, but for meshes drawn into the sun-shadow maps (defaults higher: shadows tolerate earlier LOD). Lower toward the view values if a large caster's shadow silhouette looks low-poly or pops. Default: 0.35, 0.18, 0.08
914
+ SetContrast(Contrast: Number) // Power curve around mid-gray, pre-tonemap: shadows compress, never crush to black. Default: 1 = neutral; 0 = flat gray, 2 = strong
837
915
  SetSaturation(Saturation: Number) // Saturation multiplier. Default: 1 = neutral; 0 = grayscale, 2 = vivid
838
- SetExposure(Exposure: Number) // Default: 0
916
+ SetExposure(Exposure: Number) // EV stops, pre-tonemap: +1 = twice as bright, -1 = half. Default: 0
839
917
  SetShadowLift(Amount: Number) // Default: 2; 0 = off
840
918
  SetShadowLiftThreshold(Threshold: Number) // Default: 0.06
841
919
  SetSmaaEdgesThreshold(Threshold01: Number) // Default: 0.01
@@ -852,12 +930,12 @@ SetAllLocalShadowsEnabled(Enabled: Bool)
852
930
  SetAllDecalsEnabled(Enabled: Bool)
853
931
  SetGravity(Gravity: Number)
854
932
  SetTerminalVelocityZ(MaxFallSpeed: Number)
855
- SetFogStartDistance(StartDistance: Number) // Default: 10. The skybox is fogged only via SetFogSkyboxAffect or height fog; underwater, automatic murk overrides all fog settings
856
- SetFogEndDistance(EndDistance: Number) // Default: 600
933
+ SetFogStartDistance(StartDistance: Number) // Default: 10. Meters (not cm). The skybox is fogged only via SetFogSkyboxAffect or height fog; underwater, automatic murk overrides all fog settings
934
+ SetFogEndDistance(EndDistance: Number) // Default: 600. Meters
857
935
  SetHeightFogEnabled(Enabled: Bool)
858
- SetHeightFogTop(Top: Number) // Default: 30
859
- SetHeightFogBottom(Bottom: Number) // Default: 0
860
- SetHeightFogSoftness(Softness: Number) // Default: 0.25
936
+ SetHeightFogTop(Top: Number) // Default: 50. Meters
937
+ SetHeightFogBottom(Bottom: Number) // Default: 0. Meters
938
+ SetHeightFogSoftness(Softness: Number) // Default: 1
861
939
  SetGpuCullingDistance(DistanceMeters: Number) // Default: 0 (distance culling off)
862
940
  SetFrustumCulling(Enabled: Bool)
863
941
  SetFrustumCullingMargin(MarginMeters: Number) // Default: 0
@@ -867,12 +945,17 @@ SetGrassTerrainActiveDistanceM(DistanceMeters: Number) // Default: 140
867
945
  SetGrassCullDistanceM(DistanceMeters: Number) // Default: 140
868
946
  SetGrassFadeDistancePercent(FadeBandPercent: Number) // Default: 50
869
947
  SetGrassFarThinningPower(Power: Number) // Default: 3
948
+ SetTreeLodSwitchDistanceM(DistanceMeters: Number) // Leaf cards -> whole-tree impostor switch distance, desktop meters (mobile auto-scales down). Default: 0 = auto (140 / 90)
949
+ SetTreeMeshDistanceM(DistanceMeters: Number) // Real branch-mesh max draw distance, desktop meters; impostors only beyond. Default: 0 = auto (400 / 260)
950
+ SetTreeCullDistanceM(DistanceMeters: Number) // Trees vanish entirely past this, desktop meters (the outermost bound - it caps the mesh + leaf-switch ranges under it). Default: 0 = auto (800 / 300)
951
+ SetTreeFadeDistancePercent(FadeBandPercent: Number) // Card->impostor crossfade band as % of the switch distance, clamped 5..80. Default: 36
952
+ SetTreeFarLeafDensityPercent(DensityPercent: Number) // Leaf-card density kept across the fade band, clamped 10..300; <100 thins earlier (cheaper), >100 denser. Default: 100
870
953
  SetAssetSubmeshHue(ModelAsset: Object, SubmeshIndex: Number, HueDegrees: Number)
871
954
  SetAssetSubmeshSaturation(ModelAsset: Object, SubmeshIndex: Number, Saturation: Number) // Chroma multiplier: 1 = neutral, 0 = grayscale
872
955
  SetAssetSubmeshBrightness(ModelAsset: Object, SubmeshIndex: Number, Brightness: Number) // Gain: 1 = neutral
873
956
  SetAssetSubmeshContrast(ModelAsset: Object, SubmeshIndex: Number, Contrast: Number) // Multiplier around mid-gray: 1 = neutral, 0 = flat gray. Use only when the user asks for it specifically; Tint/Brightness/Saturation cover normal fitting
874
957
  SetAssetSubmeshTint(ModelAsset: Object, SubmeshIndex: Number, TintRGB: Vector)
875
- SetAssetSubmeshEmissionIntensity(ModelAsset: Object, SubmeshIndex: Number, Intensity: Number)
958
+ SetAssetSubmeshEmissionIntensity(ModelAsset: Object, SubmeshIndex: Number, Intensity: Number) // Linear glow multiplier for this submesh; composes with object emission
876
959
  SetAssetSubmeshMinRoughness(ModelAsset: Object, SubmeshIndex: Number, MinRoughness: Number)
877
960
  SetAssetSubmeshRoughnessSmoothStepRemap(ModelAsset: Object, SubmeshIndex: Number, Enabled: Bool)
878
961
  SetAssetSubmeshNormalStrength(ModelAsset: Object, SubmeshIndex: Number, Strength: Number)
@@ -1005,8 +1088,8 @@ Object.GetRotation() -> Vector
1005
1088
  Object.GetScale() -> Vector
1006
1089
  Object.GetSize() -> Vector
1007
1090
  Object.GetSizeY() -> Number
1008
- Object.GetSizeX() -> Number
1009
- Object.GetHeight() -> Number
1091
+ Object.GetSizeX() -> Number // Footprint X size in cm (Y: GetSizeY). Vertical size is GetHeight - there is no GetSizeZ
1092
+ Object.GetHeight() -> Number // Vertical (Z) size in cm
1010
1093
  Object.GetHalfHeight() -> Number
1011
1094
  Object.GetHeightInTiles() -> Number
1012
1095
  Object.GetHalfHeightInTiles() -> Number
@@ -1024,7 +1107,7 @@ Object.MoveTowardContinuousHorizontal(Position: Position, Speed: Number)
1024
1107
  Object.MoveTowardContinuousHorizontalFacing(Position: Position, Speed: Number) // Facing means object's forward (local +X) is aligned to world-space Direction
1025
1108
  Object.MoveInDirectionContinuously(Direction: Vector, Speed: Number)
1026
1109
  Object.MoveInDirectionContinuouslyFacing(Direction: Vector, Speed: Number) // Facing means object's forward (local +X) is aligned to world-space Direction. On players with SetOrientingWithCameraYaw(true) the facing part is ignored (camera yaw owns the body).
1027
- Object.MoveWithVelocityContinuously(Velocity: Vector) // On gravity-affected characters (the PlayerObject default) the Z component is silently DISCARDED - gravity/jump owns vertical velocity; only X/Y apply. For flying/hovering turn gravity off first (SetCharacterGravity(false); players: SetPlayerModeHover); for jumps use Jump(). Gravity-off and bodyless objects honor full 3D
1110
+ Object.MoveWithVelocityContinuously(Velocity: Vector) // On gravity-affected characters (the PlayerObject default) the Z component is silently DISCARDED - gravity/jump owns vertical velocity; only X/Y apply. For flying/hovering turn gravity off first (EnableCharacterGravity(false); players: SetPlayerModeHover); for jumps use Jump(). Gravity-off and bodyless objects honor full 3D
1028
1111
  Object.MoveWithVelocityContinuouslyFacing(Velocity: Vector)
1029
1112
  Object.RotateLeftInstantly(Degree: Number)
1030
1113
  Object.RotateRightInstantly(Degree: Number)
@@ -1047,12 +1130,12 @@ Object.GetPlayerInputDirectionCamera(ForwardKey: Number, BackwardKey: Number, Le
1047
1130
  Object.GetMoveVectorWorld() -> Vector
1048
1131
  Object.GetMoveVectorCamera() -> Vector
1049
1132
  IsKeyPressed(Key: Number) -> Bool
1050
- Object.GetMousePositionDelayed() -> Vector // Player only. Sim cursor, 1600x900 base space; delayed a few frames behind the OS cursor. Clicks/hover/aim/drag/tooltips have dedicated calls (Click events, IsMouseOnUi, GetMouseTarget*, SetUiDraggable, SetUiFollowsCursor)
1133
+ Object.GetMousePositionDelayed() -> Vector // Player only. Sim cursor, 1600x900 base space (same space as UI rects; can run negative/past-base in the letterbox bands of non-16:9 windows); delayed a few frames behind the OS cursor, so a read cached across frames is NOT a click/drag anchor - under load it lags the press; anchor on Event.MouseDownPosition/Event.MousePosition. Clicks/hover/aim/drag/tooltips have dedicated calls (Click events, IsMouseOnUi, GetMouseTarget*, SetUiDraggable, SetUiFollowsCursor)
1051
1134
  Object.GetMouseDirection() -> Vector
1052
- Object.IsMouseOnUi() -> Bool // returns true if this UI element is hovered. Shared canvas: any player. Owned canvas: owner only.
1135
+ Object.IsMouseOnUi() -> Bool // True if the cursor's click target (topmost interactable or block_input element) is this element or inside it; screenCanvas receiver: over ANY of that canvas's clickable/blocking elements (empty canvas space stays false) - the usual whole-HUD gate. Gate raw click/action handlers with it. Plain labels/images never match (give them block_input=true or a class). Shared canvas: any player. Owned canvas: owner only.
1053
1136
  Object.GetMouseTargetObject() -> Object
1054
1137
  Object.GetMouseTargetPosition() -> Position
1055
- Object.GetMouseTargetPositionOnPlaneZ(PlaneZ: Number) -> Position
1138
+ Object.GetMouseTargetPositionOnPlaneZ(PlaneZ: Number) -> Position // Player only. Cursor-ray hit on plane z=PlaneZ; Invalid position when the ray misses (cursor at/above horizon) - gate with .IsValid()
1056
1139
  Object.GetMouseTargetNormal() -> Vector
1057
1140
  Object.GetMouseTargetObjectWithTag(Tag: Number) -> Object
1058
1141
  Object.GetMouseTargetPositionWithTag(Tag: Number) -> Position
@@ -1062,7 +1145,7 @@ Object.GetMouseTargetPlayer() -> Object
1062
1145
  Object.GetMouseTargetPlayerWithTag(Tag: Number) -> Object
1063
1146
  Object.GetCrosshairDirection() -> Vector
1064
1147
  Object.GetCrosshairTargetObject() -> Object
1065
- Object.GetCrosshairTargetPosition() -> Position // On miss, returns a point 20000 units along the crosshair ray. For gunfire: bullet Dir = (this - GetWeaponAnchorWorldPos("muzzle")).GetNormal().
1148
+ Object.GetCrosshairTargetPosition() -> Position // On miss, returns a point 1000 units along the crosshair ray. For gunfire: bullet Dir = (this - GetWeaponAnchorWorldPos("muzzle")).GetNormal().
1066
1149
  Object.GetCrosshairTargetNormal() -> Vector
1067
1150
  Object.GetCrosshairTargetObjectWithTag(Tag: Number) -> Object
1068
1151
  Object.GetCrosshairTargetPositionWithTag(Tag: Number) -> Position
@@ -1144,7 +1227,7 @@ Object.AddTag(Tag: Number)
1144
1227
  Object.HasTag(Tag: Number) -> Bool
1145
1228
  Object.ClearTag()
1146
1229
  Object.GetColor() -> Vector
1147
- Object.IsEditorMode() -> Bool
1230
+ Object.IsEditorMode() -> Bool // Per-player state; player receiver (runtime error on non-players). From non-player classes call it on a player, e.g. GetAllPlayers().Get(0).IsEditorMode()
1148
1231
  Object.IsMouseLookOn() -> Bool
1149
1232
  Object.IsMouseVisible() -> Bool
1150
1233
  Object.IsCrosshairVisible() -> Bool
@@ -1233,14 +1316,14 @@ TileGridFindPath8(Grid: List, StartX: Number, StartY: Number, GoalX: Number, Goa
1233
1316
  TileGridPathTile(Grid: List, TileId: Number, OriginTile: Vector) -> Vector
1234
1317
  TileGridPathWorld(Grid: List, TileId: Number, OriginTile: Vector) -> Position
1235
1318
  Object.GetTile() -> Position
1236
- Object.GetMouseTargetTileOnZeroZ() -> Vector
1319
+ Object.GetMouseTargetTileOnZeroZ() -> Position // Player only. Tile index (hit/TileSizeXY floored) of the cursor ray on z=0; Invalid on miss - gate with .IsValid()
1237
1320
  Object.WorldToScreenPosition(Position: Position) -> Vector // Base UI viewport coords (1600x900). Returns Vector(-1,-1,0) if behind/unprojectable.
1238
1321
  Object.LocalOffsetToWorld(OffsetLocal: Position) -> Position // Origin: GetPosition()
1239
1322
  Object.LocalDirectionToWorld(DirLocal: Vector) -> Vector
1240
- Object.EquipWeapon(WeaponModel: Object) // attach a weapon model to this object's hand (render) + enable weapon anchors (sim)
1323
+ Object.EquipWeapon(WeaponModel: Object) // attach a weapon model to this object's hand (render) + enable weapon anchors (sim). Use for ALL player-held items - never a per-frame position-follow loop: scripts read sim aim, which trails the instantly-rendered camera, so the item swings ~100ms late on every turn. Aim/strafe games: add SetOrientingWithCameraYaw(true). Models with no grip data are held at a bbox guess (mid-body grips): give #mesh weapons socket("grip"); fix library/store models with SetModelAnchor
1241
1324
  Object.UnequipWeapon() // remove the held weapon
1242
1325
  Object.GetHeldWeapon() -> Object // held weapon model, or InvalidId when unarmed
1243
- Object.GetWeaponAnchorWorldPos(AnchorName: Text) -> Position // AnchorName can be any model anchor; returns held-weapon anchor position in world space
1326
+ Object.GetWeaponAnchorWorldPos(AnchorName: Text) -> Position // AnchorName can be any model anchor; returns held-weapon anchor position in world space. Market-model anchors register only once the model has downloaded (OnSpawned can be too early); SetModelAnchor anchors are available immediately
1244
1327
  Object.LookAtTileHorizontal(TileX: Number, TileY: Number)
1245
1328
  Object.LookAtTileHorizontal(TileX: Number, TileY: Number, TileZ: Number)
1246
1329
  Object.LookAtTileHorizontal(TileXYZ: Vector)
@@ -1258,10 +1341,11 @@ SpawnAtTile(SavedObject: Object, TileXYZ: Vector) -> Object
1258
1341
  Object.TeleportToTile(TileX: Number, TileY: Number)
1259
1342
  Object.TeleportToTile(TileX: Number, TileY: Number, TileZ: Number)
1260
1343
  Object.TeleportToTile(TileXYZ: Vector)
1261
- Object.SetRotation(Rotation: Vector) // Rotates about GetPosition() (bottom-center pivot for mesh objects). To spin about the middle: SetRotation(R) then SetCenterPosition(C) with C the fixed world center
1262
- Object.SetRotation(X: Number, Y: Number, Z: Number) // Rotates about GetPosition() (bottom-center pivot for mesh objects). To spin about the middle: SetRotation(R) then SetCenterPosition(C) with C the fixed world center
1344
+ Object.SetRotation(Rotation: Vector) // Rotates about GetPosition() (bottom-center pivot for mesh objects). To spin about the middle: SetRotation(R) then SetCenterPosition(C) with C the fixed world center; any other hinge point: SetRotationWithPivot
1345
+ Object.SetRotation(X: Number, Y: Number, Z: Number) // Rotates about GetPosition() (bottom-center pivot for mesh objects). To spin about the middle: SetRotation(R) then SetCenterPosition(C) with C the fixed world center; any other hinge point: SetRotationWithPivot
1263
1346
  Object.SetGroupRotationWithPivot(Pivot: AnyBasicType, Rotation: Vector)
1264
1347
  Object.LookAtGroupWithPivot(Pivot: AnyBasicType, Target: AnyBasicType)
1348
+ Object.SetRotationWithPivot(Pivot: Position, Rotation: Vector) // Part swings around its middle instead of its mount (wing root, tail base, door edge) -> rigid-rotate about world-space Pivot: sets Rotation and moves GetPosition accordingly. Rejects gravity/force receivers. Group receivers: SetGroupRotationWithPivot
1265
1349
  Object.SetScale(Scale: Vector) // raw multiplier of the model's authored size; prefer SetSize (world-unit footprint) for world objects AND effects (effects: size scales vs their boundingBoxSize)
1266
1350
  Object.SetScale(X: Number, Y: Number, Z: Number) // raw multiplier of the model's authored size; prefer SetSize (world-unit footprint) for world objects AND effects (effects: size scales vs their boundingBoxSize)
1267
1351
  Object.SetSize(Size: Vector) // Resizes around the object's CENTER: a grounded object sinks by half the growth (re-ground after, or use ScaleToHeight which stays base-anchored). Per-axis values stretch imported models - prefer uniform scaling for marketplace meshes
@@ -1271,11 +1355,12 @@ Object.SetSizeTilesUniform(Size: Number)
1271
1355
  Object.SetSizeTiles(Size: Vector)
1272
1356
  Object.SetSizeTiles(X: Number, Y: Number, Z: Number)
1273
1357
  Position.IsCloseXY(Other: Position) -> Bool
1358
+ Position.IsValid() -> Bool // False for Invalid positions (the GetMouseTarget* miss sentinel); position-writing calls (SetPosition/TeleportTo/Spawn*) error on Invalid
1274
1359
  Object.ScaleToHeight(Height: Number) // Uniform scale anchored at the base/bottom (feet stay planted) - the safe resize for characters and imported models. SetSize* grows around the CENTER: a grounded object sinks by half the growth
1275
1360
  Object.SnapToGround() // Snap bottom-center downward to first collider; ignores prior Z offsets
1276
1361
  Object.SnapToGroundWithTag(Tag: Number)
1277
1362
  Object.SnapToGroundIgnoreTag(Tag: Number)
1278
- Object.SetColor(RGB: Vector)
1363
+ Object.SetColor(RGB: Vector) // Tint that MULTIPLIES the material's existing colors (white = unchanged, black = black); it does not replace the albedo
1279
1364
  Object.SetColor(R: Number, G: Number, B: Number)
1280
1365
  Object.SetOpacity(Opacity01: Number)
1281
1366
  Object.SetDebugMetallic(Metallic01: Number)
@@ -1288,19 +1373,21 @@ Object.SetWaterDeepColor(RGB: Vector) // Deep-water tint.
1288
1373
  Object.SetWaterDeepColor(R: Number, G: Number, B: Number)
1289
1374
  Object.SetWaterColorMixRange(ShallowCm: Number, DeepCm: Number) // Depth range for shallow-to-deep tint.
1290
1375
  Object.SetWaterTransparency(Alpha: Number) // Water alpha/refraction blend.
1291
- Object.SetEmissionColor(RGB: Vector)
1292
- Object.SetEmissionColor(R: Number, G: Number, B: Number)
1293
- Object.SetEmissionIntensity(Intensity: Number)
1376
+ Object.SetEmissionColor(RGB: Vector) // Glow tint; unset/near-black = glows in the object's own color
1377
+ Object.SetEmissionColor(R: Number, G: Number, B: Number) // Glow tint; unset/near-black = glows in the object's own color
1378
+ Object.SetEmissionIntensity(Intensity: Number) // Linear glow: 0 off, 1 soft self-lit, 4 lamp, 16+ neon bloom
1294
1379
  Object.SetLightIntensity(Intensity: Number)
1295
- Object.SetLightFadeWidth(FadeWidth: Number)
1380
+ Object.SetLightFadeWidth(FadeWidthCm: Number) // diffuseOnlyLight only (edge fade band; default 30, clamped to the light radius)
1296
1381
  Object.SetLightCastShadow(Enabled: Bool)
1297
1382
  Object.SetLightDiffuseScale(Scale: Number)
1298
1383
  Object.SetLightSpecularScale(Scale: Number)
1299
- Object.GetLightFadeWidth() -> Number
1384
+ Object.GetLightFadeWidth() -> Number // diffuseOnlyLight only
1300
1385
  Object.GetLightDiffuseScale() -> Number
1301
1386
  Object.GetLightSpecularScale() -> Number
1387
+ Object.SetLightRadius(RadiusCm: Number)
1388
+ Object.GetLightRadius() -> Number
1302
1389
  Object.SetTeam(TeamId: Number)
1303
- Object.GetTeam() -> Number // Unset team = 32767 (no SetTeam yet; players spawn teamless). Compare against team ids you assigned; never assume 0/1.
1390
+ Object.GetTeam() -> Number // Unset team = 32767 (no SetTeam or scene `team =` yet; players spawn teamless). Compare against team ids you assigned; never assume 0/1.
1304
1391
  Object.SetPlayerOwnerIndex(PlayerIndex: Number)
1305
1392
  Object.GetPlayerOwnerIndex() -> Number
1306
1393
  Object.GetPlayerName() -> Text
@@ -1321,8 +1408,10 @@ Object.SpawnFloatingText(Text: Text)
1321
1408
  Object.SpawnFloatingText(Text: Text, Color: Vector)
1322
1409
  Object.SetHoverHPBar(HP: Number, MaxHP: Number)
1323
1410
  Object.SetHoverSPBar(SP: Number, MaxSP: Number)
1324
- Object.SetHoverBar(BarIndex: Number, Value: Number, MaxValue: Number, Color: Vector, BackgroundColor: Vector, OffsetX: Number, OffsetY: Number, Width: Number, Height: Number)
1325
- Object.ClearHoverBars()
1411
+ Object.SetHoverBar(BarIndex: Number, Value: Number, MaxValue: Number, Color: Vector, BackgroundColor: Vector, OffsetX: Number, OffsetY: Number, Width: Number, Height: Number) // Floating stat bar above the caller at BarIndex 0..=29 (30/31 are the built-in SP/HP bars — use SetHoverSPBar/SetHoverHPBar). MaxValue <= 0 hides the bar. Overlapping bars on one object auto-separate toward their intended side; offsets need not be pixel-exact
1412
+ Object.ClearHoverBars() // Remove all of the caller's hover bars; SetHoverText("") clears the label. Objects reused without Destroy (repositioned indicators, manual pools) keep their previous life's bars and label - clear on repurpose
1413
+ Object.SetHoverText(Text: Text) // Persistent label above the object (unit names, vendor tags); empty Text removes it
1414
+ SetPlayerNamesVisible(IsVisible: Bool) // Default: visible. Hide the engine's username labels when drawing custom nameplates
1326
1415
  DrawLine(Start: Position, End: Position, Color: Vector, Thickness: Number)
1327
1416
  DrawSphere(Position: Position, Radius: Number, Color: Vector)
1328
1417
  DrawSphere(Position: Position, Radius: Number)
@@ -1331,16 +1420,24 @@ DrawDot(Position: Position)
1331
1420
  SpawnEffect(Effect: Object, Position: Position) -> Object
1332
1421
  SpawnEffectWithAutoDestroy(Effect: Object, Position: Position) -> Object
1333
1422
  SpawnEffectForDuration(Effect: Object, Position: Position, Duration: Number) -> Object
1423
+ SpawnDecal(Material: Object, Position: Position, RotationEuler: Vector, Size: Vector) -> Object // Projects onto geometry inside its oriented box along local -Z (down at rotation 0); Size z = projection depth cm. Bullet holes/stains/scorch marks. No engine pool: keep spawned decals in a List and Destroy the oldest past a few hundred
1424
+ SetScreenShader(Material: Object) // Fullscreen screen effect (film grain, color grade, distortion, damage flash): a #shader material applied after tonemap/AA; fragment reads ctx.color/ctx.depth. None clears. Animate via SetMaterialParameterNumber; see shader_dsl skill
1425
+ SetVolumetricLightIntensity(Percent: Number) // Sun shafts / god rays (raymarched vs the sun's shadows). Default: 0 (off); ~40 = subtle atmosphere, 100+ = dramatic alley/doorway shafts. Reads best with a low sun + occluding geometry
1426
+ SetVolumetricLightDensity(Percent: Number) // Haze thickness the shafts scatter in, percent of baseline. Default: 100; lower = crisper thinner shafts, higher = foggier
1427
+ Object.EnableLocalWeaponView(Offset: Position, RotationDegrees: Vector, Scale: Number, FovDegrees: Number) // EnableLocalWeaponView + own weapon FOV: FovDegrees 0 = follow camera; ~50-70 = classic FPS viewmodel that ignores ADS zoom and never clips walls
1428
+ SetSoundVolume(Sound: Object, Volume: Number) // Live per-asset volume (1 = as authored): retunes every instance already playing, loops included, and every later play; the per-play PlaySound volume multiplies on top. 0 mutes but keeps loops running - StopSound to end them
1429
+ FadeOutSound(Sound: Object, Seconds: Number) // Ramps this sound's playing instances to silence over Seconds, then stops them; queued plays drop immediately, plays started after the call are unaffected. Music/ambience handoffs - StopSound cuts instantly
1334
1430
  PlaySound(Sound: Object, Volume: Number) // Global (every client, full volume): UI/music. Positional per-player attenuation: PlaySoundAtPosition; stop an asset's instances: StopSound
1335
1431
  PlaySound(Sound: Object, Volume: Number, Pitch: Number)
1336
1432
  PlaySoundLooped(Sound: Object, Volume: Number) // Loops until StopSound(sound)/StopAllSounds; for BGM/engine hum (PlaySound stays one-shot)
1433
+ PlaySoundForPlayer(Player: Object, Sound: Object, Volume: Number) // Plays only on that player's client (global mix, no position): personal announcements/feedback the other players must not hear. Runtime error if Player is not a live player
1337
1434
  SetSoundAssetPriority(Sound: Object, Priority: Number)
1338
1435
  SetSoundAssetCooldown(Sound: Object, CooldownSeconds: Number)
1339
1436
  SetMaxSimultaneousSoundEffect(Max: Number)
1340
- GetRayHitPosition(Position: Position, Direction: Vector, Tag: Number) -> Position // Tag filters hits (no max-distance param). On miss, returns Position + Direction * 1000000.
1341
- GetRayHitObject(Position: Position, Direction: Vector, Tag: Number) -> Object
1342
- GetRayHitStaticPosition(Position: Position, Direction: Vector, Tag: Number) -> Position // Tag filters hits (no max-distance param). On miss, returns Position + Direction * 1000000.
1343
- GetRayHitStaticObject(Position: Position, Direction: Vector, Tag: Number) -> Object
1437
+ GetRayHitPosition(Position: Position, Direction: Vector, Tag: Number) -> Position // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. No max-distance param (range-gate on GetRayHitDistance); on miss returns Position + Direction * 1000000.
1438
+ GetRayHitObject(Position: Position, Direction: Vector, Tag: Number) -> Object // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. Returns null Object on miss (check .Exists()).
1439
+ GetRayHitStaticPosition(Position: Position, Direction: Vector, Tag: Number) -> Position // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. No max-distance param (range-gate on GetRayHitDistance); on miss returns Position + Direction * 1000000.
1440
+ GetRayHitStaticObject(Position: Position, Direction: Vector, Tag: Number) -> Object // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. Returns null Object on miss (check .Exists()).
1344
1441
  Object.SetCameraThisFrame(Position: Position, YawDegrees: Number, PitchDegrees: Number)
1345
1442
  Object.SetCameraLookAtThisFrame(Position: Position, LookAt: Position)
1346
1443
  Object.SetChaseCameraThisFrame(TargetPosition: Position, ForwardDirection: Vector, BackDistance: Number, Height: Number, LookAtHeight: Number) // Places camera at TargetPosition - normalize(ForwardDirection.xy)*BackDistance + Z*Height, looking at TargetPosition + Z*LookAtHeight
@@ -1363,18 +1460,7 @@ PersistGetNumber(Player: Object, Key: Text) -> Number
1363
1460
  PersistSetNumber(Player: Object, Key: Text, Value: Number)
1364
1461
  PersistAddNumber(Player: Object, Key: Text, Delta: Number)
1365
1462
  RefreshStaticBaseline() // Call once at the end of heavy static-scene init; non-moving objects then skip per-frame display CPU (a later manipulation opts that object out).
1366
- RestartGame()
1367
- AdvertiseLobbySession() // Advertise this relay as a joinable lobby session for the running project; not a gameplay replication/server-authority API.
1368
- StopAdvertisingLobbySession() // Stop advertising this relay lobby session.
1369
- ListLobbySessions() // Request discoverable lobby sessions of the running project (identity automatic, exact content version). Results update LobbySession* getters and fire OnLobbySessionListUpdated; state 2 with 0 sessions = nobody hosts this exact version.
1370
- JoinLobbySession(RelayPubkey: Text) // Join a discovered relay lobby session by LobbySessionRelayPubkey; deterministic shared sim still applies after joining.
1371
- LobbySessionCount() -> Number // Number of discovered relay lobby sessions from the latest ListLobbySessions result.
1372
- LobbySessionRelayPubkey(Index: Number) -> Text // Relay pubkey for a discovered lobby session; pass to JoinLobbySession.
1373
- LobbySessionConnectId(Index: Number) -> Text // Debug/connect id for a discovered relay lobby session; usually not needed for gameplay code.
1374
- LobbySessionDisplayName(Index: Number) -> Text // Human display name for a discovered relay lobby session.
1375
- LobbySessionRefreshState() -> Number // Latest list state: 0 idle, 1 pending, 2 ok, 3 error (read LobbySessionErrorText).
1376
- LobbySessionStage(Index: Number) -> Text // #stage name a discovered session is currently on (empty for stageless projects); show or filter it in the session browser.
1377
- LobbySessionErrorText() -> Text // Reason for state 3 (empty otherwise), e.g. relay unreachable or no loaded world.
1463
+ RestartGame() // Reloads the WHOLE project (fresh load flow; discards the user's unsaved manual editor edits). Hard New Game only - pair with PersistClear(). Round/match resets: reset positions/scores in code instead.
1378
1464
  AnyBasicType.NearestPlayerNonSpectator() -> Object
1379
1465
  SetSsrRoughnessFade(StartRoughness: Number, EndRoughness: Number) // Default: 0.68, 0.7
1380
1466
  SetSsrRayFade(NearFadePower: Number, FarFadePower: Number) // Default: 0.08, 0.8
@@ -1383,16 +1469,15 @@ SetSkyboxContrast(Contrast: Number) // Contrast multiplier. Default: 1 = neutral
1383
1469
  Hash2(X: Number, Y: Number, Seed: Number) -> Number // Deterministic [0,1): constant within each integer X/Y cell (see <procedural_generation>)
1384
1470
  ValueNoise2(X: Number, Y: Number, Seed: Number) -> Number // Deterministic smooth noise [0,1), X/Y in cell units
1385
1471
  Fbm2(X: Number, Y: Number, Octaves: Number, Seed: Number) -> Number // Deterministic fractal noise [0,1), Octaves 1..8
1386
- SpawnTerrainFromHeights(Heights: List, SamplesX: Number, SamplesY: Number, CellSize: Number, Position: Position) -> Object // Heights: row-major List of Numbers (cm, relative to Position.Z), len = SamplesX*SamplesY (2..513 each)
1387
- SpawnTerrainFromHeights(Name: Text, Heights: List, SamplesX: Number, SamplesY: Number, CellSize: Number, Position: Position) -> Object // Same + registers terrain asset under Name (error if name taken)
1472
+ SpawnTerrainFromHeights(Heights: List, SamplesX: Number, SamplesY: Number, CellSize: Number, Position: Position) -> Object // Heights: row-major List of Numbers (cm, relative to Position.Z), len = SamplesX*SamplesY (2..513 each). Terrain XY is CENTERED on Position
1473
+ SpawnTerrainFromHeights(Name: Text, Heights: List, SamplesX: Number, SamplesY: Number, CellSize: Number, Position: Position) -> Object // Same + registers terrain asset under Name (error if name taken or starting with 'terrain.' - reserved for uploaded assets)
1388
1474
  Object.SetTerrainMaterial(MaterialAsset: Object) // Terrain objects only; Material.X (#shader or #texture auto-material, triplanar works), None clears
1389
1475
  Object.SetTerrainLayers(Steep: Object, High: Object, Low: Object) // Terrain only; blends extra #texture materials over the base by slope/height: Steep on cliffs, High above HighStartZ, Low below LowStartZ. None skips a layer. Set base via SetTerrainMaterial
1390
1476
  Object.SetTerrainLayerParams(SlopeStartDeg: Number, SlopeEndDeg: Number, HighStartZ: Number, HighEndZ: Number, LowStartZ: Number, LowEndZ: Number) // Terrain only; tunes SetTerrainLayers blending. Slope in degrees (start..end ramp), heights in world Z cm. 0,0 for a pair = auto (slope 32..50, High top ~25%, Low bottom ~15% of height range)
1391
- CreateInstanceBatch(Model: Object, Origin: Position) -> Object // Display-only mass scatter batch (see <procedural_generation>); Model: Ori.model.* (basics like Ori.model.cone work) or Ori.alias.model.* - mesh models ONLY (effects/images/spawned Objects are rejected at runtime). Max 128 batches per world
1392
1477
  Object.AddInstance(Offset: Vector, RotationEuler: Vector, Size: Vector, ColorRGB: Vector) // Batch only. Offset cm from batch origin (within +-30000), RotationEuler deg, Size cm, ColorRGB 0..1
1393
1478
  Object.ClearInstances() // Batch only; removes all instances
1394
1479
  Object.GetInstanceCount() -> Number // Batch only
1395
- Object.SetBatchOrigin(Origin: Position) // Batch only; moves whole batch (offsets stay relative)
1480
+ Object.SetBatchOrigin(Origin: Position) // Batch only; moves whole batch (offsets stay relative). Counts as a batch edit (full re-upload) - not a per-frame motion lane
1396
1481
  Object.SetBatchCastShadow(Enabled: Bool) // Batch only; default true
1397
1482
  Object.SetBatchMaterial(Material: Object) // Batch only; Material.X (#shader/#texture auto-material) replaces all submesh materials of every instance, None reverts to the model's own
1398
1483
  Object.ShowSelectionRing(Player: Object, Radius: Number, Color: Vector) // RTS selected-unit ground circle on caller; visible only to Player; persists until Hide.
@@ -1401,7 +1486,7 @@ Object.ShowOutlineHighlight(Player: Object, ThicknessPixels: Number, Color: Vect
1401
1486
  Object.HideOutlineHighlight(Player: Object) // Removes this player's outline on caller (no-op if absent)
1402
1487
  Object.EnableLocalSelectionBox(BorderColor: Vector, FillColor: Vector, FillOpacity: Number, BorderWidthPixels: Number, MinDragPixels: Number) // PlayerObject only. Enables a visual-only local drag box for this player; gameplay selection should use Event.MouseDownPosition/Event.MousePosition
1403
1488
  Object.DisableLocalSelectionBox() // PlayerObject only. Disables the visual-only local drag box
1404
- Object.EnableLocalWeaponView(Offset: Position, RotationDegrees: Vector, Scale: Number) // PlayerObject only. Renders this player's equipped weapon as a local first-person camera-attached visual for the owning client (active only in first-person camera). Offset is camera-local cm (+X right, +Y up, +Z forward); RotationDegrees (0,0,0) = barrel forward; e.g. (Position(15, -25, 32), Vector(2, 4, 0), 1.0) for an FPS-style close lower-right framing (a close-in gun keeps the auto-arms shoulder area off-screen). Visual-only; gameplay still uses EquipWeapon/GetHeldWeapon.
1489
+ Object.EnableLocalWeaponView(Offset: Position, RotationDegrees: Vector, Scale: Number) // PlayerObject only. Renders this player's equipped weapon as a local first-person camera-attached visual for the owning client (active only in first-person camera). Offset is camera-local cm (+X right, +Y up, +Z forward); RotationDegrees is a first-person camera tilt, (0,0,0) = barrel forward (third person seats the weapon on the hand frame + grip anchor, not on this value); e.g. (Position(15, -25, 32), Vector(2, 4, 0), 1.0) for an FPS-style close lower-right framing (a close-in gun keeps the auto-arms shoulder area off-screen). Visual-only; gameplay still uses EquipWeapon/GetHeldWeapon.
1405
1490
  Object.DisableLocalWeaponView() // PlayerObject only. Disables the local first-person weapon visual
1406
1491
  Object.GetWeaponAnchorWorldDir(AnchorName: Text) -> Vector // Anchor local +X (forward) transformed to world space
1407
1492
  Object.GetWeaponAnchorWorldUp(AnchorName: Text) -> Vector // Anchor local +Z transformed to world space
@@ -1411,7 +1496,7 @@ TileGridCreate(TilesX: Number, TilesY: Number, DefaultWalkable: Bool, EdgeBlocki
1411
1496
  TileGridSetEdgeBlocked(Grid: List, X1: Number, Y1: Number, X2: Number, Y2: Number, Blocked: Bool)
1412
1497
  TileGridIsEdgeBlocked(Grid: List, X1: Number, Y1: Number, X2: Number, Y2: Number) -> Bool
1413
1498
  Object.GetCenterPosition() -> Position
1414
- Object.SetCenterPosition(Position: Position) // Center-anchored twin of SetPosition (same continuous moved semantics)
1499
+ Object.SetCenterPosition(Position: Position) // Center-anchored twin of SetPosition (same continuous moved semantics). The center resolves against the object's CURRENT rotation, so rotate first, then place (reversed order drifts rotated parts by half a box). On UI/canvas elements both calls place the element's TOP-LEFT (pivot 0,0); call SetUiPivot(1,1) once to center — GetCenterPosition mirrors the same convention
1415
1500
  Object.SetUiFollowsCursor(IsOn: Bool, OffsetX: Number, OffsetY: Number) // UI element on an owned screen canvas. Engine pins element+children at the owner's cursor + offset every frame, clamped on-screen, drawn latency-free. For tooltips/drag images, not clickable elements
1416
1501
  Object.SetRenderVisibleOnlyToPlayerIndex(PlayerIndex: Number) // World object render/pick visibility only for this player index; also sets owner metadata
1417
1502
  Object.SetRenderVisibleToAllPlayers() // Clears private render/pick visibility; does not clear owner metadata
@@ -1421,14 +1506,17 @@ Object.StopUpperBodyAnimation() // Base clip retakes the upper bones instantly;
1421
1506
  Object.SetCursorImage(ImageAsset: Object)
1422
1507
  Object.SetCursorImage(ImageAsset: Object, HotspotX: Number, HotspotY: Number, Opacity: Number)
1423
1508
  Object.SetWeaponAimCalibration(IsOn: Bool) // Default true: while OrientingWithCameraYaw, the upper body twists so the muzzle (in the weapon-hold pose) aligns with the camera/crosshair ray (aim offset); locomotion bob passes through. Turn off for reloads/cinematic poses where the gun shouldn't track the crosshair.
1509
+ Object.SetCursorAim(IsOn: Bool) // Cursor-aim games (top-down/twin-stick): the player's rendered body yaw tracks their live cursor between sim frames, hiding input latency on their own screen; the sim rotation your per-frame LookAt sets stays authoritative. Muzzle/body trails a fast-moving mouse -> turn this on.
1510
+ Object.SetCameraFixed(Position: Position, LookAt: Position) // Parks the camera at Position looking at LookAt until a camera-mode switch releases it: persistent across frames AND save/reload, camera collision off while fixed. Security cams, lobby shots, cinematics - no per-frame loop needed.
1424
1511
  SpawnAIPlayer(Name: Text) -> Object // Spawns a bot: a real PlayerObject ('#class player' code, physics, animations) driven by script instead of a user
1425
1512
  Object.IsAIPlayer() -> Bool // True only for players spawned via SpawnAIPlayer()
1513
+ Object.IsConnected() -> Bool
1426
1514
  Object.SetAIMoveVector(Direction: Vector) // Bot move input, like a held stick (persists until changed; xy, auto-clamped to length 1). Takes effect next frame
1427
1515
  Object.PressAIAction(Action: Number) // Bot action tap (Action.Jump etc). Down fires next frame, auto-releases the frame after
1428
1516
  Object.DespawnAIPlayer() // Removes a bot (errors on real players). Plain Destroy() also works on bots
1429
1517
  Object.SetTerrainGrass(Model: Object, Material: Object) // Terrain only; Model: Ori.model.* or Ori.model.mesh.* (#mesh blade), None = built-in grass (tuft cards; see SetTerrainGrassStyle); Material: Material.X (#texture auto-material) or None = built-in atlas (usually enough; colors come from the grass knobs). On tuft cards the material's texture replaces the built-in card atlas (2x2 variant cells, uv+clamp+cutout; greyscale luminance = root->tip gradient, color still comes from the grass color knobs). Per-terrain (each terrain can have its own grass style)
1430
1518
  Object.SetTerrainGrassWeightmap(Material: Object) // Terrain only; Material: #texture auto-material; R channel of its color = grass density 0..1 across the terrain; None clears (full density). Overrides #terrain_splat out.grass; prefer out.grass when density should follow the terrain (height/slope), since a #texture cannot see it
1431
- Object.SetWeaponConfig(WeaponModel: Object, IdleAnim: Object, ReloadAnim: Object, FireAnim: Object, ForegripAnchor: Text, GripAnchor: Text, MuzzleAnchor: Text, FireInterval: Number, ReloadDuration: Number) // Register per-weapon-model TPS config. IdleAnim auto-plays as upper-body loop on EquipWeapon (if no upper animation is active); anims accept InvalidId = off. Anchor names may be "" = built-in defaults (foregrip/grip). FireInterval/ReloadDuration are seconds, readable via GetWeaponFireInterval/GetWeaponReloadDuration for script fire loops.
1519
+ Object.SetWeaponConfig(WeaponModel: Object, IdleAnim: Object, ReloadAnim: Object, FireAnim: Object, ForegripAnchor: Text, GripAnchor: Text, MuzzleAnchor: Text, FireInterval: Number, ReloadDuration: Number) // Register per-weapon-model held-weapon config. IdleAnim auto-plays as upper-body loop on EquipWeapon (if no upper animation is active); anims accept InvalidId = off. Anchor names may be "" = built-in defaults (foregrip/grip). GripAnchor seats third-person AND the viewmodel; no foregrip anchor on the model = support hand dropped = one-handed hold. FireInterval/ReloadDuration are seconds, readable via GetWeaponFireInterval/GetWeaponReloadDuration for script fire loops.
1432
1520
  Object.GetWeaponFireInterval(WeaponModel: Object) -> Number // Seconds between shots from the weapon model's SetWeaponConfig (0 when unset)
1433
1521
  Object.GetWeaponReloadDuration(WeaponModel: Object) -> Number // Reload seconds from the weapon model's SetWeaponConfig (0 when unset)
1434
1522
  Object.SetEmitting(IsOn: Bool) // EffectObject only. false: stop spawning new particles (existing ones finish while still following the emitter); true: resume. Emitter stays alive.
@@ -1437,7 +1525,7 @@ Object.DestroyImmediate() // EffectObject only. Destroys the emitter and hard-cl
1437
1525
  Object.ShowSelectionRing(Player: Object, Radius: Number, Color: Vector, ZOffset: Number) // As 3-arg; ZOffset cm shifts ring vertically from feet (negative grounds a flying unit's ring)
1438
1526
  SetSkyboxCloudCoverage(Coverage: Number) // Procedural sky only (no SetSkybox asset). Default: 0.1; 0 = clear sky, 1 = overcast
1439
1527
  SetSkyboxHaze(Haze: Number) // Procedural sky only. Default: 0.5; 0.3 = crisp alpine air, 2-3 = hazy horizon
1440
- SetSkyboxStarIntensity(Intensity: Number) // Night star overlay brightness. Default: 1; 0 = no stars
1528
+ SetSkyboxStarIntensity(Intensity: Number) // Night star overlay brightness; visible only with the sun near/below the horizon or SetSkyboxIsNight(true). Default: 1; 0 = no stars
1441
1529
  SetSkyboxCloudDetail(Detail: Number) // Procedural sky only. Cloud fractal detail. Default: 1; 0 = soft blobs, 2 = max wispy detail
1442
1530
  SetSkyboxCloudScale(Scale: Number) // Procedural sky only. Cloud pattern size. Default: 1; 0.5 = big clouds, 2 = many small clouds
1443
1531
  SetSkyboxGroundColor(Color: Vector) // Procedural sky only. Ground-bounce light color lighting surfaces from below. Default: (0.34, 0.44, 0.32) dim green; brightness follows the sky
@@ -1452,7 +1540,7 @@ Object.SetTerrainSplat(SplatRules: Text) // Terrain only; SplatRules: #terrain_s
1452
1540
  SetWaterPlanarReflection(Strength: Number, Distortion: Number) // Mirror reflection of the scene on the GLOBAL water surface (SetWaterEnabled) only; placed water meshes always use skybox reflections. Strength 0 disables, Distortion scales wave wobble. Defaults: 0.7, 0.3
1453
1541
  Object.SetHoverHPBarMaterial(Material: Object) // Custom look for caller's hover HP bar; Material.X (#shader or #texture auto-material), None reverts to default. #shader gets ctx.fraction/ctx.background_color and draws the whole bar as one quad (textures don't squash). Persists across SetHoverHPBar updates; ClearHoverBars removes it
1454
1542
  Object.SetHoverSPBarMaterial(Material: Object) // Same as SetHoverHPBarMaterial for the hover SP bar
1455
- Object.SetHoverBarMaterial(BarIndex: Number, Material: Object) // Custom look for caller's SetHoverBar bar at BarIndex (0..28); Material.X or None reverts to default. See SetHoverHPBarMaterial for the #shader contract
1543
+ Object.SetHoverBarMaterial(BarIndex: Number, Material: Object) // Custom look for caller's SetHoverBar bar at BarIndex (0..=29); Material.X or None reverts to default. See SetHoverHPBarMaterial for the #shader contract
1456
1544
  SetDefaultHoverBarMaterial(Material: Object) // World default material for all hover bars (HP/SP/custom) without a per-object material; None reverts to built-in look
1457
1545
  StopSound(Sound: Object) // Stops all currently playing and queued instances of this sound asset (e.g. cut off a long music one-shot)
1458
1546
  StopAllSounds() // Stops every currently playing and queued sound
@@ -1534,12 +1622,14 @@ Object.GetTerrainHeightAt(X: Number, Y: Number) -> Number
1534
1622
  Text.Word(Index: Number) -> Text
1535
1623
  Text.WordCount() -> Number
1536
1624
  Text.ToNumber() -> Number
1625
+ Text.Length() -> Number // Character count (chars, not bytes - Thai etc. count per character).
1626
+ Text.Substring(Start: Number, Count: Number) -> Text // Chars [Start, Start+Count), 0-based, clamped to the text; negative Start or Count <= 0 -> empty Text.
1537
1627
  SetOcclusionCulling(Enabled: Bool) // Default: false (experimental Hi-Z occlusion culling)
1538
1628
  TerrainHeightsErosionFilter(Heights: List, SamplesX: Number, SamplesY: Number, CellCm: Number, ScaleCells: Number, Octaves: Number, StrengthCm: Number, Seed: Number, Mask: List, RidgeMapOut: List) // Stateless erosion filter (Phacelle gullies, no droplet sim): slope-aligned V-gullies + dendritic ridges; altitude fade keeps peaks sharp and valley floors intact. ScaleCells=largest gully size, StrengthCm=max carve/raise, Octaves 1..8, Mask=0..1 weights ([]=all), RidgeMapOut (pass [] List, auto-sized) 0..1: 0=crease 1=ridge (splat/tree/drainage rules). The go-to eroded-mountain look. Heavy: cost ~ sx*sy*Octaves (513 grid + 5 octaves = noticeable load time; prefer <=257 grids or <=4 octaves when loads must stay snappy)
1539
1629
  SetTerrainErosionDetailPercent(Percent: Number) // Global display-only knob: scales the GPU sub-cell erosion-gully detail on all terrain surfaces, and the displayed gully depth of #terrain recipe terrains (100 = default, clamped 0..300, 0 = smooth). Visual only — collision/heights unchanged; grass follows the displayed surface automatically
1540
1630
  TerrainHeightsOriErosion(Heights: List, SamplesX: Number, SamplesY: Number, CellCm: Number, BaseScaleCells: Number, ErosionScaleCells: Number, Octaves: Number, StrengthCm: Number, SlopeStrength: Number, BranchStrength: Number, Seed: Number, Mask: List, ErosionMapOut: List) // OriErosion directional erosion (the #terrain recipe engine as a bulk op; docs/ori_erosion_spec.md): cos-wave gullies steered by the base slope with per-octave branch feedback. BaseScaleCells = base-noise feature size (for slope direction recovery), ErosionScaleCells = first-octave gully size, SlopeStrength ~3, BranchStrength ~0.5, StrengthCm = carve depth. Mask scales accumulation ([] = full); note masked zones sink ~0.5*StrengthCm (water datum). ErosionMapOut (pass [] List, auto-sized) = hx*0.5+0.5 for splat user-plane coloring
1541
1631
  SetTerrainDisplayExtension(RadiusM: Number) // Display-only: renders procedural terrain BEYOND the sim grid out to RadiusM (0 = off), no collision. The generator params come from the world's #terrain block (required; errors without one) — build the sim grid with TerrainHeightsFromTerrain from the same block and the extension continues it seamlessly. Pair with SetCameraFarDistanceM to actually see it
1542
- SetCameraFarDistanceM(Meters: Number) // Display-only camera far plane in meters; 0 = engine default 1000, else clamped 1000..8000. Distant terrain needs SetFogEndDistance raised too; shadows/light clustering keep near-field tuning (distant geometry is unshadowed)
1632
+ SetCameraFarDistanceM(Meters: Number) // Display-only camera far plane in meters; 0 = engine default 1000, else clamped 1000..8000. Distant terrain needs SetFogEndDistance raised too; light clustering keeps near-field tuning; static geometry keeps sun shadows to ~2km (cached far layer; desktop)
1543
1633
  Object.SpawnDamageNumber(Amount: Number) // Damage number above caller: pop-arc motion, world-scaled, size grows with Amount; rounds to integer. Call from WasAttacked with Event.Damage. For custom look use the 5-arg SpawnFloatingText overload
1544
1634
  Object.SpawnFloatingText(Text: Text, Color: Vector, SizeScale: Number, Motion: Number, Anchor: Number) // Styled overload: SizeScale multiplies text size; Motion 0=RiseFade 1=PopArc 2=Sticky 3=BounceLand; Anchor 0=constant screen size 1=scales with camera distance
1545
1635
  SetDefaultFloatingTextFont(Font: Object) // World default font for SpawnFloatingText/SpawnDamageNumber (e.g. Ori.font.google.Lobster); None reverts to built-in
@@ -1551,15 +1641,15 @@ Object.SetTerrainGrassSpawnMaterial(Material: Object) // Terrain only; Material:
1551
1641
  Object.SetTerrainSurfaces(SurfacesName: Text) // Terrain only; SurfacesName: #terrain_surfaces block name (up to 8 tiled PBR surfaces packed into texture arrays; lines `surface = <albedo>, <normal>, <tiling_m>[, <orm>[, <grass01>]]` - optional orm alpha carries height for crisp height-blended transitions; optional grass01 = grass density factor on that surface (default 1 for the first surface, 0 for the rest, so grass skips rock/snow); pair with SetTerrainSurfaceWeightsMaterial for per-texel weights). "" clears
1552
1642
  Object.SetTerrainSurfaceWeightsMaterial(Material: Object) // Terrain only; Material: material with a #shader - a CUSTOM surface-weights COMPUTE shader writing 8 blend weights per terrain texel (recipe terrains; covers the display extension via the surface clipmap). None clears
1553
1643
  GetWaterHeightAt(X: Number, Y: Number) -> Number // Deterministic ocean surface height (cm) at world XY this frame - the wave swell all clients agree on (visual-only fine detail excluded). Use for floating/buoyancy. Returns sea level when the ocean is off or flat
1554
- GetWaterNormalAt(X: Number, Y: Number) -> Vector // Deterministic ocean surface unit normal (Z-up) at world XY this frame - pair with GetWaterHeightAt to tilt floating objects (e.g. LookAlong(GetWaterNormalAt(P.X, P.Y)) style alignment). Returns (0,0,1) when the ocean is off or flat
1555
- Object.SetMakesWaterWake(Enabled: Bool) // Opt this object into churning the global ocean: wake ripples + foam while moving through the surface band, and a splash on crossing the surface. Visual only. Tag boats/swimmers/thrown objects; players splash automatically without it. Default: off
1644
+ GetWaterNormalAt(X: Number, Y: Number) -> Vector // Deterministic ocean surface unit normal (Z-up) at world XY this frame - pair with GetWaterHeightAt to tilt floating objects (e.g. AlignRotationWith(GetWaterNormalAt(P.X, P.Y)) style alignment). Returns (0,0,1) when the ocean is off or flat
1645
+ Object.SetMakesWaterWake(Enabled: Bool) // Opt this object into churning the global ocean: wake ripples + foam while moving through the surface band, and a splash on crossing the surface. Visual only. Tag boats/swimmers/thrown objects; players splash automatically without it. Wake width scales with the hull's horizontal size (floor ~0.6 m); rings spread outward like real water. Default: off
1556
1646
  Object.GetCrosshairRayOrigin() -> Position // Camera eye point the crosshair ray starts from (= this player's camera position; pair with GetCrosshairDirection for the camera pose). Use this (not GetPosition().ShiftUp(N), which sits below the eye and lands low) as the origin for hand-rolled crosshair rays/markers.
1557
1647
  SetWaterSplashEffect(Effect: Object) // Replace the built-in ocean surface-crossing splash particle effect world-wide (all wake/splash objects and players). Pass an Effect asset (e.g. Ori.alias.effect.big_splash). Visual only; the built-in droplet burst is used until this is called
1558
1648
  Object.SetCharacterOverlapIgnoreTag(Tag: Number, IsOn: Bool) // This character's overlap resolution skips objects carrying Tag (walk through them; ground/other collision unaffected, Touched/regions still fire). Call with IsOn=false to restore. Per-tag, stacks
1559
1649
  Object.SetDefaultPlayerControlsEnabled(IsOn: Bool) // Weave2 only, player receiver. true: attach the built-in default player controls (WASD camera-relative move + jump) alongside this class — the one-liner for '#class player' hooks that don't code their own movement. false: detach them (freeze for cutscenes; classless-world players can be frozen from any class). Idempotent
1560
1650
  Object.AttachTo(Parent: Object) // Rigidly ride Parent from the CURRENT relative pose (re-baked from Parent's post-physics transform every tick; overrides this object's own moves). Child must be collision-free: display/effect/light/marker objects, or SetCanCollide(false) first. Chains allowed (arm -> gun); cycles error. Replaces per-frame SetPosition follow loops. Order-sensitive in OnSpawned (parent may not be placed yet) - prefer the 3-arg overload there
1561
1651
  Object.AttachTo(Parent: Object, LocalPosition: Position, LocalRotation: Vector) // Explicit-mount overload: LocalPosition (absolute cm) and LocalRotation (euler degrees) in Parent's local frame; safe in OnSpawned. The frame runs CENTER-to-CENTER (not the bottom-center pivot GetPosition/#mesh use): seat a child by adding half ITS height to the offset. Offsets don't rescale when Parent's size changes - re-attach after growth
1562
- Object.AttachToAnchor(Parent: Object, AnchorName: Text) // AttachTo at a named model anchor of Parent's mesh (e.g. "muzzle", "grip"); errors when the anchor doesn't exist (anchors resolve after asset import). The anchor's axis is its local +Z but object/effect forward is local +X - directional effects on an anchor need a LocalRotation (usually RotY +-90). Anchor offsets don't follow Parent scale. A `bone:`-bound socket anchor makes the child visually ride that bone's animation (sim pose stays rest)
1652
+ Object.AttachToAnchor(Parent: Object, AnchorName: Text) // AttachTo at a named model anchor of Parent's mesh (e.g. "muzzle", "grip"); errors when the anchor doesn't exist (anchors resolve after asset import). The anchor's axis is its local +Z but object/effect forward is local +X - directional effects on an anchor need a LocalRotation (usually RotY +-90). The mount point follows the parent's scale at attach time (where the socket visually sits); captured offsets don't re-follow LATER parent rescales. A `bone:`-bound socket anchor makes the child visually ride that bone's animation (sim pose stays rest)
1563
1653
  Object.Detach() // Remove this object's attachment; it keeps its current world transform
1564
1654
  Object.GetAttachParent() -> Object // Attachment parent, or None-like non-existing Object when not attached (check with Exists())
1565
1655
  EaseIn(T: Number) -> Number
@@ -1579,11 +1669,11 @@ Vector.RotateByEuler(Yaw: Number, Pitch: Number, Roll: Number) -> Vector // Rota
1579
1669
  Vector.ToYawPitch() -> Vector // Treat this vector as a direction: returns Vector(yaw, pitch, 0) in degrees. Yaw 0 = +X (counter-clockwise toward +Y), pitch positive = up. Zero-length input gives (0, 0, 0)
1580
1670
  YawPitchToDirection(Yaw: Number, Pitch: Number) -> Vector // Unit direction from aim angles in degrees (inverse of ToYawPitch): yaw 0 = +X, pitch positive = up
1581
1671
  SetDefaultFont(Font: Object) // World default font for all UI text/buttons that don't set one (e.g. Ori.font.google.Fredoka); None reverts to the engine default (Nunito)
1582
- SetModelAnchor(Model: Object, AnchorName: Text, LocalPosition: Position, LocalRotationEulerDegrees: Vector) // Weapon sits wrong in the hand / anchor misplaced -> define or replace any named anchor ("grip", "muzzle", custom): model-root local, cm Z-up, rotation maps anchor axes (+X = forward) into model axes; wins over the asset's own anchors, call before or after EquipWeapon
1672
+ SetModelAnchor(Model: Object, AnchorName: Text, LocalPosition: Position, LocalRotationEulerDegrees: Vector) // Weapon sits wrong in the hand / anchor misplaced -> define or replace any named anchor ("grip", "muzzle", custom): model-root local, cm Z-up, rotation maps anchor axes (+X = forward) into model axes; wins over the asset's own anchors, call before or after EquipWeapon; a grip anchor seats BOTH the third-person hold and the EnableLocalWeaponView viewmodel
1583
1673
  SwitchStage(Name: Text) // `#stage` projects only: switch to another stage of this project (multiplayer-safe; all stages ship in the project source, so Name may be dynamic). Use LoadWorldTransferPush before switching to carry values across.
1584
- Object.SetRenderOffset(OffsetCm: Vector, RotationEulerDegrees: Vector, Scale: Number) // Visual-only offset on top of the sim transform (bob/lean/sway); colliders/AI/queries unaffected. OffsetCm shifts in WORLD space (not rotated into the object's local frame); rotation composes after the object's own, Scale multiplies visuals. Call per-frame for procedural animation; ClearRenderOffset resets
1674
+ Object.SetRenderOffset(OffsetCm: Vector, RotationEulerDegrees: Vector, Scale: Number) // World/display/player objects only (crowds/instance batches reject). Visual-only offset on top of the sim transform (bob/lean/sway); colliders/AI/queries unaffected. OffsetCm shifts in WORLD space (not rotated into the object's local frame); rotation composes after the object's own and pivots at the object CENTER (SetRotation pivots at origin = ref-box bottom-center). Call per-frame for procedural animation; ClearRenderOffset resets
1585
1675
  Object.ClearRenderOffset() // Remove this object's visual render offset
1586
- Object.RaycastObject(Origin: Position, Direction: Vector, MaxDistance: Number) -> Object // Nearest object the ray hits within MaxDistance (this object excluded), or a None-like Object on miss (check Exists()). Hits anything solid incl. untagged walls/floor/terrain - the go-to for projectile impact and line-of-sight tests. Direction need not be normalized
1676
+ Object.RaycastObject(Origin: Position, Direction: Vector, MaxDistance: Number) -> Object // Nearest object the ray hits within MaxDistance (this object excluded), or a None-like Object on miss (check Exists()). Hits anything solid incl. untagged walls/floor/terrain - the go-to for projectile impact and line-of-sight tests. Direction need not be normalized; fast projectiles: raycast the segment travelled this frame (origin = previous position, MaxDistance = the step) - positional overlap checks skip thin targets between frames
1587
1677
  Object.RaycastObjectWithTag(Origin: Position, Direction: Vector, MaxDistance: Number, Tag: Number) -> Object // RaycastObject limited to objects carrying Tag
1588
1678
  Object.RaycastObjectIgnoreTag(Origin: Position, Direction: Vector, MaxDistance: Number, IgnoreTag: Number) -> Object // RaycastObject that skips objects carrying IgnoreTag (e.g. ignore pickups/projectiles)
1589
1679
  Object.RaycastPosition(Origin: Position, Direction: Vector, MaxDistance: Number) -> Position // Impact point of RaycastObject's ray; on miss returns Origin + Direction.GetNormal() * MaxDistance
@@ -1595,11 +1685,11 @@ TileGridHasLineOfSight(Grid: List, StartX: Number, StartY: Number, StartZ: Numbe
1595
1685
  TileGridSmoothPath(Grid: List, Path: List) -> List
1596
1686
  Object.SetTerrainGrassVisible(Visible: Bool) // Terrain only; false = no grass on this terrain (deserts/cities/moon), true = grass even while the terrain object is hidden, and counts as grass intent (enables the auto ground tint = full meadow look). Unset default: sparse untinted grass following terrain visibility. Same as declarative grass_visible
1597
1687
  SpawnEffectAimed(Effect: Object, Position: Position, Direction: Vector) -> Object // Spawns with the effect's authored up/emission axis aimed along Direction (e.g. exhaust: SpawnEffectAimed(fx, GetPosition(), -GetFacingDirection())). Zero direction = unrotated.
1598
- Object.SetEmitterLightsEnabled(Enabled: Bool)
1688
+ Object.SetEmitterLightsEnabled(Enabled: Bool) // Toggle the lights this object's mesh bakes via emitter(...) sockets (default on) - e.g. lamps dark at day
1599
1689
  Object.GetEmitterLightsEnabled() -> Bool
1600
1690
  SetFogSkyboxAffect(Percent: Number) // Default: 0 (sky ignores distance fog). 100 = distant sky fully fogged, ~30 = horizon haze
1601
1691
  TileGridSetWalkableFromStaticColliders(Grid: List, OriginTile: Vector, ZMin: Number, ZMax: Number)
1602
- Object.SetCursorUiMode(On: Bool)
1692
+ Object.SetCursorUiMode(On: Bool) // true: release mouse-look AND show the cursor so players can click UI (menus/shops); false: restore gameplay look. Prefer over toggling SetIsMouseLookOn/SetMouseVisible separately.
1603
1693
  Object.SetBoneRotation(BoneName: Text, EulerDegrees: Vector) // Replace the named bone(...) rig joint's local rotation (Euler degrees Z-up, #clip convention); wins over the playing clip for that bone until cleared. Max 16 bones per object. Display-only: colliders/physics unaffected. Call per-frame in forever{} for tails, breathing, head tracking
1604
1694
  Object.ClearBoneOverrides() // Remove all of this object's bone rotation overrides (SetBoneRotation, Mix, and BoneLookAt)
1605
1695
  Object.SetBoneRotationMix(BoneName: Text, EulerDegrees: Vector, Weight01: Number, BlendSeconds: Number, Additive01: Number) // Weighted, smoothed bone layer over the playing clip: Weight01 blends toward EulerDegrees, BlendSeconds eases changes, Additive01=1 offsets the clip pose instead of replacing it. On a rig with no clip playing it animates from rest - the fully procedural mode. Max 16 bones per object; display-only
@@ -1611,6 +1701,73 @@ AnyBasicType.NearestObjectXYWithTagNotOnTeam(Tag: Number, TeamId: Number, MaxDis
1611
1701
  AnyBasicType.NearestObjectWithTagOnTeam(Tag: Number, TeamId: Number, MaxDistance: Number) -> Object
1612
1702
  AnyBasicType.NearestObjectXYWithTagOnTeam(Tag: Number, TeamId: Number, MaxDistance: Number) -> Object
1613
1703
  AnyBasicType.GetObjectsWithinDistanceWithTagNotOnTeam(Distance: Number, Tag: Number, TeamId: Number) -> List // Tag-carriers with team != TeamId within Distance cm (AoE damage lists); for single-target picks prefer NearestObjectWithTagNotOnTeam
1704
+ GetRayHitPosition(Position: Position, Direction: Vector) -> Position // Unfiltered: first hit of anything (dynamic + static). No max-distance param (range-gate on GetRayHitDistance); on miss returns Position + Direction * 1000000.
1705
+ GetRayHitObject(Position: Position, Direction: Vector) -> Object // Unfiltered: first hit object. Returns null Object on miss (check .Exists()).
1706
+ GetRayHitStaticPosition(Position: Position, Direction: Vector) -> Position // Unfiltered: first STATIC hit (terrain, walls). No max-distance param (range-gate on GetRayHitDistance); on miss returns Position + Direction * 1000000.
1707
+ GetRayHitStaticObject(Position: Position, Direction: Vector) -> Object // Unfiltered: first static hit object. Returns null Object on miss (check .Exists()).
1708
+ Object.GetAnchorWorldPos(AnchorName: Text) -> Position // World position of a socket/anchor on THIS object's own mesh (declare socket("Name", ...) in its #mesh), scaled with the object — unlike AttachToAnchor offsets, this follows object scale. Bone-bound sockets resolve the rest pose. Errors when the anchor doesn't exist.
1709
+ Object.GetAnchorWorldDir(AnchorName: Text) -> Vector // Anchor local +X (forward) rotated to world space; unit vector, unaffected by object scale. The socket axis: parameter is the anchor's local +Z (see GetAnchorWorldUp).
1710
+ Object.GetAnchorWorldUp(AnchorName: Text) -> Vector // Anchor local +Z (the socket's declared axis:) rotated to world space; unit vector, unaffected by object scale.
1711
+ IsWorldReady() -> Bool // true once the current load's scene OnSpawned batch has fully started (see event OnWorldReady); false again from the start of a new load/restart. Poll it before placing things on procedurally built ground/rooms.
1712
+ SetShadowDistance(DistanceMeters: Number) // Crisp sun-shadow range in meters; past it only static geometry casts (coarse cached shadows to ~2km, desktop). Default: 50 (character scale); building-onto-building / city blocks need 200-300. Cost: shadow edges get chunkier as range grows
1713
+ SetDebugRenderMode(Mode: Number) // Diagnostic view: renders ONE lighting/material component ungraded instead of the shaded frame, to tell wrong albedo from wrong light from wrong geometry. 0 off, 1 albedo, 2 normal, 3 material ao, 4 ssao, 5 ao*ssao (what ambient is multiplied by - 0 means no ambient can reach the surface), 6 direct sun, 7 ambient/sky, 8 shadow mask, 9 roughness, 10 metallic, 11 emissive, 12 local lights. Ship worlds with 0; the playtest/live_screenshot `debug_view` parameter is the per-capture equivalent. Default: 0
1714
+ PersistClear()
1715
+ Object.SetInstance(Index: Number, Offset: Vector, RotationEuler: Vector, Size: Vector, ColorRGB: Vector) // Batch only. Overwrites the instance at Index (0..GetInstanceCount-1); args like AddInstance. Errors on out-of-range Index
1716
+ Object.RemoveInstance(Index: Number) // Batch only. Swap-remove: the LAST instance moves into Index, count shrinks by 1. Errors on out-of-range Index
1717
+ SetSkyboxColor(Color: Vector) // Procedural sky only. Base color multiplied into the sky bake before grading (alien/stylized skies); SetSkyboxTint post-multiplies the graded display instead. Default: (1, 1, 1)
1718
+ GetRayHitDistance(Position: Position, Direction: Vector, Tag: Number) -> Number // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. Distance in cm to the first hit, -1 on miss — range gates and damage falloff.
1719
+ GetRayHitDistance(Position: Position, Direction: Vector) -> Number // Unfiltered. Distance in cm to the first hit, -1 on miss — range gates and damage falloff; prefer this over measuring to GetRayHitPosition, whose miss sentinel is Position + Direction * 1000000.
1720
+ SetSkyboxCloudType(Type: Number) // Procedural sky only. Cloud vertical form. Default: 0.35; 0 = flat haze layer, 1 = towering cumulus
1721
+ CreateStaticInstanceBatch(Model: Object, Origin: Position) -> Object // Display-only STATIC scatter batch (see <procedural_generation>); Model: Ori.model.* (basics like Ori.model.cone work) or Ori.alias.model.* - mesh models ONLY (effects/images/spawned Objects are rejected at runtime). Max 128 live per world - Destroy() frees. Never mutate per frame - each edit (SetBatchOrigin included) re-uploads the whole batch. Sway/shimmer: vertex #shader on the model's material; instances that truly move: objects or #particles
1722
+ GetRayHitPart(Position: Position, Direction: Vector, Tag: Number) -> Text // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. Names the hit volume posed by the target's current animation: "head"/"torso"/"pelvis"/"upper_arm_l"/"forearm_r"/"thigh_l"/"shin_r"/...; "" on miss, rigless target, or a hit outside every volume (treat as a plain body hit) — gate damage on GetRayHitObject, use the part for multipliers.
1723
+ GetRayHitPart(Position: Position, Direction: Vector) -> Text // Unfiltered twin: same part names and "" semantics as GetRayHitPart.
1724
+ TileGridFlowFieldBuild(Grid: List, GoalTiles: List) -> List
1725
+ Object.StartTileFlowFollow(Field: List, Grid: List, OriginTile: Vector, Speed: Number) -> Bool
1726
+ TilePathFollowTakeBlocked() -> List
1727
+ Object.SetSubmeshTint(SubmeshIndex: Number, TintRGB: Vector) // THIS object's per-submesh tint (faction colors): same LOD-flattened slot space as SetAssetSubmeshTint; multiplies SetColor and the asset tint (black kills). Tint (1,1,1) clears the slot. Skinned models fully supported; tinted statics leave the LOD-collapse fast path; InstanceBatch/TileMap handles reject
1728
+ SetFogOfWarEnabled(Enabled: Bool) // RTS fog of war: per-TEAM vision grid sized from the largest terrain (runtime error without one). Each viewer sees own-team vision; unexplored = black, explored-but-unseen = grey memory (objects hidden), visible = clear. Radius discs only (no line-of-sight blocking); audio is not gated. Enable once at world start; false clears everything. No default sight: until Object.SetSightRadius lands on TEAMED objects, teamed viewers see a near-black stage - grant sight in the same change as the enable
1729
+ Object.SetSightRadius(RadiusCm: Number) // THIS object provides fog vision to ITS team (GetTeam) in a RadiusCm disc, refreshed ~every 8 ticks while alive; 0 unregisters. Teamless objects provide no vision. Give it to units/buildings after SetFogOfWarEnabled
1730
+ IsPositionVisibleToTeam(Position: Position, TeamId: Number) -> Bool // True when the position is currently in TeamId's live vision (explored-only memory returns false). Unset/no-team viewers see everything; a team that never held vision sees nothing. Fog disabled = always true. For AI/scripting; rendering hides automatically
1731
+ GetTerrainBoundsMin() -> Position // World-space min corner of the LARGEST terrain (z = lowest height sample). Errors when no terrain exists. Fog and the minimap snapshot use the same terrain.
1732
+ GetTerrainBoundsMax() -> Position // World-space max corner of the LARGEST terrain (z = highest height sample). Minimap math: uv = (P - Min) / (Max - Min).
1733
+ GetRayHitNormal(Position: Position, Direction: Vector, Tag: Number) -> Vector // Tag filters to objects WITH that tag; 0 is a real tag index, NOT "no filter" — use the 2-arg overload for unfiltered rays. Unit surface normal at the hit (world axes; box = face normal, sphere = radial); zero Vector on miss.
1734
+ GetRayHitNormal(Position: Position, Direction: Vector) -> Vector // Unfiltered twin: same normal and zero-Vector miss semantics as GetRayHitNormal.
1735
+ Object.SetSavedToWorld(IsOn: Bool) // Script-spawned objects are TRANSIENT: saving the world never writes them into the scene text (your Spawn code recreates them). SetSavedToWorld(true) makes this one save like an editor-placed object; editor-placed objects always save
1736
+ ArcToTarget(From: Position, To: Position, FlightSeconds: Number, GravityCmS2: Number) -> List // Arc handle leaving From and passing through To exactly at FlightSeconds under GravityCmS2 (positive cm/s^2, e.g. 980; captured in the handle - world SetGravity is NOT used). Query with ArcPositionAt/ArcVelocityAt/ArcFlightSeconds.
1737
+ ArcToTargetWithPeak(From: Position, To: Position, PeakHeightCm: Number, GravityCmS2: Number) -> List // As ArcToTarget but sized by apex height above From (the 'make it float higher' knob); errors when PeakHeightCm can't reach the target height. Read the flight time back with ArcFlightSeconds.
1738
+ ArcFlightSeconds(Arc: List) -> Number // Seconds from launch until the arc reaches its target - the contact time to hand to MoveToArriveIn / PlayAnimationHittingContactIn.
1739
+ ArcPositionAt(Arc: List, Seconds: Number) -> Position // Closed-form arc sample at Seconds. Drive the ball each frame with SetPosition(ArcPositionAt(arc, t)) - no drift, no lerp seams between phases. Past the flight time it keeps extrapolating downward; stop driving it yourself.
1740
+ ArcVelocityAt(Arc: List, Seconds: Number) -> Vector // Arc velocity at Seconds: face the object along travel, or reflect it on a block/bounce.
1741
+ GetAnimationLeadSeconds(AnimationAsset: Object, ContactPercent: Number) -> Number // Seconds from clip start to ContactPercent at normal playback - how early PlayAnimationHittingContactIn can fire. Accounts for SetAnimationAssetSpeedFactor; never derive this from GetAnimationDuration by hand.
1742
+ Object.PlayAnimationHittingContactIn(Animation: Object, ContactPercent: Number, SecondsUntilContact: Number) -> Bool // Call every frame with the remaining seconds: false while too early, then plays Animation rate-scaled (0.5x..2x, skipping into the clip when already late) so ContactPercent lands exactly SecondsUntilContact from now, and true from then on. Will not restart a clip it already started, so no 'started' guard var - use a dedicated action clip, not one that may already be playing.
1743
+ Object.MoveToArriveIn(Target: Position, SecondsRemaining: Number) // One frame of movement sized to arrive at Target in SecondsRemaining - recompute the remainder each frame like the other continuous movers (a constant argument eases out and never lands; 0 or negative arrives this frame). Auto-move animations see the speed, so characters run instead of sliding. On gravity characters Z is discarded (as with MoveTowardContinuous).
1744
+ Object.MoveToArriveInFacing(Target: Position, SecondsRemaining: Number) // MoveToArriveIn, turning the object's forward (+X) along the movement direction.
1745
+ Object.AttachToAnchor(Parent: Object, ParentAnchor: Text, ChildAnchor: Text) // Mounts the CHILD's named socket onto the parent's socket (position + orientation) - the part pivots at its socket instead of its center
1746
+ GetAllObjectsWithTagOnTeam(Tag: Number, TeamId: Number) -> List // GetAllObjectsWithTag filtered to TeamId (SetTeam / scene team =; unset objects never match). Same allocate-per-call advice
1747
+ AdvertiseSession() // List this session in the project's public session browser (hosts can also toggle this in the engine session panel; last write wins). Not a replication/server-authority API.
1748
+ StopAdvertisingSession() // Unlist this session (it stays joinable via share link).
1749
+ ListSessions() // Request the project's listed sessions (identity automatic, exact content version). Results update Session* getters and fire OnSessionListUpdated; state 2 with 0 sessions = nobody hosts this exact version.
1750
+ JoinSession(RelayPubkey: Text) // Join a listed session by SessionRelayPubkey; deterministic shared sim still applies after joining.
1751
+ SessionCount() -> Number // Number of listed sessions from the latest ListSessions result.
1752
+ SessionRelayPubkey(Index: Number) -> Text // Relay pubkey for a listed session; pass to JoinSession.
1753
+ SessionConnectId(Index: Number) -> Text // Debug/connect id for a listed session; usually not needed for gameplay code.
1754
+ SessionDisplayName(Index: Number) -> Text // Host display name for a listed session.
1755
+ SessionRefreshState() -> Number // Latest list state: 0 idle, 1 pending, 2 ok, 3 error (read SessionErrorText).
1756
+ SessionStage(Index: Number) -> Text // #stage name a listed session is currently on (empty for stageless projects); show or filter it in a browser.
1757
+ SessionErrorText() -> Text // Reason for state 3 (empty otherwise), e.g. relay unreachable or no loaded world.
1758
+ SessionPlayerCount(Index: Number) -> Number // Connected player count for a listed session (0 = unknown).
1759
+ OpenSessionBrowser() // Open the engine's built-in session browser UI (no-op in headless/validation).
1760
+ Object.ShatterAndDestroy() // Destroy() plus a debris burst from the model's baked chunk set (needs `#mesh Name destructible: <word>`; without it, plain Destroy()). Debris is display-only - no sim collision.
1761
+ TerrainSlopeAtXY(X: Number, Y: Number) -> Number // slope01 (g/(1+g); 0.5 = 45deg) of topmost terrain at world XY; 1 if no terrain (void = unwalkable). Walkers wedge above ~0.5
1762
+ TileGridSetWalkableFromTerrain(Grid: List, OriginTile: Vector, MaxSlope01: Number)
1763
+ TileGridSetEdgesFromTerrain(Grid: List, OriginTile: Vector, MaxStepCm: Number)
1764
+ TileGridStampRect(Grid: List, X1: Number, Y1: Number, X2: Number, Y2: Number, Blocked: Bool)
1765
+ TileGridStampObject(Grid: List, OriginTile: Vector, Obj: Object, Blocked: Bool)
1766
+ TileGridIsReachable(Grid: List, X1: Number, Y1: Number, X2: Number, Y2: Number) -> Bool
1767
+ TileGridIsReachable(Grid: List, X1: Number, Y1: Number, Z1: Number, X2: Number, Y2: Number, Z2: Number) -> Bool
1768
+ TileGridBakeCrowdObstacles(Grid: List, OriginTile: Vector)
1769
+ TerrainHeightsFromTiers(Heights: List, SamplesX: Number, SamplesY: Number, Tiers: Text, TierCm: Number) // REPLACE Heights from an ASCII tier map (digits = tier * TierCm, '.' = 0, 'r' = ramp; first row = max Y). See <terrain_generation>
1770
+ RaiseWorldError(Message: Text) // Fail-loud assert: raises a Weave runtime error carrying Message (validation surfaces it; in play the program pauses like any script error)
1614
1771
 
1615
1772
  </built_in_function_list>
1616
1773
 
@@ -1619,7 +1776,7 @@ AnyBasicType.GetObjectsWithinDistanceWithTagNotOnTeam(Distance: Number, Tag: Num
1619
1776
  - X/Y are in cell units: scale world coords down (e.g. Fbm2(pos.X / 800, pos.Y / 800, 4, 7)); features repeat at integer-cell scale. Use a different Seed per layer (trees vs rocks).
1620
1777
  - SpawnTerrainFromHeights spawns a collidable terrain heightfield from code; heights are row-major (iy * SamplesX + ix), in cm relative to Position.Z. Terrain XY is centered on Position.
1621
1778
  - terrain.GetTerrainHeightAt(X, Y) returns the world-space surface Z at world (X, Y) (bilinear); errors if the caller has no heightfield or (X, Y) is outside it.
1622
- - Terrain grass: every terrain grows sparse built-in grass automatically; grass_visible = false / SetTerrainGrassVisible(false) removes it entirely (deserts, cities). The ground under the blades is the terrain's own splat/material — style it with `#terrain_splat` rules or terrain layers. Set from code via terrain.SetTerrainGrass / terrain.SetTerrainGrassWeightmap / terrain.SetTerrainGrassHeightCm / terrain.SetTerrainGrassStyle / terrain.SetTerrainGrassVisible (all per-terrain), or declaratively on terrain blocks (grass_model = "mesh.X", grass_material = "material.X", grass_weightmap = "image.X" or a #texture color, grass_height_cm = 28, grass_visible = true/false, grass_style = "blades"). No model = built-in grass: cheap alpha-cut tuft cards by default, or geometry blades via SetTerrainGrassStyle(1) (heavier, best with SetGrassSpacingCm 8..15 for dense hero fields). Custom #mesh blade budget: tiny tuft (<=100 verts), single material slot, bottom-anchored at Z=0; blade size comes from authoring the mesh at the right cm size, then grass_height_cm rescales it (28 = as-authored). Global tuning calls: SetGrassSpacingCm, SetGrassTerrainActiveDistanceM, SetGrassCullDistanceM, SetGrassFadeDistancePercent, SetGrassFarThinningPower; look calls: SetGrassRootColor, SetGrassTipColor, SetGrassBrightnessVariationPercent, SetGrassSizeVariationPercent, SetGrassHeightMinCm, SetGrassHeightMaxCm, SetGrassBladeWidthPercent, SetGrassWavePeriodCm, SetGrassWindStrengthCm, SetGrassWindSpeedPercent.
1779
+ - Terrain grass: every terrain grows sparse built-in grass automatically; grass_visible = false / terrain.SetTerrainGrassVisible(false) removes it entirely (deserts, cities). The ground under the blades is the terrain's own splat/material — style it with `#terrain_splat` rules or terrain layers. Set from code via terrain.SetTerrainGrass / terrain.SetTerrainGrassWeightmap / terrain.SetTerrainGrassHeightCm / terrain.SetTerrainGrassStyle / terrain.SetTerrainGrassVisible (all per-terrain), or declaratively on terrain blocks (grass_model = "mesh.X", grass_material = "material.X", grass_weightmap = "image.X" or a #texture color, grass_height_cm = 28, grass_visible = true/false, grass_style = "blades"). No model = built-in grass: cheap alpha-cut tuft cards by default, or geometry blades via SetTerrainGrassStyle(1) (heavier, best with SetGrassSpacingCm 8..15 for dense hero fields). Custom #mesh blade budget: tiny tuft (<=100 verts), single material slot, bottom-anchored at Z=0; blade size comes from authoring the mesh at the right cm size, then grass_height_cm rescales it (28 = as-authored). Global tuning calls: SetGrassSpacingCm, SetGrassTerrainActiveDistanceM, SetGrassCullDistanceM, SetGrassFadeDistancePercent, SetGrassFarThinningPower; look calls: SetGrassRootColor, SetGrassTipColor, SetGrassBrightnessVariationPercent, SetGrassSizeVariationPercent, SetGrassHeightMinCm, SetGrassHeightMaxCm, SetGrassBladeWidthPercent, SetGrassWavePeriodCm, SetGrassWindStrengthCm, SetGrassWindSpeedPercent.
1623
1780
  - The same noise is available in #shader WGSL: ori_hash2(x, y, seed), ori_value_noise2(x, y, seed), ori_fbm2(x, y, octaves, seed) (f32; seed/octaves u32/i32), ori_hash2_cell(cx, cy, seed) -> u32. Matches the Weave functions exactly at integer lattice points, so sim placement and shader visuals can share one layout.
1624
1781
  Example (hills + deterministic tree scatter):
1625
1782
  #class WorldGen
@@ -1653,6 +1810,8 @@ event OnSpawned {
1653
1810
  - Flat gameplay areas: TerrainHeightsFlattenDisc/Rect/Path AFTER noise/erosion so nothing roughens them again. ThermalErode + RemapCurve make raw noise look natural (erode first, then remap for plateaus/cliffs).
1654
1811
  - Prefer 129..257 samples per axis (513 max; big lists inflate sim state — generate in OnSpawned local vars so they are GC'd after spawn).
1655
1812
  - GroundHeightAtXY(x, y) -> world Z cm of the topmost ground at world XY (downward raycast over terrain + static colliders; 0 if nothing). Use for placing props/villages on generated ground; works immediately after SpawnTerrainFromHeights in the same event.
1813
+ - TerrainSlopeAtXY(x, y) -> slope01 of the topmost terrain at world XY (0.5 = 45deg; no terrain = 1, treat void as unwalkable). Slopes steeper than ~0.5 silently immobilize walkers - keep unit routes below it or block those tiles/edges in the nav grid (see <tile_pathfinding>).
1814
+ - TerrainHeightsFromTiers(Heights, SamplesX, SamplesY, Tiers, TierCm) REPLACES the field with plateau heights from an ASCII tier map in a triple-quoted Text (digits 0-9 = tier, h = tier * TierCm; '.' = 0; 'r' = ramp graded toward its lower neighbor; rows = lines, FIRST row = MAX Y / north). Hand-drawn RTS cliff maps: one char per nav tile, then TileGridSetEdgesFromTerrain(MaxStepCm = TierCm/3) derives the cliff edges; compose Smooth/Erode/splat after.
1656
1815
  - Realism combo: Warp after base noise (kills grid-straight lines), then ErosionFilter (fast stateless gullies/ridges; run AFTER RemapCurve so the detail lands at final scale, and its look continues sub-cell on the GPU display surface). Avoid HydraulicErode/ThermalErode by default — slow iterative CPU sims; reach for HydraulicErode only when routed drainage or its flow/deposit masks are truly needed. MaskFromFlow gives a wetness mask WITHOUT eroding.
1657
1816
  - Terrain-following rivers: FlowPath traces downhill from a spring cell into a CarvePath-ready point list (stops at sea level = MinHeightCm). Check PathXYOut has >= 4 numbers (2 points) before carving.
1658
1817
  - Roads that climb: GradePath lerps the flatten height start->end along the path (use terrain heights at the endpoints); FlattenPath only does constant height.
@@ -1717,20 +1876,20 @@ Two terrain display types:
1717
1876
  </terrain_generation>
1718
1877
 
1719
1878
  <procedural_generation_batches>
1720
- - InstanceBatch: thousands of decorative copies of one model as ONE display object (petals, pebbles, grass tufts). Far cheaper than SpawnObject; instances have no physics/logic/picking and can't be edited individually (only ClearInstances + re-add). Use SpawnObject for anything interactive.
1879
+ - InstanceBatch: thousands of decorative copies of one model as ONE display object (pebbles, rubble, grass tufts). Instances have no physics/logic/picking and are invisible to the editor - never clickable, movable, or saved there; the layout lives in this code (edit by index via SetInstance/RemoveInstance, or ClearInstances + re-add). Identical static meshes already render as one instanced draw either way - a batch saves per-object sim/load cost (spawn, rollback state, join checkpoints), not draw calls. Copies a human should hand-place or that need any interactivity: SpawnObject. Drifting petals/leaves/debris: #particles mesh components, not a batch. Animated look on scatter (sway/shimmer): vertex #shader on the model's material - never per-frame batch edits (SetBatchOrigin included), and batch handles reject Object.* calls like SetRenderOffset.
1880
+ - Batches outlive their creator; only Destroy() frees a slot (128 live max). One batch per model kind, edited in place - don't create per region or per round.
1721
1881
  - Model can be a basic primitive (Ori.model.cube/cone/wedge/...); add SetBatchMaterial(Material.X) for textured scatter without any market asset.
1722
- Example (3000 petals):
1723
- var petals = CreateInstanceBatch(Ori.alias.model.petal, Origin())
1724
- for i in 0..3000 {
1725
- petals.AddInstance(Vector((Hash2(i, 0, 5) - 0.5) * 4000, (Hash2(i, 1, 5) - 0.5) * 4000, 150),
1726
- Vector(0, 0, Hash2(i, 2, 5) * 360), Vector(14, 14, 2), Vector(0.95, 0.45, 0.78))
1882
+ Example (2000 pebbles):
1883
+ var pebbles = CreateStaticInstanceBatch(Ori.alias.model.pebble, Origin())
1884
+ for i in 0..2000 {
1885
+ var s = 6 + Hash2(i, 3, 5) * 14
1886
+ pebbles.AddInstance(Vector((Hash2(i, 0, 5) - 0.5) * 4000, (Hash2(i, 1, 5) - 0.5) * 4000, 0),
1887
+ Vector(0, 0, Hash2(i, 2, 5) * 360), Vector(s, s, s * 0.7), Vector(0.62, 0.6, 0.55))
1727
1888
  }
1728
1889
  </procedural_generation_batches>
1729
1890
 
1730
1891
  <ternary>
1731
- - Use If(cond, a, b) for ternary (No ?: operator)
1732
- - If(cond, a, b) -> T
1733
- - Only the taken branch is evaluated
1892
+ - No ?: operator - use If(cond, a, b) -> T; only the taken branch is evaluated
1734
1893
  </ternary>
1735
1894
 
1736
1895
  <events>
@@ -1745,6 +1904,8 @@ for i in 0..3000 {
1745
1904
  - All OnPreSpawn finish for the frame before the first OnSpawned runs
1746
1905
  - Ordering between objects' OnPreSpawn is unspecified
1747
1906
  - GameCommand fires on every class that declares it when a user sends a command text message (headless build orders or chat-driven commands). Event.Text is the raw command line, Event.Sender the sender's player object. Parse Text yourself (e.g. "build house 3 4").
1907
+ - OnWorldReady fires once per load (again after RestartGame) when every scene object's OnSpawned has STARTED (run to its first yield), spilled load batches included. Poll IsWorldReady() for spawn/placement code that depends on procedurally built ground or rooms (terrain, BuildRoom) instead of Wait() timers; a handler that Wait()s mid-body may still be mid-flight at ready.
1908
+ - OnPlayerLeft fires on the leaver's player object the moment IsConnected() flips false (the body stays ~90s for reconnect; OnDestroy only at grace expiry): clear SetHoverText labels / roster rows here. OnPlayerReconnected fires when the SAME body resumes within the grace; a post-grace rejoin is a fresh spawn (OnSpawned).
1748
1909
  </events>
1749
1910
  <event_list>
1750
1911
  event OnPreSpawn
@@ -1753,7 +1914,7 @@ event OnDestroy
1753
1914
  event WasAttacked
1754
1915
  Event.Attacker .. Object
1755
1916
  Event.Damage .. Number
1756
- event OnLobbySessionListUpdated
1917
+ event OnSessionListUpdated
1757
1918
  event GameCommand
1758
1919
  Event.Text .. Text
1759
1920
  Event.Sender .. Object
@@ -1775,8 +1936,10 @@ event ClickThisObject
1775
1936
  Event.Presser .. Object
1776
1937
  event ClickThisUI
1777
1938
  Event.Presser .. Object
1939
+ Event.ClickFraction .. Vector
1778
1940
  event ClickThisButton
1779
1941
  Event.Presser .. Object
1942
+ Event.ClickFraction .. Vector
1780
1943
  event ClickButton
1781
1944
  Event.ButtonClicker .. Object
1782
1945
  Event.ClickedButton .. Object
@@ -1882,6 +2045,9 @@ event InventoryDropOnWorld
1882
2045
  Event.SlotIndex .. Number
1883
2046
  Event.DropObject .. Object
1884
2047
  Event.DropPosition .. Position
2048
+ event OnWorldReady
2049
+ event OnPlayerLeft
2050
+ event OnPlayerReconnected
1885
2051
 
1886
2052
  </event_list>
1887
2053
 
@@ -1905,7 +2071,7 @@ event InventoryDropOnWorld
1905
2071
  </3. Physics step & motion application>
1906
2072
  <Single-frame overwrite rule>
1907
2073
  - See <movement_laws>: one continuous translation + one rotation per tick, last call wins.
1908
- - Jump() and gravity own a character's vertical velocity: MoveWithVelocityContinuously on a gravity-affected character never writes Z at all (the component is discarded), so scripted vertical motion there needs SetCharacterGravity(false) first. On DYNAMIC bodies a same-tick SetPhysicsVelocity built from a velocity read taken BEFORE a JumpInDirection writes the old Z back and the jump never lifts - re-read velocity after the jump, or keep script movement XY-only and let physics own Z.
2074
+ - Jump() and gravity own a character's vertical velocity: MoveWithVelocityContinuously on a gravity-affected character never writes Z at all (the component is discarded), so scripted vertical motion there needs EnableCharacterGravity(false) first. On DYNAMIC bodies a same-tick SetPhysicsVelocity built from a velocity read taken BEFORE a JumpInDirection writes the old Z back and the jump never lifts - re-read velocity after the jump, or keep script movement XY-only and let physics own Z.
1909
2075
  </Single-frame overwrite rule>
1910
2076
  <Readback semantics>
1911
2077
  - Within the same tick, GetPosition()/GetRotation() return the pre-physics values. Updated transforms become visible to script on the next tick.
@@ -1913,6 +2079,7 @@ event InventoryDropOnWorld
1913
2079
  <Notes>
1914
2080
  - OnSpawned ordering across different objects is unspecified.
1915
2081
  - Per-object forever/Update brains are the right default. Past a few hundred always-active objects the per-object dispatch itself dominates the frame - switch to one manager forever loop calling into a kept List of members (u.Tick() per member; ~8x cheaper at 600 objects), and for per-tick targeting call NearestObjectWithTagNotOnTeam(Tag, MyTeam, Range) instead of looping a GetAllObjectsWithTag list per object; if you must scan a list, rescan on a ~0.25s staggered cooldown, not every tick.
2082
+ - Wait()/forever parks a per-object saved continuation (every handler local + call scratch) against one global 262,144-slot budget - hundreds of live instances of one class exhaust it ('continuation slot capacity exceeded' freeze). Swarm classes (pickups, projectiles): use event Update + class vars (a returning handler parks nothing).
1916
2083
  - 1000+ active units: regular colliding objects cap near ~500 (collision pairs). Spawn hordes as ClimbingCrowdObject (lean deterministic crowd bodies; steer with SetAsClimbingCrowdTarget) - or SetCanCollide(false) units picking targets via NearestObjectWithTagNotOnTeam on a staggered ~0.25s cooldown.
1917
2084
  - Dense pickup/particle fields (hundreds+): every spawned object is replicated multiplayer state, so thousands of individual pickups bloat rollback and read as network lag even at healthy ping. Pool + recycle a capped set (destroy/respawn churn also hitches), keep pickups non-colliding (SetCanCollide(false) - solid ones physically shove players; 33+ colliders stacked at one point also overflow crowd steering), collect via distance checks, and bundle several visuals per synced object when you want thousands on screen.
1918
2085
  - See also: <movement_laws> and <time> sections for API-level constraints and examples.
@@ -1921,21 +2088,23 @@ event InventoryDropOnWorld
1921
2088
 
1922
2089
  <ui>
1923
2090
  - Use ClickButton event to detect any button click. Event properties: Event.ButtonClicker, Event.ClickedButton
1924
- - event ClickThisUI fires when any UI element of this class (frame/image/label/button) is clicked; buttons additionally fire ClickThisButton/ClickButton
2091
+ - event ClickThisUI fires when any UI element of this class (frame/image/label/button) is clicked; buttons additionally fire ClickThisButton/ClickButton. Both carry Event.ClickFraction: the 0..1 click position inside the element (x right, y down) — minimaps, sliders, pickers
1925
2092
  - Drag-drop: mark elements draggable=true (or SetUiDraggable(true)). Engine handles press+move threshold, drag ghost and click suppression. UiDragStart fires on drag start (Event.Dragger, Event.DragElement); UiDrop on release (Event.Dragger, Event.DragElement, Event.DropElement = UI element under cursor or None, Event.DropObject = world object hit when dropped outside UI, Event.DropPosition). Both broadcast to every class defining the handler (player class or the element's class both work); ClickThisUI instead fires only on the clicked element's own class. A short press without movement still clicks normally.
1926
2093
  - Tooltip/cursor-follow UI lagging behind the mouse: never chase the cursor with per-frame offset loops - call SetUiFollowsCursor(true, dx, dy) once (owned screen canvas); the engine glues it to the real cursor. Scripts only refresh its text.
1927
2094
  - SetCursorImage(ImageAsset) turns the OS hardware cursor into that image (zero-lag, unlike moving a UI element each frame); SetCursorImage(NONE) restores the stock cursor. Overload SetCursorImage(ImageAsset, HotspotX, HotspotY, Opacity): hotspot is 0..1 of the image ((0,0)=top-left default, (0.5,0.5)=image centered on the pointer - ideal drag ghost), opacity 0..1 scales the image alpha. Don't animate opacity per frame (each step builds a new OS cursor). The image is downscaled to <=64px; touch devices have no cursor, so mobile drag ghosts still need a UI element. Pattern: SetCursorImage(ghost, 0.5, 0.5, 0.8) in UiDragStart, SetCursorImage(NONE) in UiDrop.
1928
2095
  - Hit-testing ignores plain images/labels (clicks pass through), BUT any element whose class has script or with draggable=true becomes clickable and blocks hit tests under it. Keep overlay visuals (tooltips, custom drag ghosts) class-free and move them from another class's code, or they will swallow the hover/click you are trying to detect.
1929
2096
  - Inventories/hotbars/chests: see <inventory> — item clauses + BindInventory give a fully engine-synced slot UI.
2097
+ - Minimap: an image element with image = "ori.image.engine.minimap" shows an auto-refreshing top-down snapshot of the largest terrain, spanning its XY bounds (terrain only — no objects/fog). Blips/camera rect = child frames placed via GetTerrainBoundsMin/Max; map clicks back to world XY with Event.ClickFraction.
1930
2098
  - ui object tags for world description: screenCanvas/worldCanvas/frame/textLabel/button; elements nest
1931
2099
  - Element handles in script: scene-declared elements resolve as Scene.X; children of a SpawnCanvas canvas via canvas.FindChildUI("Name")
2100
+ - Spawning saved UI at runtime: canvas-root saved block -> SpawnCanvas(SavedObject.X, pos); element-root block (frame/textLabel/button...) -> SpawnChildUI(SavedObject.X, ParentUI). SpawnObject rejects both.
1932
2101
  - worldCanvas = screenCanvas anchored to a world position (position = (x, y, z)): children use the same base-unit offsets around the projected anchor and keep constant on-screen size (nameplates, above-object markers); draws under screenCanvas; move the canvas object to follow things
1933
2102
 
1934
2103
  - UI offset and size uses base unit assuming 1600x900 window size. Resize -> ui scales proportionally. Make layouts work in 1600x900 base unit.
1935
2104
  - text align uses words: align=(horizontal, vertical) where horizontal=left|center|right, vertical=top|center|bottom. Defaults: button text is already CENTER, textLabel is top-left; only set align to override (e.g. align=(center, center) to center a label). In script use SetTextAlign(h, v) with 0/1/2 = left/center/right, top/center/bottom.
1936
2105
  - Text off-center in a panel: put the textLabel INSIDE the frame with NO size (a size-less child of a plain frame inherits the frame's full rect) + align=(center, center). Never center by hand-tuned offset - text width is not measurable, guessed offsets are always off.
1937
2106
  - Fonts: SetFont(Ori.font.google.<Family_Name>) uses any Google Font (fonts.google.com) directly, no upload; underscores for spaces, optional weight/style suffix (e.g. Ori.font.google.Roboto_Mono, Ori.font.google.Lobster, Ori.font.google.Roboto_Mono.700italic). Uploaded font assets use SetFont(Ori.font.<creator>.<name>). UI text without a font uses the world default: SetDefaultFont(Ori.font...) sets it, default Nunito.
1938
- - Declarative fonts: textLabel/button clauses accept font_size = N (base units, default 16) and font = "font.google.<Family_Name>" / "font.<creator>.<name>" — prefer these over SetFontSize/SetFont calls in OnSpawned. Inside a saved block font_size works but font urls do not (set custom fonts from script after Spawn). Text never auto-shrinks to fit its rect - match font_size to the rect.
2107
+ - Declarative fonts: textLabel/button clauses accept font_size = N (base units, default 16) and font = "font.google.<Family_Name>" / "font.<creator>.<name>" — prefer these over SetFontSize/SetFont calls in OnSpawned. Text never auto-shrinks to fit its rect - match font_size to the rect.
1939
2108
 
1940
2109
  - PREFER auto-layout containers over manual offsets: vStack (stack down) / hStack (stack across) / grid (set columns). They self-arrange children, so you don't hand-place each element.
1941
2110
  - HUG (recommended default): a vStack/hStack with NO `size` auto-sizes to fit its children + padding + gaps. Just author the children; the container grows to wrap them.
@@ -1946,12 +2115,16 @@ event InventoryDropOnWorld
1946
2115
  - padding/gap/margin take N (all sides) or (x, y) with x=left/right, y=top/bottom; per-side: padding_left/right/top/bottom, margin_left/right/top/bottom.
1947
2116
  - Word values: child_align = left|center|right (x) or top|center|bottom (y); align_self_x/y = auto|start|center|end|stretch (per-child override of child_align; stretch fills the cross axis).
1948
2117
  - frame = plain/absolute container (no auto-layout). Use it for screen-anchored HUD corners and for elements you reposition at runtime via SetUiOffset. A frame/grid without `size` inherits the parent's size. Position a child by ONE of: offset; anchor; insets.
1949
- - anchor = (x, y): pin the element to a spot on the parent (x=left|center|right, y=top|center|bottom). It also sets the pivot to match, so anchor=(center,center) centers, anchor=(center,bottom) makes a bottom bar, anchor=(right,bottom) a corner. `offset` still nudges. Decouple with pivot=(x,y) (or anchor_x/y + pivot_x/y) when the element's own pivot must differ from the parent attach point.
2118
+ - anchor = (x, y): pin the element to a spot on the parent (x=left|center|right, y=top|center|bottom). It also sets the pivot to match, so anchor=(center,center) centers, anchor=(center,bottom) makes a bottom bar, anchor=(right,bottom) a corner. `offset` still nudges, always in raw screen units (+x right, +y down) regardless of anchor - from a bottom anchor, inward is negative y. Decouple with pivot=(x,y) (or anchor_x/y + pivot_x/y) when the element's own pivot must differ from the parent attach point.
1950
2119
  - inset = N | (x, y): pin all four edges to the parent content rect so the child fills parent - inset (per-side: inset_left/inset_right/inset_top/inset_bottom). Use it to inset content inside a button/frame (e.g. an icon: inset=10). insets differ from margin (a child's outer spacing inside a stack/grid) and padding (a container's inner inset applied to ALL its children); a button is not a container, so inset the child rather than padding the button.
1951
- - clip_to_bounds = true clips children (and hit-testing) to this rect; block_input = true blocks clicks passing through.
2120
+ - cover = screen (frame): rect = the player's FULL real window instead of the 1600x900 base box - backdrops/vignettes reach wide-monitor edges (unflagged UI lays out in the centered base box, leaving side bands there). Children anchor inside it normally, so a HUD wrapped in a cover frame hugs true screen corners. Owned screen canvases only.
2121
+ - visible = false starts any element hidden (there is no `hidden` field); SetVisible(true) reveals it - same flag.
2122
+ - clip_to_bounds = true clips children (and hit-testing) to this rect; block_input = true blocks clicks passing through. Containers (frame/vStack/hStack/grid) default block_input = TRUE - set block_input = false on overlay panels that must not swallow clicks on the world behind them (other elements default false).
2123
+ - Interactable elements (buttons, classed elements, draggables) consume clicks before the world: no click-through move/shoot/ClickThisObject under them, so no guard code needed. block_input additionally swallows clicks on non-interactable containers; a decorative classed element that must NOT eat world clicks sets click_through = true.
1952
2124
  - Scrollable container: set clip_to_bounds=true + block_input=true on the container, then SetUiScrollOffset(X, Y) shifts its content (children move by -offset; rect/clip stay). Read back with GetUiScrollOffsetX/Y(). Wire event OnMouseScroll { } (Event.ScrollDelta, positive = wheel up) to adjust Y. Wheel over input-blocking UI does not zoom the camera. Authored start offset: scroll_offset = (x, y).
1953
- - Rounded corners + border work on frame/button/progressBar (ignored by textLabel/image): corner_radius = N (base units, all 4 corners), border_width = N (base units; border shows only when > 0), border_color = #RRGGBB | #RRGGBBAA | (r,g,b).
1954
- - count = N (2..=256, canvas child elements): parse-time repeat — the element and its whole subtree expand into N copies named <Name>0..<Name>N-1 (e.g. `frame Slot { count = 9 ... }` -> Slot0..Slot8, each resolvable by name). Use for marker/gauge/slot pools instead of hand-numbering; copies stack until code positions them via SetUiOffset. Counts nest (rows x cells).
2125
+ - Rounded corners + border work on frame/button/progressBar (textLabel: neither; image: corner_radius only): corner_radius = N (base units, all 4 corners), border_width = N (base units; border shows only when > 0), border_color = #RRGGBB | #RRGGBBAA | (r,g,b). Authoring-only: no SetUi* simcall changes these at runtime.
2126
+ - progressBar: color = fill, background_color = track; fill ratio via SetProgress(Value, MaxValue).
2127
+ - count = N (2..=256, canvas child elements): parse-time repeat — the element and its whole subtree expand into N copies named <Name>0..<Name>N-1 (e.g. `frame Slot { count = 9 ... }` -> Slot0..Slot8, each resolvable by name). Use for marker/gauge/slot pools instead of hand-numbering; copies stack until code positions them via SetUiOffset; resolve copies dynamically via Panel.FindChildUI("Slot" + i). Counts nest (rows x cells).
1955
2128
  - image nine_slice = (uv_frac, border): scalable panel art — the outer uv_frac (0..0.5) of the texture becomes a fixed border-unit rim (corners keep size, edges/center stretch). Use for stretchy frames/panels from one texture.
1956
2129
  - frame/vStack/hStack/grid also accept image = "image.*" as a background fill (drawn over color/border, under children; nine_slice applies). Combine with material= freely.
1957
2130
  - text/button shadows are off by default; set shadow_color = #000000B4 (or any alpha > 0) to enable.
@@ -1960,10 +2133,11 @@ event InventoryDropOnWorld
1960
2133
  - Colors accept optional 8-digit hex alpha (e.g. #3A3A3AFF) in addition to #RRGGBB and (r,g,b).
1961
2134
  - Two-stop gradients: gradient(colorA, colorB[, angle_deg]) (linear; angle 0 = top->bottom, 90 = left->right) or radial(inner, outer) (center->corners). ONLY on frame/vStack/hStack/grid `color` (fill) and on `border_color` of frame-like UI — NOT on button/textLabel colors (wrap the button in a gradient frame instead).
1962
2135
 
1963
- - Example (hugging vStack panel; rounded corners + subtle border):
2136
+ - Give visible UI a light theme pass by default: one Google font, one accent color on primary buttons/highlights, corner_radius on panels/buttons, panel colors from the game's palette (not pure black) — a few properties total.
2137
+ - Example (hugging vStack panel; themed: font + accent + rounded corners + subtle border):
1964
2138
  vStack Panel { offset=(40,40) padding=12 gap=8 color=#1E1E1EF0 corner_radius=12 border_width=1 border_color=#3A3A3AFF
1965
- textLabel Title { size=(200,28) text="Shop" }
1966
- button Buy { size=(200,44) text="Buy" corner_radius=8 }
2139
+ textLabel Title { size=(200,28) text="Shop" font="font.google.Cinzel" font_size=22 }
2140
+ button Buy { size=(200,44) text="Buy" corner_radius=8 color=#7A4FBF }
1967
2141
  }
1968
2142
  - Example (gradient card fill + gradient border):
1969
2143
  frame Card { size=(320,180) color=gradient(#1A2A6C, #B21F1F, 45) corner_radius=16 border_width=2 border_color=radial(#FFFFFF80, #FFFFFF10) }
@@ -2019,6 +2193,8 @@ event InventoryDropOnWorld {
2019
2193
  - use the function PlayAnimationOnRepeat to loop animations like run forever
2020
2194
  - PlayAnimationOnRepeat(Ori.anim.default.goblin.run)
2021
2195
  - PlayAnimation(Ori.anim.default.goblin.attack)
2196
+ - a marketplace model's own clips: Ori.anim.<creator>.<model>.<clip> (e.g. Ori.anim.vk7t7mestuxl_5378.murlocfishgi.attack); clip names are exact-case
2197
+ - Timing a clip to a moment (hand/bat/racket meeting a ball): call PlayAnimationHittingContactIn(Anim, ContactPercent, SecondsUntilContact) every frame - it returns false and does nothing while still too early, then fires rate-scaled so ContactPercent lands exactly on contact, and refuses to restart a clip it already started (no 'started' guard var needed). GetAnimationLeadSeconds tells how early it can fire. Never derive a trigger delay from GetAnimationDuration by hand.
2022
2198
 
2023
2199
  - Rigged store characters can also play canonical clips (world `#clip Name on canon` blocks -> Ori.anim.canon.Name) and other humanoid rigs' clips (e.g. a template mesh's Ori.anim.mesh.<Mesh>.walk) — the engine retargets by bone-name map, display-only
2024
2200
 
@@ -2026,8 +2202,8 @@ event InventoryDropOnWorld {
2026
2202
  - the clip is picked automatically: .idle when still, .run when moving (falling back to .walk), .jumpHover when airborne; camera-locked players also use .runBack/.runLeft/.runRight by movement direction
2027
2203
  - using PlayAnimation will stop these animations until the animation is finished
2028
2204
  - DisableAutoMoveAnimations() to turn this off
2029
- - Rigged humanoid store characters whose model ships NO clips idle/walk with the built-in canonical clips automatically (display-only retarget); models with their own clips always use them
2030
- - Custom/procedural character: author ONE rigged #mesh (bone(...) joints + idle/walk #clips, standard bone names — see the humanoid example in the mesh skill) and SetModel it; auto move-animations then drive it. AttachTo assembly is for props/machines (turret, arm -> gun), not walking bodies
2205
+ - Rigged humanoid models (store characters or DSL rigs) that ship no `.idle` clip idle/walk/run (jumpHover airborne) with the built-in canonical clips automatically (display-only retarget); models with their own clips always use them
2206
+ - Custom/procedural character: author ONE rigged #mesh (bone(...) joints + optional idle/walk #clips, standard bone names — see the humanoid example in the mesh skill) and SetModel it; auto move-animations then drive it. AttachTo assembly is for props/machines (turret, arm -> gun), not walking bodies
2031
2207
  - Procedural bone layer on any bone(...) rig (display-only, max 16 bones): SetBoneRotation(Mix) replaces/blends a joint over the playing clip, BoneLookAt aims one at a world point, PlayUpperBodyAnimation layers a clip over the base walk. Head-track + breathe over auto-walk:
2032
2208
  forever {
2033
2209
  BoneLookAt("Head", Friend.GetCenterPosition(), 55, 240)
@@ -2035,13 +2211,2547 @@ event InventoryDropOnWorld {
2035
2211
  }
2036
2212
 
2037
2213
  - Die() auto-spawns a visual-only corpse+animation; non-player deaths dissolve away during the die animation and despawn the object
2038
- - Die() on a PLAYER (human or AI bot) is a warp-only death: corpse at the death spot, body warps to Origin(), class vars/cooldowns SURVIVE, OnSpawned does not re-run, the player is never despawned (attacks landing the same frame are dropped). Respawn pattern: in WasAttacked at HP <= 0 call Die() and start a respawn Timer; when it finishes, restore HP and TeleportTo(your team spawn). Removing a bot needs DespawnAIPlayer()/Destroy(); ResetLogic() is the explicit re-run-OnSpawned-with-fresh-vars reset
2214
+ - Die() on a PLAYER (human or AI bot) is a warp-only death: corpse at the death spot, body warps to Origin(), class vars/cooldowns SURVIVE, OnSpawned does not re-run, the player is never despawned (attacks landing the same frame are dropped). Respawn pattern: in WasAttacked at HP <= 0 call Die() and start a respawn Timer; when it finishes, restore HP and TeleportTo(your team spawn). Removing a bot needs DespawnAIPlayer()/Destroy(). No engine call re-runs OnSpawned or resets class vars - put per-life re-init in one func called from both OnSpawned and your respawn point
2039
2215
  </animations>
2040
2216
 
2041
2217
 
2042
2218
  <world_description_examples>
2043
2219
  These examples are trimmed-down teaching games: copy their patterns, not their scope — a real request usually deserves more content and polish than any example here.
2044
2220
  Examples by genre: stardew_valley (farming/life-sim), vampire_survivor (top-down action/swarm), breakout (2D arcade/side-view), dota (top-down hero/lane).
2221
+ <example_breakout>
2222
+ // Breakout (Fixed-Bounds Top-Down)
2223
+ // Multiplayer: shared board.
2224
+ // One shared brick field and one shared ball; each player gets one paddle in the same arena.
2225
+ // Do not duplicate the whole map unless intentionally making parallel solo boards.
2226
+ alias model.ball // bare: auto-binds a generated mesh from the library
2227
+
2228
+ #class BoardManager
2229
+ var TS = 50
2230
+ var W = 12
2231
+ var H = 16
2232
+ event OnSpawned {
2233
+ SetVisible(false)
2234
+
2235
+ // Spawn one shared ball for the whole match.
2236
+ var ball = SpawnObject(SavedObject.ball, Position(0, (-H*TS*0.5) + TS*2.6, 0))
2237
+ ball.BoundCenterX = 0
2238
+ ball.BoundHalfX = W*TS*0.5 - TS*0.3
2239
+ ball.BoundTopY = H*TS*0.5 - TS*0.4
2240
+ ball.BoundBottomY = -H*TS*0.5
2241
+
2242
+ // Build one shared brick field. Destroyed bricks disappear for everyone.
2243
+ for r in 0..4 {
2244
+ for c in 0..10 {
2245
+ var x = (c - 5) * TS * 0.9
2246
+ var y = (r * TS * 0.7) + TS * 3.5
2247
+ var b = SpawnObject(SavedObject.brick, Position(x, y, 0))
2248
+ b.SetColor(0.6, 0.3 + r*0.1, 0.9 - c*0.05)
2249
+ }
2250
+ }
2251
+ }
2252
+ #end
2253
+
2254
+ #class player
2255
+ var TS = 50
2256
+ var W = 12
2257
+ var H = 16
2258
+ var pad = 0.6
2259
+ var PaddleObj: Object
2260
+ event OnSpawned {
2261
+ SetPlayerModeSpectator()
2262
+ SetVisible(false)
2263
+
2264
+ var minX = (-W*TS*0.5 - pad*TS)
2265
+ var maxX = ( W*TS*0.5 + pad*TS)
2266
+ var minY = (-H*TS*0.5 - pad*TS)
2267
+ var maxY = ( H*TS*0.5 + pad*TS)
2268
+ SetCameraToTopDownSpectatorFixedBoundsXY(minX, maxX, minY, maxY)
2269
+ SetCameraCollisionEnabled(false)
2270
+
2271
+ // Each player adds a paddle to the same shared board.
2272
+ // Compact arcade layout: first few players get nearby bottom lanes.
2273
+ var lane = GetPlayerIndex()
2274
+ if lane > 5 { lane = 5 }
2275
+ var laneY = (-H*TS*0.5) + TS*(1.0 + lane*0.8)
2276
+ PaddleObj = SpawnObject(SavedObject.paddle, Position(0, laneY, 0))
2277
+ PaddleObj.SetPlayerOwnerIndex(GetPlayerIndex())
2278
+ PaddleObj.SetColor(0.2 + lane*0.12, 0.8 - lane*0.08, 1.0)
2279
+
2280
+ var PaddleHalf = (W*TS*0.5 - TS*0.6)
2281
+ forever {
2282
+ var move = GetMoveVectorWorld()
2283
+ var vx = move.X * 600
2284
+ var p = PaddleObj.GetPosition()
2285
+ var nextX = p.X + vx*DeltaSecs()
2286
+ if (vx < 0 and nextX <= -PaddleHalf) or (vx > 0 and nextX >= PaddleHalf) {
2287
+ vx = 0
2288
+ }
2289
+ PaddleObj.MoveWithVelocityContinuously(Vector(vx, 0, 0))
2290
+ }
2291
+ }
2292
+ event OnDestroy {
2293
+ if PaddleObj.Exists() { PaddleObj.Destroy() }
2294
+ }
2295
+ #end
2296
+
2297
+ #class Ball
2298
+ var V = Vector(260, 260, 0)
2299
+ var ServePos = Position(0, 0, 0)
2300
+ var BoundCenterX = 0
2301
+ var BoundHalfX = 0
2302
+ var BoundTopY = 0
2303
+ var BoundBottomY = 0
2304
+ event OnSpawned {
2305
+ SetSizeTiles(0.5, 0.5, 0.5)
2306
+ SnapToGroundWithTag(Tag.Ground)
2307
+ ServePos = GetPosition()
2308
+ }
2309
+ event Update {
2310
+ var n = GetPosition() + V * DeltaSecs()
2311
+
2312
+ // Reflect on bounds using predicted next pos.
2313
+ if n.X <= BoundCenterX - BoundHalfX or n.X >= BoundCenterX + BoundHalfX { V.X = -V.X }
2314
+ if n.Y >= BoundTopY { V.Y = -V.Y }
2315
+ // Missed ball: soft serve reset (RestartGame() would reload the whole project).
2316
+ if n.Y <= BoundBottomY {
2317
+ SetPosition(ServePos)
2318
+ V = Vector(260, 260, 0)
2319
+ return
2320
+ }
2321
+
2322
+ for o in GetTouchedObjects() {
2323
+ if o.HasTag(Tag.Brick) {
2324
+ o.Destroy()
2325
+ V.Y = -V.Y
2326
+ break
2327
+ }
2328
+ if o.HasTag(Tag.Paddle) {
2329
+ V.Y = V.Y.Abs()
2330
+ V.X = V.X + (GetPosition().X - o.GetPosition().X) * 4
2331
+ }
2332
+ }
2333
+
2334
+ MoveWithVelocityContinuously(V)
2335
+ }
2336
+ #end
2337
+
2338
+ saved paddle { object P { model = "alias.model.paddle" size_tiles = (2, 0.6, 0.2) tag = Tag.Paddle } }
2339
+ saved ball { object B : Ball { model = "alias.model.ball" } }
2340
+ saved brick { object K { model = "alias.model.brick" size_tiles = (0.8, 0.5, 0.3) tag = Tag.Brick } }
2341
+
2342
+ material GroundMat { base = "PolishedConcrete" }
2343
+ object ground { model = "cube" material = "GroundMat" position = (0, 0, -100) size = (10000, 10000, 100) tag = Tag.Ground }
2344
+ display manager : BoardManager { visible = false }
2345
+ screenCanvas HUD { textLabel T { offset = (20, 20) size = (520, 28) align = (left, center) text = "Shared board: move your paddle with A/D or mobile thumbstick" color = (1, 1, 1) } }
2346
+
2347
+ </example_breakout>
2348
+
2349
+
2350
+ <example_dota>
2351
+ // dota.weave
2352
+ // Multiplayer: team-based MOBA (per-player heroes via SetPlayerOwnerIndex/SetTeam, ByThisPlayer input).
2353
+ // Caveat: the shop window is a single shared canvas.
2354
+
2355
+ alias effect.explosion = "ori.effect.nuttametee_b_2523.w_explosion0"
2356
+ alias model.player_unit = "sphere"
2357
+ alias model.creep = "sphere"
2358
+ alias model.projectile // bare: auto-binds a generated mesh from the library
2359
+
2360
+ #class TowerBrain
2361
+ var HP = 500
2362
+ var MaxHP = 500
2363
+ var LastShotTime: Number
2364
+ var AttackRange = 270
2365
+ event OnSpawned {
2366
+ LastShotTime = TimeSecs() - 1000.0
2367
+ SetHoverHPBar(HP, MaxHP)
2368
+ }
2369
+ event Update {
2370
+ var team = GetTeam()
2371
+ var bestDist = 1000000
2372
+ var target = None
2373
+
2374
+ for creep in GetAllObjectsWithSavedType(SavedObject.CreepUnit) {
2375
+ if creep.GetTeam() != team {
2376
+ var dist = DistanceXYTo(creep)
2377
+ if dist < bestDist and dist <= AttackRange {
2378
+ bestDist = dist
2379
+ target = creep
2380
+ }
2381
+ }
2382
+ }
2383
+ if target == None {
2384
+ for pUnit in GetAllObjectsWithSavedType(SavedObject.PlayerUnit) {
2385
+ if pUnit.GetTeam() != team {
2386
+ var dist = DistanceXYTo(pUnit)
2387
+ if dist < bestDist and dist <= AttackRange {
2388
+ bestDist = dist
2389
+ target = pUnit
2390
+ }
2391
+ }
2392
+ }
2393
+ }
2394
+ if target == None { return }
2395
+ var now = TimeSecs()
2396
+ if (now - LastShotTime) < 1.5 { return }
2397
+ var start = GetPosition().ShiftUp(50)
2398
+ var proj = SpawnObject(SavedObject.Projectile, start)
2399
+ proj.target = target
2400
+ proj.SetColor(0.40, 0.700, 0.9)
2401
+ LastShotTime = now
2402
+ }
2403
+ event WasAttacked {
2404
+ var dmg = Event.Damage.Round()
2405
+ HP = HP - dmg
2406
+ if HP < 0 { HP = 0 }
2407
+ SetHoverHPBar(HP, MaxHP)
2408
+ SpawnFloatingText("-" + dmg, Vector(1, 0, 0))
2409
+ if HP <= 0 {
2410
+ SpawnEffect(Ori.alias.effect.explosion, GetPosition())
2411
+ Die()
2412
+ }
2413
+ }
2414
+ #end
2415
+
2416
+
2417
+ #class GameManager
2418
+ event OnSpawned {
2419
+ var MapHalfTiles = 12
2420
+ var WallThicknessTiles = 1
2421
+ var WallHeightTiles = 2
2422
+ var WallLenTiles = (MapHalfTiles * 2 + 2)
2423
+
2424
+ var wTop = SpawnAtTile(SavedObject.WallCube, 0, MapHalfTiles + (WallThicknessTiles * 0.5))
2425
+ wTop.SetSizeTiles(WallLenTiles, WallThicknessTiles, WallHeightTiles)
2426
+ var wBottom = SpawnAtTile(SavedObject.WallCube, 0, -MapHalfTiles - (WallThicknessTiles * 0.5))
2427
+ wBottom.SetSizeTiles(WallLenTiles, WallThicknessTiles, WallHeightTiles)
2428
+ var wLeft = SpawnAtTile(SavedObject.WallCube, -MapHalfTiles - (WallThicknessTiles * 0.5), 0)
2429
+ wLeft.SetSizeTiles(WallThicknessTiles, WallLenTiles, WallHeightTiles)
2430
+ var wRight = SpawnAtTile(SavedObject.WallCube, MapHalfTiles + (WallThicknessTiles * 0.5), 0)
2431
+ wRight.SetSizeTiles(WallThicknessTiles, WallLenTiles, WallHeightTiles)
2432
+
2433
+ var marginTiles = 2
2434
+ var CP1TilePos = Vector(-MapHalfTiles + marginTiles, -MapHalfTiles + marginTiles, 0)
2435
+ var CP2TilePos = Vector(-MapHalfTiles + marginTiles, MapHalfTiles - marginTiles, 0)
2436
+ var CP3TilePos = Vector( MapHalfTiles - marginTiles, -MapHalfTiles + marginTiles, 0)
2437
+ var CP4TilePos = Vector( MapHalfTiles - marginTiles, MapHalfTiles - marginTiles, 0)
2438
+ var CP5TilePos = Vector(0, 0, 0)
2439
+
2440
+ var CP1 = SpawnAtTile(SavedObject.Checkpoint, CP1TilePos); CP1.AddTag(Tag.CP1)
2441
+ var CP2o = SpawnAtTile(SavedObject.Checkpoint, CP2TilePos); CP2o.AddTag(Tag.CP2)
2442
+ var CP3o = SpawnAtTile(SavedObject.Checkpoint, CP3TilePos); CP3o.AddTag(Tag.CP3)
2443
+ var CP4 = SpawnAtTile(SavedObject.Checkpoint, CP4TilePos); CP4.AddTag(Tag.CP4)
2444
+ var CP5o = SpawnAtTile(SavedObject.Checkpoint, CP5TilePos); CP5o.AddTag(Tag.CP5)
2445
+
2446
+ var laneThicknessTiles = 3.0
2447
+ var laneHeightTiles = 0.1
2448
+
2449
+ var mid12 = (CP1TilePos + CP2TilePos) * 0.5
2450
+ var d12 = CP1TilePos.DistanceTo(CP2TilePos)
2451
+ var L12 = SpawnAtTile(SavedObject.LaneCube, mid12)
2452
+ L12.SetSizeTiles(d12, laneThicknessTiles, laneHeightTiles)
2453
+ L12.LookAtTileHorizontal(CP2TilePos)
2454
+ var mid15 = (CP1TilePos + CP5TilePos) * 0.5
2455
+ var d15 = CP1TilePos.DistanceTo(CP5TilePos)
2456
+ var L15 = SpawnAtTile(SavedObject.LaneCube, mid15)
2457
+ L15.SetSizeTiles(d15, laneThicknessTiles, laneHeightTiles)
2458
+ L15.LookAtTileHorizontal(CP5TilePos)
2459
+ var mid13 = (CP1TilePos + CP3TilePos) * 0.5
2460
+ var d13 = CP1TilePos.DistanceTo(CP3TilePos)
2461
+ var L13 = SpawnAtTile(SavedObject.LaneCube, mid13)
2462
+ L13.SetSizeTiles(d13, laneThicknessTiles, laneHeightTiles)
2463
+ L13.LookAtTileHorizontal(CP3TilePos)
2464
+ var mid42 = (CP4TilePos + CP2TilePos) * 0.5
2465
+ var d42 = CP4TilePos.DistanceTo(CP2TilePos)
2466
+ var L42 = SpawnAtTile(SavedObject.LaneCube, mid42)
2467
+ L42.SetSizeTiles(d42, laneThicknessTiles, laneHeightTiles)
2468
+ L42.LookAtTileHorizontal(CP2TilePos)
2469
+ var mid45 = (CP4TilePos + CP5TilePos) * 0.5
2470
+ var d45 = CP4TilePos.DistanceTo(CP5TilePos)
2471
+ var L45 = SpawnAtTile(SavedObject.LaneCube, mid45)
2472
+ L45.SetSizeTiles(d45, laneThicknessTiles, laneHeightTiles)
2473
+ L45.LookAtTileHorizontal(CP5TilePos)
2474
+ var mid43 = (CP4TilePos + CP3TilePos) * 0.5
2475
+ var d43 = CP4TilePos.DistanceTo(CP3TilePos)
2476
+ var L43 = SpawnAtTile(SavedObject.LaneCube, mid43)
2477
+ L43.SetSizeTiles(d43, laneThicknessTiles, laneHeightTiles)
2478
+ L43.LookAtTileHorizontal(CP3TilePos)
2479
+
2480
+ var Dir_CP2_to_CP1 = (CP1TilePos - CP2TilePos).GetNormal()
2481
+ var Pos_Tower_A = CP2TilePos + (Dir_CP2_to_CP1 * 6)
2482
+ var TowerA = SpawnAtTile(SavedObject.TowerUnitR350, Pos_Tower_A)
2483
+ TowerA.SetColor(0, 1, 0); TowerA.SetTeam(0)
2484
+ var Dir_CP5_to_CP1 = (CP1TilePos - CP5TilePos).GetNormal()
2485
+ var Pos_Tower_B = CP5TilePos + (Dir_CP5_to_CP1 * 5)
2486
+ var TowerB = SpawnAtTile(SavedObject.TowerUnit, Pos_Tower_B)
2487
+ TowerB.SetColor(0, 1, 0); TowerB.SetTeam(0)
2488
+ var Dir_CP3_to_CP1 = (CP1TilePos - CP3TilePos).GetNormal()
2489
+ var Pos_Tower_C = CP3TilePos + (Dir_CP3_to_CP1 * 6)
2490
+ var TowerC = SpawnAtTile(SavedObject.TowerUnitR350, Pos_Tower_C)
2491
+ TowerC.SetColor(0, 1, 0); TowerC.SetTeam(0)
2492
+ var Dir_CP2_to_CP4 = (CP4TilePos - CP2TilePos).GetNormal()
2493
+ var Pos_Tower_X = CP2TilePos + (Dir_CP2_to_CP4 * 6)
2494
+ var TowerX = SpawnAtTile(SavedObject.TowerUnitR350, Pos_Tower_X)
2495
+ TowerX.SetColor(1, 0, 0); TowerX.SetTeam(1)
2496
+ var Dir_CP5_to_CP4 = (CP4TilePos - CP5TilePos).GetNormal()
2497
+ var Pos_Tower_Y = CP5TilePos + (Dir_CP5_to_CP4 * 5)
2498
+ var TowerY = SpawnAtTile(SavedObject.TowerUnit, Pos_Tower_Y)
2499
+ TowerY.SetColor(1, 0, 0); TowerY.SetTeam(1)
2500
+ var Dir_CP3_to_CP4 = (CP4TilePos - CP3TilePos).GetNormal()
2501
+ var Pos_Tower_Z = CP3TilePos + (Dir_CP3_to_CP4 * 6)
2502
+ var TowerZ = SpawnAtTile(SavedObject.TowerUnitR350, Pos_Tower_Z)
2503
+ TowerZ.SetColor(1, 0, 0); TowerZ.SetTeam(1)
2504
+ // Player heroes spawn per join in '#class player' OnSpawned (any player count works).
2505
+ forever {
2506
+ var cg12a = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(0, 1, 0))
2507
+ cg12a.SetColor(0, 1, 0); cg12a.SetTeam(0); cg12a.AddTag(Tag.PathToCP2)
2508
+ var cg12b = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(0, -1, 0))
2509
+ cg12b.SetColor(0, 1, 0); cg12b.SetTeam(0); cg12b.AddTag(Tag.PathToCP2)
2510
+ var cg15a = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(1, 0, 0))
2511
+ cg15a.SetColor(0, 1, 0); cg15a.SetTeam(0); cg15a.AddTag(Tag.PathToCP5)
2512
+ var cg15b = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(-1, 0, 0))
2513
+ cg15b.SetColor(0, 1, 0); cg15b.SetTeam(0); cg15b.AddTag(Tag.PathToCP5)
2514
+ var cg13a = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(0, 2, 0))
2515
+ cg13a.SetColor(0, 1, 0); cg13a.SetTeam(0); cg13a.AddTag(Tag.PathToCP3)
2516
+ var cg13b = SpawnAtTile(SavedObject.CreepUnit, CP1TilePos + Vector(0, -2, 0))
2517
+ cg13b.SetColor(0, 1, 0); cg13b.SetTeam(0); cg13b.AddTag(Tag.PathToCP3)
2518
+ var cr42a = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(0, 1, 0))
2519
+ cr42a.SetColor(1, 0, 0); cr42a.SetTeam(1); cr42a.AddTag(Tag.PathToCP2)
2520
+ var cr42b = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(0, -1, 0))
2521
+ cr42b.SetColor(1, 0, 0); cr42b.SetTeam(1); cr42b.AddTag(Tag.PathToCP2)
2522
+ var cr45a = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(1, 0, 0))
2523
+ cr45a.SetColor(1, 0, 0); cr45a.SetTeam(1); cr45a.AddTag(Tag.PathToCP5)
2524
+ var cr45b = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(-1, 0, 0))
2525
+ cr45b.SetColor(1, 0, 0); cr45b.SetTeam(1); cr45b.AddTag(Tag.PathToCP5)
2526
+ var cr43a = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(0, 2, 0))
2527
+ cr43a.SetColor(1, 0, 0); cr43a.SetTeam(1); cr43a.AddTag(Tag.PathToCP3)
2528
+ var cr43b = SpawnAtTile(SavedObject.CreepUnit, CP4TilePos + Vector(0, -2, 0))
2529
+ cr43b.SetColor(1, 0, 0); cr43b.SetTeam(1); cr43b.AddTag(Tag.PathToCP3)
2530
+ Wait(5)
2531
+ }
2532
+ }
2533
+ #end
2534
+ #class UnitStats
2535
+ var HP = 100
2536
+ var MaxHP = 100
2537
+ var MP = 100
2538
+ var Level = 1
2539
+ var XP = 0
2540
+ var XPToNextLevel = 100
2541
+ var Armor = 3
2542
+ var LastShotTime: Number
2543
+ var MoveGoal: Position
2544
+ var HasMoveGoal = false
2545
+
2546
+ event OnSpawned {
2547
+ LastShotTime = TimeSecs() - 1000.0
2548
+ SetHoverHPBar(HP, 100)
2549
+ SetHoverBar(1, MP, 100, Vector(0.2, 0.6, 1.0), Vector(0.1, 0.1, 0.2), 0, 8, 50, 6)
2550
+ }
2551
+ func LevelUp() {
2552
+ Level += 1
2553
+ XP = XP - XPToNextLevel
2554
+ XPToNextLevel = (XPToNextLevel * 1.1).Round()
2555
+ Armor = 3 + ((Level - 1) * 1.7)
2556
+
2557
+ var maxHP = 100 + (Level * 10)
2558
+ var maxMP = 100 + (Level * 5)
2559
+ HP = maxHP
2560
+ MP = maxMP
2561
+ SetHoverHPBar(HP, maxHP)
2562
+ SetHoverBar(1, MP, maxMP, Vector(0.2, 0.6, 1.0), Vector(0.1, 0.1, 0.2), 0, 8, 50, 6)
2563
+ SpawnFloatingText("LEVEL UP!", Vector(1, 0.8, 0))
2564
+ }
2565
+ func GainXP(amount: Number) {
2566
+ XP += amount
2567
+ SpawnFloatingText("+" + amount + " XP", Vector(1, 1, 0))
2568
+ while XP >= XPToNextLevel {
2569
+ LevelUp()
2570
+ }
2571
+ }
2572
+ event Update {
2573
+ if HasMoveGoal {
2574
+ if DistanceXYTo(MoveGoal) < GetSizeX() * 0.5 { HasMoveGoal = false }
2575
+ else { MoveTowardContinuousHorizontalFacing(MoveGoal, 250) }
2576
+ }
2577
+ var team = GetTeam()
2578
+ var best = 1000000
2579
+ var target = None
2580
+ for e in GetAllObjectsWithSavedType(SavedObject.PlayerUnit) {
2581
+ if e.GetTeam() != team {
2582
+ var d = DistanceXYTo(e)
2583
+ if d < best and d <= 200 { best = d; target = e }
2584
+ }
2585
+ }
2586
+ for c in GetAllObjectsWithSavedType(SavedObject.CreepUnit) {
2587
+ if c.GetTeam() != team {
2588
+ var d2 = DistanceXYTo(c)
2589
+ if d2 < best and d2 <= 200 { best = d2; target = c }
2590
+ }
2591
+ }
2592
+ for t in GetAllObjectsWithSavedType(SavedObject.TowerUnit) {
2593
+ if t.GetTeam() != team {
2594
+ var d3 = DistanceXYTo(t)
2595
+ if d3 < best and d3 <= 200 { best = d3; target = t }
2596
+ }
2597
+ }
2598
+ if target == None { return }
2599
+ var now2 = TimeSecs()
2600
+ if (now2 - LastShotTime) < 0.8 { return }
2601
+ var start = GetPosition().ShiftUp(20)
2602
+ var proj = SpawnObject(SavedObject.Projectile, start)
2603
+ proj.target = target
2604
+ proj.SetColor(0.10, 0.600, 0)
2605
+ LastShotTime = now2
2606
+ }
2607
+ event WasAttacked {
2608
+ var dmg = Event.Damage.Round()
2609
+ HP = HP - dmg
2610
+ if HP < 0 { HP = 0 }
2611
+ SetHoverHPBar(HP, 100 + (Level * 10))
2612
+ SpawnFloatingText("-" + dmg, Vector(1, 0, 0))
2613
+ if HP <= 0 {
2614
+ SpawnEffect(Ori.alias.effect.explosion, GetPosition())
2615
+ Die()
2616
+ }
2617
+ }
2618
+ #end
2619
+ #class ProjectileBrain
2620
+ var target: Object
2621
+ event OnSpawned {
2622
+ var col = GetColor()
2623
+ var dmg = (col.X * 100).Round()
2624
+ var spd = (col.Y * 1000).Round()
2625
+ if dmg <= 0 { dmg = 10 }
2626
+ if spd <= 0 { spd = 600 }
2627
+
2628
+ forever {
2629
+ if not target.Exists() { Destroy(); return }
2630
+ var aim = target.GetPosition()
2631
+ MoveTowardContinuousFacing(aim, spd)
2632
+ if DistanceTo(aim) <= 18 {
2633
+ Self.Attack(target, dmg)
2634
+ SpawnEffect(Ori.alias.effect.explosion, aim)
2635
+ Destroy()
2636
+ return
2637
+ }
2638
+ }
2639
+ }
2640
+ #end
2641
+ #class CreepBrain
2642
+ var HP = 10
2643
+ var LastAttackTime = 0
2644
+ var XPReward = 10
2645
+ event OnSpawned {
2646
+ SetHoverHPBar(HP, 10)
2647
+ }
2648
+ event Update {
2649
+ var dest = GetPosition()
2650
+ if HasTag(Tag.PathToCP2) {
2651
+ var rs = GetAllRegionsWithTag(Tag.CP2); if not rs.IsEmpty() { dest = rs.Get(0).GetPosition() }
2652
+ } else if HasTag(Tag.PathToCP5) {
2653
+ var rs2 = GetAllRegionsWithTag(Tag.CP5); if not rs2.IsEmpty() { dest = rs2.Get(0).GetPosition() }
2654
+ } else if HasTag(Tag.PathToCP3) {
2655
+ var rs3 = GetAllRegionsWithTag(Tag.CP3); if not rs3.IsEmpty() { dest = rs3.Get(0).GetPosition() }
2656
+ }
2657
+ if DistanceXYTo(dest) >= GetSizeX() * 0.5 {
2658
+ MoveTowardContinuousHorizontalFacing(dest, 180)
2659
+ }
2660
+ var now = TimeSecs()
2661
+ if (now - LastAttackTime) >= 1.0 {
2662
+ var team = GetTeam()
2663
+ var enemyTeam = 1 - team
2664
+ var target = None
2665
+ var best = 1000000
2666
+ for o in GetObjectsWithinDistanceWithTeam(30, enemyTeam) {
2667
+ if o.IsSavedType(SavedObject.CreepUnit) or o.IsSavedType(SavedObject.PlayerUnit) or o.IsSavedType(SavedObject.TowerUnit) {
2668
+ var dd = DistanceXYTo(o)
2669
+ if dd < best { best = dd; target = o }
2670
+ }
2671
+ }
2672
+ if target != None {
2673
+ var dmg = RandomNumber(1, 3).Round()
2674
+ Self.Attack(target, dmg)
2675
+ LastAttackTime = now
2676
+ }
2677
+ }
2678
+ }
2679
+ event WasAttacked {
2680
+ var dmg = Event.Damage.Round()
2681
+ HP = HP - dmg
2682
+ if HP < 0 { HP = 0 }
2683
+ SetHoverHPBar(HP, 10)
2684
+
2685
+ SpawnFloatingText("-" + dmg, Vector(1, 0.5, 0))
2686
+ if HP <= 0 {
2687
+ for unit in GetAllObjectsWithSavedType(SavedObject.PlayerUnit) {
2688
+ unit.GainXP(XPReward)
2689
+ }
2690
+ SpawnEffect(Ori.alias.effect.explosion, GetPosition())
2691
+ Die()
2692
+ }
2693
+ }
2694
+ #end
2695
+
2696
+
2697
+ #class player
2698
+ var IsAimingSkill = false
2699
+ var LastSkillQTime: Number
2700
+ var IsShowingCooldownIndicator = false
2701
+ var Gold = 200
2702
+ var GoldTimer: Timer // auto-constructed timer
2703
+ event OnSpawned {
2704
+ SetCameraToTopDown()
2705
+ SetVisible(false)
2706
+ SetPlayerModeSpectator()
2707
+ LastSkillQTime = TimeSecs() - 1000.0
2708
+ GoldTimer.StartCountDown(1)
2709
+
2710
+ var side = GetPlayerIndex() % 2
2711
+ SetTeam(side)
2712
+
2713
+ // Field my hero at my team's corner; slot offset keeps teammates from stacking.
2714
+ var slot = (GetPlayerIndex() / 2).RoundDown()
2715
+ var unitTile = Vector(-10 - slot, -10 + slot, 0)
2716
+ var unitColor = Vector(0, 1, 0)
2717
+ if side == 1 { unitTile = Vector(10 + slot, 10 - slot, 0); unitColor = Vector(1, 0, 0) }
2718
+ var u = SpawnAtTile(SavedObject.PlayerUnit, unitTile)
2719
+ u.SetColor(unitColor)
2720
+ u.SetTeam(side)
2721
+ u.SetPlayerOwnerIndex(GetPlayerIndex())
2722
+ u.SetHoverText(GetPlayerName()) // owner's username above the hero (the spectator body is hidden)
2723
+ }
2724
+ event OnDestroy {
2725
+ var u = GetMyUnit()
2726
+ if u.Exists() { u.Destroy() }
2727
+ }
2728
+ event KeyboardDownByThisPlayer[Q] {
2729
+ var cdLeft = 2.5 - (TimeSecs() - LastSkillQTime)
2730
+ if cdLeft <= 0 {
2731
+ IsAimingSkill = not IsAimingSkill
2732
+ IsShowingCooldownIndicator = false
2733
+ } else {
2734
+ IsShowingCooldownIndicator = not IsShowingCooldownIndicator
2735
+ IsAimingSkill = false
2736
+ }
2737
+ }
2738
+ func GetMyUnit() -> UnitStats {
2739
+ var myIndex = GetPlayerIndex()
2740
+ for unit in GetAllObjectsWithSavedType(SavedObject.PlayerUnit) {
2741
+ if unit.GetPlayerOwnerIndex() == myIndex { return (unit as UnitStats) }
2742
+ }
2743
+ return None
2744
+ }
2745
+ event Update {
2746
+ var myUnit = GetMyUnit()
2747
+ if myUnit == None { return }
2748
+ SetCameraPivotTargetThisFrame(myUnit.GetPosition())
2749
+ if GoldTimer.IsDone() {
2750
+ Gold += 1
2751
+ GoldTimer.StartCountDown(1)
2752
+ }
2753
+ if IsShowingCooldownIndicator {
2754
+ var p = GetMouseTargetPositionOnPlaneZ(0)
2755
+ DrawSkillAreaCircle(p, 200, Vector(1, 0, 0))
2756
+ } else if IsAimingSkill {
2757
+ var p = GetMouseTargetPositionOnPlaneZ(0)
2758
+ DrawSkillAreaCircle(p, 200, Vector(1, 0.5, 0.2))
2759
+ }
2760
+ var cdLeft = 2.5 - (TimeSecs() - LastSkillQTime)
2761
+ var unitName = ""
2762
+ var myIndex = GetPlayerIndex()
2763
+ if myIndex == 0 { unitName = "A" }
2764
+ if myIndex == 1 { unitName = "B" }
2765
+ if myIndex == 2 { unitName = "C" }
2766
+ if myIndex == 3 { unitName = "X" }
2767
+ if myIndex == 4 { unitName = "Y" }
2768
+ if myIndex >= 5 { unitName = "Z" }
2769
+
2770
+ var statsLine1 = unitName + " | LV: " + myUnit.Level + " | ARM: " + myUnit.Armor + " | Gold: " + Gold
2771
+ var statsLine2 = "XP: " + myUnit.XP + "/" + myUnit.XPToNextLevel
2772
+
2773
+ var fullText = statsLine1 + "\n" + statsLine2
2774
+ if cdLeft > 0 {
2775
+ fullText = fullText + "\nSkill Cooldown"
2776
+ }
2777
+ myUnit.Say(fullText)
2778
+ }
2779
+ event ActionDownByThisPlayer[Primary] {
2780
+ var myUnit = GetMyUnit()
2781
+ if myUnit == None { return }
2782
+ var cdLeft = 2.5 - (TimeSecs() - LastSkillQTime)
2783
+ if IsAimingSkill and cdLeft > 0 {
2784
+ myUnit.SpawnFloatingText("COOLDOWN", Vector(1, 1, 0))
2785
+ return
2786
+ }
2787
+ if not IsAimingSkill { return }
2788
+ var manaCost = 10
2789
+ if myUnit.MP < manaCost {
2790
+ myUnit.SpawnFloatingText("Not enough mana", Vector(0.2, 0.6, 1.0))
2791
+ IsAimingSkill = false
2792
+ return
2793
+ }
2794
+
2795
+ myUnit.MP -= manaCost
2796
+ myUnit.SetHoverBar(1, myUnit.MP, 100 + (myUnit.Level * 5), Vector(0.2, 0.6, 1.0), Vector(0.1, 0.1, 0.2), 0, 8, 50, 6)
2797
+ LastSkillQTime = TimeSecs()
2798
+ var p = GetMouseTargetPositionOnPlaneZ(0)
2799
+ SpawnEffect(Ori.alias.effect.explosion, p)
2800
+ var enemyTeam = 1 - myUnit.GetTeam()
2801
+ for t in p.GetObjectsWithinXYDistanceWithTeam(200, enemyTeam) {
2802
+ if t.IsSavedType(SavedObject.PlayerUnit) or t.IsSavedType(SavedObject.CreepUnit) {
2803
+ myUnit.Attack(t, 5)
2804
+ }
2805
+ }
2806
+
2807
+ IsAimingSkill = false
2808
+ }
2809
+ event RightMouseClickByThisPlayer {
2810
+ if IsAimingSkill { IsAimingSkill = false; return }
2811
+
2812
+ var myUnit = GetMyUnit()
2813
+ if myUnit == None { return }
2814
+
2815
+ var p = GetMouseTargetPositionOnPlaneZ(0)
2816
+ var lim = 580
2817
+ var goal = Position(p.X.Clamp(-lim, lim), p.Y.Clamp(-lim, lim), myUnit.GetPosition().Z)
2818
+
2819
+ // issue move order; unit drives itself in its Update (over-time builtins can't run on foreign refs)
2820
+ myUnit.MoveGoal = goal
2821
+ myUnit.HasMoveGoal = true
2822
+ }
2823
+ #end
2824
+
2825
+
2826
+ #class UIManager
2827
+ var ShopWindow: Object
2828
+ var ShopButton: Object
2829
+ var BuyHPButton: Object
2830
+ var BuyManaButton: Object
2831
+ var RosterRows: List
2832
+
2833
+ event OnSpawned {
2834
+ ShopWindow = Scene.ShopWindow
2835
+ ShopButton = Scene.ShopButton
2836
+ BuyHPButton = Scene.BuyHPButton
2837
+ BuyManaButton = Scene.BuyManaButton
2838
+ ShopWindow.SetVisible(false)
2839
+
2840
+ // Roster: cache the count-expanded rows once, then rebind names each second.
2841
+ RosterRows = []
2842
+ var i = 0
2843
+ while i < 6 { RosterRows.Add(FindChildUI("RosterRow" + i.ToText())); i += 1 }
2844
+ forever {
2845
+ var r = 0
2846
+ for p in GetAllPlayers() {
2847
+ if r >= RosterRows.Count() { break }
2848
+ var n = p.GetPlayerName()
2849
+ if n == "" { n = "Player " + (r + 1).ToText() } // validation/headless players have no username
2850
+ var c = Vector(0.4, 0.8, 1.0)
2851
+ if p.GetTeam() == 1 { c = Vector(1.0, 0.45, 0.4) }
2852
+ RosterRows.Get(r).SetText(n); RosterRows.Get(r).SetUiColor(c); RosterRows.Get(r).SetVisible(true)
2853
+ r += 1
2854
+ }
2855
+ while r < RosterRows.Count() { RosterRows.Get(r).SetVisible(false); r += 1 }
2856
+ Wait(1)
2857
+ }
2858
+ }
2859
+ event ClickButton {
2860
+ var clicked = Event.ClickedButton
2861
+ var player = Event.ButtonClicker
2862
+
2863
+ if clicked == ShopButton {
2864
+ var opened = ShopWindow.IsVisible()
2865
+ ShopWindow.SetVisible(not opened)
2866
+ }
2867
+
2868
+ if clicked == BuyHPButton {
2869
+ var cost = 10
2870
+ var myUnit = player.GetMyUnit()
2871
+ if myUnit.Exists() and player.Gold >= cost {
2872
+ player.Gold = player.Gold - cost
2873
+ var maxHP = 100 + (myUnit.Level * 10)
2874
+ myUnit.HP = maxHP
2875
+ myUnit.SetHoverHPBar(myUnit.HP, maxHP)
2876
+ myUnit.SpawnFloatingText("HP Full!", Vector(0, 1, 0))
2877
+ } else if myUnit.Exists() {
2878
+ myUnit.SpawnFloatingText("Not enough gold!", Vector(1, 1, 0))
2879
+ }
2880
+ }
2881
+
2882
+ if clicked == BuyManaButton {
2883
+ var cost = 5
2884
+ var myUnit = player.GetMyUnit()
2885
+ if myUnit.Exists() and player.Gold >= cost {
2886
+ player.Gold = player.Gold - cost
2887
+ var maxMP = 100 + (myUnit.Level * 5)
2888
+ myUnit.MP = maxMP
2889
+ myUnit.SetHoverBar(1, myUnit.MP, maxMP, Vector(0.2, 0.6, 1.0), Vector(0.1, 0.1, 0.2), 0, 8, 50, 6)
2890
+ myUnit.SpawnFloatingText("MP Full!", Vector(0.2, 0.6, 1.0))
2891
+ } else if myUnit.Exists() {
2892
+ myUnit.SpawnFloatingText("Not enough gold!", Vector(1, 1, 0))
2893
+ }
2894
+ }
2895
+ }
2896
+ #end
2897
+
2898
+
2899
+ material GroundMat { base = "StylizedGrass" }
2900
+ object ground { model = "cube" material = "GroundMat" position=(0, 0, -100) size=(10000, 10000, 100) }
2901
+ display Manager0 : GameManager { visible = false }
2902
+
2903
+ saved Checkpoint {
2904
+ region mark { model="alias.model.checkpoint" size=(20, 20, 60) }
2905
+ }
2906
+ saved WallCube {
2907
+ object cube { model="alias.model.wall" size=(40, 40, 80) }
2908
+ }
2909
+ saved LaneCube {
2910
+ object cube { model="alias.model.lane" size=(80, 20, 6) }
2911
+ }
2912
+ saved PlayerUnit {
2913
+ object hero : UnitStats { model="alias.model.player_unit" size=40 }
2914
+ }
2915
+ saved CreepUnit {
2916
+ crowd sphere : CreepBrain { model="alias.model.creep" size=20 }
2917
+ }
2918
+ saved Projectile {
2919
+ object sphere : ProjectileBrain { model="alias.model.projectile" size=6 }
2920
+ }
2921
+ saved TowerUnit {
2922
+ object tower : TowerBrain { model="alias.model.tower" size=(60, 60, 120) }
2923
+ }
2924
+ saved TowerUnitR350 {
2925
+ object tower : TowerBrain {
2926
+ model="alias.model.tower"
2927
+ size=(60, 60, 120)
2928
+ override AttackRange=350
2929
+ }
2930
+ }
2931
+
2932
+ screenCanvas MainUI : UIManager {
2933
+ // Team roster (top-left): who is in the match, colored by team.
2934
+ vStack Roster {
2935
+ offset = (20, 20)
2936
+ padding = 8
2937
+ gap = 4
2938
+ color = (0.1, 0.1, 0.14)
2939
+ textLabel RosterRow { count = 6 size = (200, 22) font_size = 14 }
2940
+ }
2941
+
2942
+ // vStack title over a row of buy buttons; no size = hug to fit content.
2943
+ vStack ShopWindow {
2944
+ offset = (1200, 600)
2945
+ padding = 10
2946
+ gap = 10
2947
+ color = (0.2, 0.2, 0.25)
2948
+ textLabel ShopTitle { size = (180, 30) text = "Item Shop" }
2949
+ hStack BuyRow {
2950
+ gap = (20, 0)
2951
+ button BuyHPButton { size = (80, 80) text = "HP (10G)" }
2952
+ button BuyManaButton { size = (80, 80) text = "MP (5G)" }
2953
+ }
2954
+ }
2955
+
2956
+ // hStack lays the skill buttons across; no size = hug (-> 370x100). Bottom-center anchored ability bar.
2957
+ hStack SkillBar {
2958
+ anchor = (center, bottom)
2959
+ offset = (0, -20)
2960
+ padding = 10
2961
+ gap = 10
2962
+ button SkillButtonQ { size = (80, 80) color = (0.1, 0.1, 0.1) text = "Q (10)" }
2963
+ button SkillButtonW { size = (80, 80) color = (0.1, 0.1, 0.1) text = "W" }
2964
+ button SkillButtonE { size = (80, 80) color = (0.1, 0.1, 0.1) text = "E" }
2965
+ button SkillButtonR { size = (80, 80) color = (0.1, 0.1, 0.1) text = "R" }
2966
+ }
2967
+ frame ShopPanel {
2968
+ anchor = (right, bottom)
2969
+ offset = (-20, -20)
2970
+ size = (140, 100)
2971
+ button ShopButton {
2972
+ offset = (10, 10)
2973
+ size = (120, 80)
2974
+ textLabel ShopText {
2975
+ text = "SHOP"
2976
+ }
2977
+ }
2978
+ }
2979
+ }
2980
+
2981
+
2982
+ </example_dota>
2983
+
2984
+
2985
+ <example_fps_viewmodel>
2986
+ // Minimal FPS viewmodel skeleton — the engine defaults do the genre work:
2987
+ // EquipWeapon seats the hand on the library rifle's grip socket, the built-in canon
2988
+ // hold poses the first-person arms (hands + a sliver of forearm), and
2989
+ // EnableLocalWeaponView(Offset, RotationDeg, Scale, FovDegrees) renders weapon+arms
2990
+ // through the dedicated viewmodel pass (own FOV, never clips into walls).
2991
+ // No SetWeaponConfig, no anchors, no clips required.
2992
+
2993
+ #class player
2994
+ var T: Number
2995
+
2996
+ event OnSpawned {
2997
+ SetCameraToFirstPerson()
2998
+ EnableAutoMoveAnimations() // optional (walk/run clips while moving) - the viewmodel renders without it
2999
+ EquipWeapon(Ori.alias.model.rifle)
3000
+ EnableLocalWeaponView(Position(15, -22, 38), Vector(2, 4, 0), 1.0, 55)
3001
+ }
3002
+
3003
+ event Update {
3004
+ // Walk into the wall ahead: arms stay held, the viewmodel never clips.
3005
+ T = T + 1
3006
+ if T > 120 {
3007
+ if T < 420 {
3008
+ MoveInDirectionContinuously(Vector(0, 1, 0), 620)
3009
+ }
3010
+ }
3011
+ }
3012
+ #end
3013
+
3014
+ object Ground {
3015
+ model = "cube"
3016
+ size = (2400, 2400, 20)
3017
+ position = (0, 0, -10)
3018
+ color = (0.45, 0.5, 0.4)
3019
+ }
3020
+
3021
+ object Wall {
3022
+ model = "cube"
3023
+ size = (800, 60, 300)
3024
+ position = (0, 300, 150)
3025
+ color = (0.6, 0.55, 0.5)
3026
+ }
3027
+
3028
+ </example_fps_viewmodel>
3029
+
3030
+
3031
+ <example_grass_meadow_adjuster_demo>
3032
+ // Grass meadow: built-in tuft-card grass on gentle hills, all global grass/ground
3033
+ // look knobs declared (values apply on load; sliders live in the Terrain inspector -
3034
+ // click the terrain; Ctrl+S folds edits back into this block). Ground under grass auto-tints.
3035
+
3036
+ #adjuster
3037
+ section "Grass"
3038
+ knob SetGrassRootColor = #41591c
3039
+ knob SetGrassTipColor = #93b944
3040
+ knob SetGrassBrightnessVariationPercent = 120
3041
+ knob SetGrassSizeVariationPercent = 100
3042
+ knob SetGrassHeightMinCm = 20
3043
+ knob SetGrassHeightMaxCm = 90
3044
+ knob SetGrassWavePeriodCm = 700
3045
+ knob SetGrassWindStrengthCm = 4 range 0..30
3046
+ knob SetGrassWindSpeedPercent = 100 range 0..300
3047
+ knob SetGrassBladeWidthPercent = 100
3048
+ knob SetGrassSpacingCm = 20 range 0..60
3049
+ #end
3050
+
3051
+ // Tuft-card atlas (replaces the built-in one via SetTerrainGrass material): 2x2 variant
3052
+ // cells, GREYSCALE luminance = root->tip gradient; color comes from the grass color knobs.
3053
+ #texture GrassTufts
3054
+ mapping = uv;
3055
+ wrap = clamp;
3056
+ render_mode = cutout;
3057
+ param blades = 34.0 range 12.0..60.0;
3058
+ let vid = floor(uv.x * 2.0) + floor(uv.y * 2.0) * 2.0;
3059
+ let lu = fract(uv.x * 2.0);
3060
+ let g = 1.0 - fract(uv.y * 2.0);
3061
+ let fan = (lu - 0.5) * (0.3 + 0.3 * rand(vid, seed: 3.0)) * g * g;
3062
+ let x = lu - fan;
3063
+ let id = floor(x * blades) + vid * 977.0;
3064
+ let bell = sat(1.0 - abs((floor(x * blades) + 0.5) / blades - 0.5) * 2.4);
3065
+ let h = (0.25 + 0.70 * rand(id)) * (0.25 + 0.75 * bell);
3066
+ let t = sat((g - 0.06) / max(h - 0.06, 0.05));
3067
+ let curve = (rand(id, seed: 13.0) - 0.5) * 0.55 * t * t;
3068
+ let w = (0.34 + 0.30 * rand(id, seed: 7.0)) * (1.0 - 0.75 * t * t) * sat(t * 7.0);
3069
+ let strip = 1.0 - smoothstep(w, w + 0.22, abs(fract(x * blades) - 0.5 + curve) * 2.0);
3070
+ let a = strip * (1.0 - smoothstep(h - 0.10, h, g)) * step(0.08, h) * smoothstep(0.0, 0.05, t);
3071
+ out.color = ramp(t, [0.0: color("#0a0a0a"), 1.0: color("#e6e6e6")]) * (0.85 + 0.30 * rand(id, seed: 5.0));
3072
+ out.alpha = sat(a) * smoothstep(0.01, 0.05, lu) * (1.0 - smoothstep(0.95, 0.99, lu));
3073
+ out.orm = orm(occlusion: 1.0, roughness: 0.85, metallic: 0.0);
3074
+ out.normal = normal_from_height(g * 0.0, strength: 1.0);
3075
+ #end
3076
+
3077
+ #class MeadowWorld
3078
+ event OnSpawned {
3079
+ var heights: List<Number> = []
3080
+ heights.Resize(49 * 49, 0)
3081
+ TerrainHeightsNoise(heights, 49, 49, 0, 16, 3, 7, 220, 0) // gentle fbm hills
3082
+ var t = SpawnTerrainFromHeights(heights, 49, 49, 100, Origin())
3083
+ t.SetTerrainGrass(None, Material.GrassTufts) // (None, None) also fine: built-in atlas, same knobs
3084
+ }
3085
+ #end
3086
+
3087
+ display MeadowWorldObject : MeadowWorld { visible = false }
3088
+
3089
+ </example_grass_meadow_adjuster_demo>
3090
+
3091
+
3092
+ <example_grassy_eroded_hills>
3093
+ #class player
3094
+ event OnSpawned {
3095
+ forever {
3096
+ var Direction = GetMoveVectorCamera()
3097
+ MoveInDirectionContinuouslyFacing(Direction, 900)
3098
+ }
3099
+ }
3100
+ event ActionDownByThisPlayer[Jump] {
3101
+ if IsOnGround() {
3102
+ Jump(1000)
3103
+ }
3104
+ }
3105
+ #end
3106
+
3107
+ // grassy_eroded_hills: a complete outdoor scene from the eroded_mountain terrain preset -
3108
+ // rolling eroded hills as one lush grass field, lakes in the valleys, gentle water swell.
3109
+ // Pattern notes:
3110
+ // - A bare #terrain block (preset only) is a complete terrain; add keys to override.
3111
+ // - The visible water level is SetWaterEnabled's sea level. The recipe field never goes
3112
+ // below its own zero, so WorldGen scans the generated heights and drops the terrain so
3113
+ // the deepest valley sits 3m under the sea - that is what makes the lakes.
3114
+ // - One #texture ground surface: a single flat color + fine grain only. Low-frequency
3115
+ // color patches tile visibly at distance - avoid them.
3116
+ // - The baked ground normal scales live via SetTerrainNormalStrengthPercent.
3117
+
3118
+ #terrain GrassLand
3119
+ preset = eroded_mountain
3120
+ #end
3121
+
3122
+ #adjuster
3123
+ section "Grass"
3124
+ knob SetGrassTipColor = #71b23e
3125
+ knob SetGrassRootColor = #66a139
3126
+ knob SetGrassBrightnessVariationPercent = 25
3127
+ knob SetGrassHeightMaxCm = 100
3128
+ knob SetGrassHeightMinCm = 50
3129
+ knob SetGrassWavePeriodCm = 700
3130
+ knob SetGrassWindStrengthCm = 6 range 0..20
3131
+ section "Ground"
3132
+ knob SetTerrainNormalStrengthPercent = 20 range 0..300
3133
+ #end
3134
+
3135
+ #texture GrassGround
3136
+ tile_m = 4.0;
3137
+ let fine = noise.fbm(uv * 20.0, octaves: 4, seed: 7, period: 20);
3138
+ let micro = noise.fbm(uv * 64.0, octaves: 2, seed: 9, period: 64);
3139
+ out.color = color("#73aa3e") * (0.96 + fine * 0.05 + micro * 0.03);
3140
+ out.height = fine * 0.6 + micro * 0.4;
3141
+ out.normal = normal_from_height(fine * 0.5 + micro * 0.5, strength: 0.5);
3142
+ out.orm = orm(occlusion: 1.0, roughness: 0.95, metallic: 0.0);
3143
+ #end
3144
+
3145
+ #terrain_surfaces GrassFieldSurfaces
3146
+ surface = baked.GrassGround.color, baked.GrassGround.normal, 4.0, baked.GrassGround.orm, 1.0
3147
+ #end
3148
+
3149
+ // One surface everywhere (weights pick surface 0; grass density = its affinity 1.0).
3150
+ #shader GrassFieldWeights
3151
+ fn ori_surface_weights(ctx: OriSurfaceWeightsCtx) -> OriSurfaceWeights {
3152
+ var w: OriSurfaceWeights;
3153
+ w.w0 = vec4<f32>(1.0, 0.0, 0.0, 0.0);
3154
+ w.w1 = vec4<f32>(0.0, 0.0, 0.0, 0.0);
3155
+ return w;
3156
+ }
3157
+ #end
3158
+
3159
+ material GrassFieldWeightsMat { shader = "GrassFieldWeights" }
3160
+
3161
+ #class WorldGen
3162
+ event OnSpawned {
3163
+ var heights: List<Number> = []
3164
+ var ridges: List<Number> = []
3165
+ TerrainHeightsFromTerrain(heights, ridges, 192, 192, 150)
3166
+ var minH = 99999999
3167
+ for h in heights {
3168
+ if h < minH { minH = h }
3169
+ }
3170
+ var terrain = SpawnTerrainFromHeights(heights, 192, 192, 150, Position(0, 0, 0 - minH - 300))
3171
+ terrain.SetTerrainSurfaces("GrassFieldSurfaces")
3172
+ terrain.SetTerrainSurfaceWeightsMaterial(Material.GrassFieldWeightsMat)
3173
+ SetTerrainDisplayExtension(3000)
3174
+ SetCameraFarDistanceM(3000)
3175
+ SetFogEndDistance(2800)
3176
+ SetSunTilt(33)
3177
+ SetSunRotation(35)
3178
+ SetWaterEnabled(true, 0)
3179
+ SetWaterWaves(5, 800, 0.3, 0, 100)
3180
+ }
3181
+ #end
3182
+
3183
+ display WorldGenObj : WorldGen { visible = false }
3184
+
3185
+ </example_grassy_eroded_hills>
3186
+
3187
+
3188
+ <example_saved_block_font>
3189
+ // Declarative fonts work inside saved blocks (font = "font.google.<Family>" +
3190
+ // font_size); spawned copies keep the custom font.
3191
+ #class player
3192
+ event OnSpawned {
3193
+ SetDefaultPlayerControlsEnabled(true)
3194
+ var hud = SpawnCanvas(SavedObject.StyledHud, Origin())
3195
+ hud.SetPlayerOwnerIndex(GetPlayerIndex())
3196
+ }
3197
+ #end
3198
+
3199
+ object Ground { model = "cube" size = (4000, 4000, 20) position = (0, 0, -10) }
3200
+
3201
+ saved StyledHud {
3202
+ screenCanvas C {
3203
+ vStack Panel { offset = (40, 40) padding = 12 gap = 8 color = #1E1E1EF0
3204
+ textLabel T { size = (360, 80) text = "Styled" font = "font.google.Lobster" font_size = 40 }
3205
+ button B { size = (260, 70) text = "Ok" font = "font.google.Roboto_Mono" font_size = 24 }
3206
+ }
3207
+ }
3208
+ }
3209
+
3210
+ </example_saved_block_font>
3211
+
3212
+
3213
+ <example_stardew_valley>
3214
+ // Stardew-like Farming Prototype: engine inventory (item clauses + BindInventory hotbar with
3215
+ // engine-side drag/swap), tools, crops, resources, drops, selling, energy/day cycle.
3216
+ // Pattern notes:
3217
+ // - Items are `item` clauses; the hotbar is ONE empty hStack + BindInventory (engine spawns
3218
+ // synced draggable slots; SetInventorySelectable gives hotbar selection). No slot markup,
3219
+ // no per-slot refresh code, no hand-rolled drag: drops land in InventoryDropOnWorld.
3220
+ // - Multiplayer: shared farm world (crops/tools/drops are shared and per-player-interactable);
3221
+ // Bag is per-player (created in player OnSpawned).
3222
+ // Caveat: the gold/energy/day HUD and shop are single shared canvases (single-player-shaped UI).
3223
+ #set TileSize = 100
3224
+
3225
+ // Bare aliases (no target): auto-bound from the generated model / UI-image libraries by name.
3226
+ alias model.soil_tile
3227
+ alias model.cabbage
3228
+ alias model.carrot
3229
+ alias model.grape
3230
+ alias model.chest
3231
+ alias model.shop_counter
3232
+ alias model.flat_rock_slab
3233
+ alias model.trunk
3234
+ alias model.grass_rock_cliff
3235
+ alias model.green_maple_tree
3236
+ alias model.bed
3237
+ alias model.ground_item
3238
+ alias model.held_item
3239
+
3240
+ alias image.gold_icon
3241
+ alias image.energy_icon
3242
+ // One icon per item type (referenced by the item clauses below).
3243
+ alias image.hoe_icon
3244
+ alias image.parsnip_seeds_icon
3245
+ alias image.parsnip_icon
3246
+ alias image.watering_can_icon
3247
+ alias image.pickaxe_icon
3248
+ alias image.axe_icon
3249
+ alias image.scythe_icon
3250
+ alias image.fishing_rod_icon
3251
+ alias image.wood_icon
3252
+ alias image.stone_icon
3253
+ alias image.potato_seeds_icon
3254
+ alias image.bean_seeds_icon
3255
+ alias image.potato_icon
3256
+ alias image.bean_icon
3257
+
3258
+ // Item definitions: name/tip/icon show in the bound hotbar; stack= caps per-slot count;
3259
+ // value= is what the shipping bin pays (no value = not sellable).
3260
+ item hoe { name="Hoe" tip="Till green plots into soil." icon="alias.image.hoe_icon" tint=(0.7,0.7,0.9) }
3261
+ item watering_can { name="Watering Can" tip="Water planted crops to speed growth. Refill at the pond." icon="alias.image.watering_can_icon" tint=(0.2,0.55,1.0) }
3262
+ item pickaxe { name="Pickaxe" tip="Break rocks for stone." icon="alias.image.pickaxe_icon" tint=(0.65,0.65,0.65) }
3263
+ item axe { name="Axe" tip="Chop trees for wood." icon="alias.image.axe_icon" tint=(0.75,0.35,0.12) }
3264
+ item scythe { name="Scythe" tip="Harvest mature crops quickly." icon="alias.image.scythe_icon" tint=(0.55,1.0,0.55) }
3265
+ item fishing_rod { name="Fishing Rod" tip="A classic Stardew tool placeholder." icon="alias.image.fishing_rod_icon" tint=(0.7,0.45,0.2) }
3266
+ item parsnip_seeds { name="Parsnip Seeds" tip="Plant on tilled soil. Grows after watering/time." icon="alias.image.parsnip_seeds_icon" tint=(0.2,0.9,0.2) stack=99 value=2 }
3267
+ item potato_seeds { name="Potato Seeds" tip="Plant on tilled soil. Sells for more than parsnips." icon="alias.image.potato_seeds_icon" tint=(0.75,0.55,0.25) stack=99 }
3268
+ item bean_seeds { name="Bean Seeds" tip="Plant on tilled soil. High value crop." icon="alias.image.bean_seeds_icon" tint=(0.15,0.75,0.35) stack=99 }
3269
+ item parsnip { name="Parsnip" tip="Crop. Drag to the shipping bin to sell for 20g." icon="alias.image.parsnip_icon" tint=(1.0,0.85,0.15) stack=99 value=20 }
3270
+ item potato { name="Potato" tip="Crop. Drag to the shipping bin to sell for 35g." icon="alias.image.potato_icon" tint=(0.85,0.65,0.28) stack=99 value=35 }
3271
+ item bean { name="Bean" tip="Crop. Drag to the shipping bin to sell for 50g." icon="alias.image.bean_icon" tint=(0.15,0.9,0.4) stack=99 value=50 }
3272
+ item wood { name="Wood" tip="Basic crafting material. Drag to the ground to drop." icon="alias.image.wood_icon" tint=(0.45,0.22,0.08) stack=99 value=1 }
3273
+ item stone { name="Stone" tip="Basic crafting material. Drag to the ground to drop." icon="alias.image.stone_icon" tint=(0.55,0.55,0.55) stack=99 value=1 }
3274
+
3275
+ #class player
3276
+ var Money = 100
3277
+ var Energy = 100
3278
+ var MaxEnergy = 100
3279
+ var Water = 0
3280
+ var MaxWater = 20
3281
+ var Day = 1
3282
+ var Sleeping = false
3283
+
3284
+ var Bag: Object
3285
+ var LastSelected: Object
3286
+ var HeldVisual: Object
3287
+ var UseAnimFrames = 0
3288
+
3289
+ var StatsLabel: Object
3290
+ var EnergyLabel: Object
3291
+ var DayLabel: Object
3292
+ var SleepFade: Object
3293
+ var DayToast: Object
3294
+
3295
+ var UsedHoe = false
3296
+ var UsedSeeds = false
3297
+ var UsedWater = false
3298
+ var UsedPickaxe = false
3299
+ var UsedAxe = false
3300
+ var UsedScythe = false
3301
+ var UsedFishing = false
3302
+ var HasHarvested = false
3303
+ var SleepMoveAllowed = false
3304
+
3305
+ var ShopPanel: Object
3306
+ var ShopMessage: Object
3307
+
3308
+ func SelectedTool() -> Object {
3309
+ return Bag.ItemAt(Bag.GetSelectedIndex())
3310
+ }
3311
+
3312
+ func IsToolLike(it: Object) -> Bool {
3313
+ if it == Item.hoe or it == Item.watering_can or it == Item.pickaxe or it == Item.axe { return true }
3314
+ if it == Item.scythe or it == Item.fishing_rod { return true }
3315
+ if it == Item.parsnip_seeds or it == Item.potato_seeds or it == Item.bean_seeds { return true }
3316
+ return false
3317
+ }
3318
+
3319
+ func FirstTimeHint(it: Object) -> Text {
3320
+ if it == Item.hoe and not UsedHoe { return "Hoe: face a green plot, then click anywhere." }
3321
+ if it == Item.parsnip_seeds and not UsedSeeds { return "Seeds: click empty tilled soil to plant." }
3322
+ if it == Item.watering_can and not UsedWater { return "Watering Can: refill at pond, then face crops and click." }
3323
+ if it == Item.pickaxe and not UsedPickaxe { return "Pickaxe: click rocks to break them." }
3324
+ if it == Item.axe and not UsedAxe { return "Axe: click trees to chop wood." }
3325
+ if it == Item.scythe and not UsedScythe { return "Scythe: click mature crops to harvest." }
3326
+ if it == Item.fishing_rod and not UsedFishing { return "Fishing Rod: try it by the pond (F)." }
3327
+ return ""
3328
+ }
3329
+
3330
+ // Add with overflow feedback; the engine fills existing stacks first.
3331
+ func FarmAddItem(it: Object, count: Number) -> Bool {
3332
+ if it == None or count <= 0 { return false }
3333
+ var overflow = Bag.AddItem(it, count)
3334
+ if overflow > 0 {
3335
+ SpawnFloatingText("Inventory full", Vector(1, 1, 0))
3336
+ if overflow == count { return false }
3337
+ }
3338
+ return true
3339
+ }
3340
+
3341
+ // Remove one of the selected slot's item (seeds on planting).
3342
+ func ConsumeSelectedOne() -> Bool {
3343
+ var slot = Bag.GetSelectedIndex()
3344
+ var it = Bag.ItemAt(slot)
3345
+ var c = Bag.CountAt(slot)
3346
+ if it == None or c <= 0 { return false }
3347
+ Bag.SetAt(slot, it, c - 1)
3348
+ return true
3349
+ }
3350
+
3351
+ func RefreshHud() {
3352
+ var waterText = ""
3353
+ if SelectedTool() == Item.watering_can { waterText = "\n" + Water + "/" + MaxWater }
3354
+ StatsLabel.SetText(Money.ToText())
3355
+ EnergyLabel.SetText(Energy + "/" + MaxEnergy + waterText)
3356
+ DayLabel.SetText("Day " + Day)
3357
+ if SelectedTool() == Item.watering_can { SetHoverSPBar(Water, MaxWater) } else { ClearHoverBars() }
3358
+ }
3359
+
3360
+ func ShowPickupItem(it: Object) {
3361
+ SpawnFloatingText("+" + ItemName(it), ItemTint(it))
3362
+ }
3363
+
3364
+ func DropItemInFront(it: Object, count: Number) {
3365
+ var pos = GetPositionFront(70)
3366
+ var d = SpawnObject(SavedObject.groundItem, Position(pos.X, pos.Y, 70))
3367
+ d.ItemDef = it
3368
+ d.Count = count
3369
+ d.RefreshDrop()
3370
+ }
3371
+
3372
+ func BuySeed(it: Object, count: Number, cost: Number) {
3373
+ if Money < cost {
3374
+ ShopMessage.SetText("Not enough money")
3375
+ ShopMessage.SetUiColor(Vector(1, 0.1, 0.1))
3376
+ return
3377
+ }
3378
+ if FarmAddItem(it, count) {
3379
+ Money -= cost
3380
+ ShopMessage.SetText("Bought " + ItemName(it))
3381
+ ShopMessage.SetUiColor(Vector(0.3, 1, 0.3))
3382
+ SpawnFloatingText("Bought " + ItemName(it), ItemTint(it))
3383
+ RefreshHud()
3384
+ }
3385
+ }
3386
+
3387
+ event OnSpawned {
3388
+ SetCameraToTopDown()
3389
+ SetCameraZoom(900)
3390
+ SetCameraTiltDown(45)
3391
+ TeleportToTile(0, -4)
3392
+
3393
+ Bag = CreateInventory(12)
3394
+ Bag.AddItem(Item.hoe, 1)
3395
+ Bag.AddItem(Item.watering_can, 1)
3396
+ Bag.AddItem(Item.pickaxe, 1)
3397
+ Bag.AddItem(Item.axe, 1)
3398
+ Bag.AddItem(Item.scythe, 1)
3399
+ Bag.AddItem(Item.parsnip_seeds, 9)
3400
+ Bag.AddItem(Item.fishing_rod, 1)
3401
+ Scene.Hotbar.BindInventory(Bag) // engine spawns synced draggable slots
3402
+ Scene.Hotbar.SetInventorySelectable(true) // click a slot to equip it
3403
+
3404
+ StatsLabel = Scene.GoldText
3405
+ EnergyLabel = Scene.EnergyText
3406
+ DayLabel = Scene.DayText
3407
+ SleepFade = Scene.SleepFade
3408
+ DayToast = Scene.DayToast
3409
+ ShopPanel = Scene.ShopPanel
3410
+ ShopMessage = Scene.ShopMessage
3411
+ Scene.GoldIcon.SetUiImage(Ori.alias.image.gold_icon)
3412
+ Scene.EnergyIcon.SetUiImage(Ori.alias.image.energy_icon)
3413
+
3414
+ ShopPanel.SetVisible(false)
3415
+ SleepFade.SetVisible(false)
3416
+ SleepFade.SetUiOpacity(0)
3417
+ DayToast.SetVisible(false)
3418
+ HeldVisual = SpawnObject(SavedObject.heldItemVisual, GetPositionFront(80).Up(25))
3419
+ HeldVisual.SetVisible(false)
3420
+ RefreshHud()
3421
+
3422
+ forever {
3423
+ var dir = GetMoveVectorWorld()
3424
+ if not Sleeping or SleepMoveAllowed { MoveInDirectionContinuouslyFacing(dir, 350) }
3425
+
3426
+ var sel = SelectedTool()
3427
+ if sel != LastSelected {
3428
+ LastSelected = sel
3429
+ if sel != None { SpawnFloatingText("Equipped " + ItemName(sel), ItemTint(sel)) }
3430
+ RefreshHud()
3431
+ }
3432
+
3433
+ if sel != None and IsToolLike(sel) {
3434
+ HeldVisual.SetVisible(true)
3435
+ HeldVisual.SetColor(ItemTint(sel))
3436
+ var phase = 0
3437
+ var push = 0
3438
+ var lift = 18
3439
+ var scale = 24
3440
+ if UseAnimFrames > 0 {
3441
+ phase = UseAnimFrames
3442
+ if phase > 6 { phase = 12 - phase }
3443
+ push = phase * 5
3444
+ lift = 18 - phase * 3
3445
+ scale = 24 + phase * 12.7
3446
+ UseAnimFrames -= 1
3447
+ }
3448
+ if phase > 0 {
3449
+ HeldVisual.SetSize(scale, scale, 6)
3450
+ var area = GetPositionFront(32 + push)
3451
+ HeldVisual.SetPosition(Position(area.X, area.Y, lift.Max(0)))
3452
+ } else {
3453
+ HeldVisual.SetSize(24, 24, 24)
3454
+ HeldVisual.SetPosition(GetPositionFront(32).Up(18))
3455
+ }
3456
+ var hint = FirstTimeHint(sel)
3457
+ if hint != "" { Say(hint) } else { ClearSay() }
3458
+ } else {
3459
+ if HeldVisual.Exists() { HeldVisual.SetVisible(false) }
3460
+ ClearSay()
3461
+ }
3462
+
3463
+ var bed = NearestObjectWithTag(Tag.Bed)
3464
+ if bed.Exists() and SurfaceGapXYTo(bed) < 140 {
3465
+ bed.Say("Press E to sleep")
3466
+ } else {
3467
+ if bed.Exists() { bed.ClearSay() }
3468
+ }
3469
+ var counter = NearestObjectWithTag(Tag.ShopCounter)
3470
+ if counter.Exists() and SurfaceGapXYTo(counter) < 180 {
3471
+ counter.Say("Tap/use counter to buy seeds")
3472
+ } else {
3473
+ if counter.Exists() { counter.ClearSay() }
3474
+ }
3475
+ for plotHint in GetAllObjectsWithTag(Tag.Plot) {
3476
+ if not HasHarvested and plotHint.CropStage == 4 and SurfaceGapXYTo(plotHint) < 220 {
3477
+ plotHint.Say("Tap/use to harvest")
3478
+ } else {
3479
+ plotHint.ClearSay()
3480
+ }
3481
+ }
3482
+ }
3483
+ }
3484
+
3485
+ event ActionDownByThisPlayer[Primary] {
3486
+ if Scene.Hotbar.IsMouseOnUi() { return } // clicking the hotbar selects/drags, not uses
3487
+ UseAnimFrames = 12
3488
+ var sel = SelectedTool()
3489
+ var front = GetPositionFront(90)
3490
+ var readyPlot = front.NearestObjectWithTag(Tag.Plot)
3491
+ if readyPlot.Exists() and front.SurfaceGapXYTo(readyPlot) < 130 and readyPlot.CropStage == 4 {
3492
+ readyPlot.ApplyTool(Self)
3493
+ return
3494
+ }
3495
+ if sel == Item.hoe or sel == Item.watering_can or sel == Item.scythe or sel == Item.parsnip_seeds or sel == Item.potato_seeds or sel == Item.bean_seeds {
3496
+ var plot = front.NearestObjectWithTag(Tag.Plot)
3497
+ if plot.Exists() and front.SurfaceGapXYTo(plot) < 130 {
3498
+ plot.ApplyTool(Self)
3499
+ } else if sel == Item.hoe {
3500
+ SpawnFloatingText("Stand close and face a green plot", Vector(1, 1, 0))
3501
+ } else if sel == Item.watering_can {
3502
+ var pond = front.NearestObjectWithTag(Tag.Pond)
3503
+ if pond.Exists() and front.SurfaceGapXYTo(pond) < 125 {
3504
+ Water = MaxWater
3505
+ UsedWater = true
3506
+ SpawnFloatingText("Water refilled", Vector(0.2, 0.6, 1))
3507
+ RefreshHud()
3508
+ }
3509
+ }
3510
+ } else if sel == Item.pickaxe or sel == Item.axe {
3511
+ var res = front.NearestObjectWithTag(Tag.Resource)
3512
+ if res.Exists() and front.DistanceXYTo(res) < 85 { res.ApplyTool(Self) }
3513
+ }
3514
+ }
3515
+
3516
+ // Bound-slot drag released over the world: sell on the shipping bin, else drop at the feet.
3517
+ // The engine does not mutate the inventory here — the game decides (clear the slot ourselves).
3518
+ event InventoryDropOnWorld {
3519
+ if Event.DropObject.Exists() and Event.DropObject.HasTag(Tag.SellBox) {
3520
+ var value = ItemValue(Event.Item) * Event.Count
3521
+ if value > 0 {
3522
+ Money += value
3523
+ SpawnFloatingText("Sold for " + value + "g", Vector(1, 0.9, 0.1))
3524
+ Bag.SetAt(Event.SlotIndex, Event.Item, 0)
3525
+ RefreshHud()
3526
+ } else {
3527
+ SpawnFloatingText("That cannot be sold", Vector(1, 0.4, 0.2))
3528
+ }
3529
+ return
3530
+ }
3531
+ DropItemInFront(Event.Item, Event.Count)
3532
+ Bag.SetAt(Event.SlotIndex, Event.Item, 0)
3533
+ }
3534
+
3535
+ event ClickButton {
3536
+ var clicked = Event.ClickedButton
3537
+ if clicked == Scene.BuyParsnipButton { BuySeed(Item.parsnip_seeds, 5, 10) }
3538
+ if clicked == Scene.BuyPotatoButton { BuySeed(Item.potato_seeds, 5, 20) }
3539
+ if clicked == Scene.BuyBeanButton { BuySeed(Item.bean_seeds, 5, 30) }
3540
+ if clicked == Scene.CloseShopButton { ShopPanel.SetVisible(false); ShopMessage.SetText("") }
3541
+ }
3542
+
3543
+ event KeyboardDownByThisPlayer[F] {
3544
+ if SelectedTool() == Item.fishing_rod {
3545
+ UsedFishing = true
3546
+ SpawnFloatingText("Fishing coming soon", ItemTint(Item.fishing_rod))
3547
+ }
3548
+ }
3549
+
3550
+ event KeyboardDownByThisPlayer[E] {
3551
+ if Sleeping { return }
3552
+ var bed = NearestObjectWithTag(Tag.Bed)
3553
+ if bed.Exists() and SurfaceGapXYTo(bed) < 140 {
3554
+ bed.ClearSay()
3555
+ Sleeping = true
3556
+ SleepMoveAllowed = false
3557
+ SleepFade.SetVisible(true)
3558
+ DayToast.SetVisible(false)
3559
+ for i in 0..11 {
3560
+ SleepFade.SetUiOpacity(i / 10)
3561
+ Wait(0.1)
3562
+ }
3563
+ Day += 1
3564
+ Energy = MaxEnergy
3565
+ for plot in GetAllObjectsWithTag(Tag.Plot) {
3566
+ plot.NewDay()
3567
+ }
3568
+ RefreshHud()
3569
+ DayToast.SetText("Day " + Day)
3570
+ DayToast.SetVisible(true)
3571
+ SleepMoveAllowed = true
3572
+ for j in 0..11 {
3573
+ SleepFade.SetUiOpacity(1 - j / 10)
3574
+ Wait(0.1)
3575
+ }
3576
+ SleepFade.SetVisible(false)
3577
+ Wait(1.0)
3578
+ DayToast.SetVisible(false)
3579
+ SleepMoveAllowed = false
3580
+ Sleeping = false
3581
+ }
3582
+ }
3583
+ #end
3584
+
3585
+ #class FarmPlot
3586
+ var Tilled = false
3587
+ var CropStage = 0
3588
+ var CropAge = 0
3589
+ var CropDef: Object
3590
+ var Watered = false
3591
+ var CropVisual: Object
3592
+
3593
+ func CropColor(stage: Number) -> Vector {
3594
+ if CropDef == Item.potato {
3595
+ if stage == 1 { return Vector(0.25, 0.16, 0.06) }
3596
+ if stage == 2 { return Vector(0.55, 0.38, 0.18) }
3597
+ if stage == 3 { return Vector(0.70, 0.50, 0.22) }
3598
+ return Vector(0.82, 0.60, 0.25)
3599
+ }
3600
+ if CropDef == Item.bean {
3601
+ if stage == 1 { return Vector(0.05, 0.22, 0.08) }
3602
+ if stage == 2 { return Vector(0.10, 0.55, 0.18) }
3603
+ if stage == 3 { return Vector(0.10, 0.78, 0.24) }
3604
+ return Vector(0.12, 0.95, 0.35)
3605
+ }
3606
+ if stage == 1 { return Vector(0.16, 0.09, 0.04) }
3607
+ if stage == 2 { return Vector(0.30, 0.70, 0.25) }
3608
+ if stage == 3 { return Vector(0.55, 0.85, 0.22) }
3609
+ return Vector(1.0, 0.85, 0.25)
3610
+ }
3611
+
3612
+ func UpdateLook() {
3613
+ if not Tilled {
3614
+ SetColor(0.70, 0.90, 0.70)
3615
+ } else {
3616
+ if Watered { SetColor(0.55, 0.50, 0.45) } else { SetColor(1, 1, 1) }
3617
+ }
3618
+ SetSizeTiles(0.9, 0.9, 0.02)
3619
+
3620
+ if CropStage == 0 {
3621
+ if CropVisual.Exists() { CropVisual.Destroy() }
3622
+ } else {
3623
+ if not CropVisual.Exists() { CropVisual = SpawnObject(SavedObject.cropVisual, GetPosition().Up(8)) }
3624
+ CropVisual.SetPosition(GetPosition().Up(8))
3625
+ if CropDef == Item.potato { CropVisual.SetModel(Ori.alias.model.carrot) }
3626
+ else if CropDef == Item.bean { CropVisual.SetModel(Ori.alias.model.grape) }
3627
+ else { CropVisual.SetModel(Ori.alias.model.cabbage) }
3628
+ if CropStage == 1 {
3629
+ CropVisual.SetColor(CropColor(1))
3630
+ CropVisual.SetSize(28, 28, 10)
3631
+ } else if CropStage == 2 {
3632
+ CropVisual.SetColor(CropColor(2))
3633
+ CropVisual.SetSize(55, 55, 25)
3634
+ } else if CropStage == 3 {
3635
+ CropVisual.SetColor(CropColor(3))
3636
+ CropVisual.SetSize(65, 65, 45)
3637
+ } else {
3638
+ CropVisual.SetColor(CropColor(4))
3639
+ CropVisual.SetSize(75, 75, 65)
3640
+ }
3641
+ }
3642
+ }
3643
+
3644
+ event OnSpawned {
3645
+ AddTag(Tag.Plot)
3646
+ UpdateLook()
3647
+ }
3648
+
3649
+ func NewDay() {
3650
+ if CropStage > 0 and CropStage < 4 {
3651
+ if Watered {
3652
+ CropAge += 1
3653
+ CropStage = 1 + CropAge
3654
+ if CropStage > 4 { CropStage = 4 }
3655
+ }
3656
+ Watered = false
3657
+ UpdateLook()
3658
+ }
3659
+ }
3660
+
3661
+ func Harvest(p: Object) {
3662
+ if p.FarmAddItem(CropDef, 1) {
3663
+ if p.SelectedTool() == Item.scythe { p.UsedScythe = true }
3664
+ p.HasHarvested = true
3665
+ var harvested = CropDef
3666
+ CropStage = 0
3667
+ CropAge = 0
3668
+ Watered = false
3669
+ if p.SelectedTool() == Item.scythe { p.SpawnFloatingText("Scythed " + ItemName(harvested), Vector(1, 1, 0.2)) }
3670
+ else { p.SpawnFloatingText("Harvested " + ItemName(harvested), Vector(1, 1, 0.2)) }
3671
+ UpdateLook()
3672
+ }
3673
+ }
3674
+
3675
+ func ApplyTool(p: Object) {
3676
+ if SurfaceGapXYTo(p) > 120 {
3677
+ p.SpawnFloatingText("Too far", Vector(1, 1, 0))
3678
+ return
3679
+ }
3680
+ if CropStage == 4 {
3681
+ Harvest(p)
3682
+ return
3683
+ }
3684
+ var sel = p.SelectedTool()
3685
+ if sel == Item.hoe {
3686
+ if not Tilled {
3687
+ if p.Energy <= 0 { p.SpawnFloatingText("Too tired", Vector(1, 0, 0)); return }
3688
+ p.UsedHoe = true
3689
+ Tilled = true
3690
+ p.Energy -= 3
3691
+ p.SpawnFloatingText("Tilled soil", Vector(0.7, 0.4, 0.1))
3692
+ p.RefreshHud()
3693
+ UpdateLook()
3694
+ } else {
3695
+ p.SpawnFloatingText("Already tilled", Vector(1, 1, 1))
3696
+ }
3697
+ } else if sel == Item.parsnip_seeds or sel == Item.potato_seeds or sel == Item.bean_seeds {
3698
+ if Tilled and CropStage == 0 {
3699
+ CropDef = Item.parsnip
3700
+ if sel == Item.potato_seeds { CropDef = Item.potato }
3701
+ if sel == Item.bean_seeds { CropDef = Item.bean }
3702
+ if p.ConsumeSelectedOne() {
3703
+ p.UsedSeeds = true
3704
+ CropStage = 1
3705
+ CropAge = 0
3706
+ p.SpawnFloatingText("Planted seeds", Vector(0.2, 1, 0.2))
3707
+ UpdateLook()
3708
+ }
3709
+ } else {
3710
+ p.SpawnFloatingText("Seeds need empty tilled soil", Vector(1, 1, 0))
3711
+ }
3712
+ } else if sel == Item.watering_can {
3713
+ if p.Water <= 0 {
3714
+ p.SpawnFloatingText("Watering can empty. Refill at pond.", Vector(0.2, 0.6, 1))
3715
+ p.RefreshHud()
3716
+ return
3717
+ }
3718
+ if Tilled and CropStage < 4 {
3719
+ p.UsedWater = true
3720
+ Watered = true
3721
+ p.Water -= 1
3722
+ p.Energy -= 1
3723
+ p.SpawnFloatingText("Watered", Vector(0.2, 0.6, 1))
3724
+ p.RefreshHud()
3725
+ UpdateLook()
3726
+ } else {
3727
+ p.SpawnFloatingText("Nothing to water", Vector(1, 1, 1))
3728
+ }
3729
+ } else if sel == Item.scythe {
3730
+ p.SpawnFloatingText("Nothing mature here", Vector(1, 1, 1))
3731
+ } else {
3732
+ p.SpawnFloatingText("Equip hoe, seeds, watering can, or scythe", Vector(1, 1, 1))
3733
+ }
3734
+ }
3735
+ #end
3736
+
3737
+ #class ShippingBin
3738
+ event OnSpawned {
3739
+ AddTag(Tag.SellBox)
3740
+ SetColor(0.45, 0.25, 0.05)
3741
+ Say("Shipping Bin\nDrag crops here to sell")
3742
+ }
3743
+ #end
3744
+
3745
+ #class ShopCounter
3746
+ event ClickThisObject {
3747
+ var p = Event.Presser
3748
+ p.ShopPanel.SetVisible(true)
3749
+ p.ShopMessage.SetText("")
3750
+ }
3751
+ #end
3752
+
3753
+ #class ToolResource
3754
+ var HP = 3
3755
+ var IsTree = true // true: axe chops it for wood; false: pickaxe breaks it for stone
3756
+ var DropCount = 1
3757
+ var Label = "Resource"
3758
+
3759
+ event OnSpawned {
3760
+ AddTag(Tag.Resource)
3761
+ Say(Label)
3762
+ }
3763
+
3764
+ func ApplyTool(p: Object) {
3765
+ var needed = Item.pickaxe
3766
+ if IsTree { needed = Item.axe }
3767
+ if p.SelectedTool() != needed {
3768
+ p.SpawnFloatingText("Use " + ItemName(needed), Vector(1, 1, 1))
3769
+ return
3770
+ }
3771
+ if SurfaceGapXYTo(p) > 125 {
3772
+ p.SpawnFloatingText("Too far", Vector(1, 1, 0))
3773
+ return
3774
+ }
3775
+ HP -= 1
3776
+ if IsTree { p.UsedAxe = true } else { p.UsedPickaxe = true }
3777
+ p.Energy -= 2
3778
+ p.RefreshHud()
3779
+ SpawnFloatingText("-1", Vector(1, 1, 1))
3780
+ if HP <= 0 {
3781
+ var drop = Item.stone
3782
+ if IsTree { drop = Item.wood }
3783
+ if p.FarmAddItem(drop, DropCount) {
3784
+ p.ShowPickupItem(drop)
3785
+ } else {
3786
+ p.DropItemInFront(drop, DropCount)
3787
+ }
3788
+ Destroy()
3789
+ }
3790
+ }
3791
+
3792
+ event ClickThisObject {
3793
+ ApplyTool(Event.Presser)
3794
+ }
3795
+ #end
3796
+
3797
+ #class GroundItem
3798
+ var ItemDef: Object
3799
+ var Count = 1
3800
+
3801
+ func RefreshDrop() {
3802
+ if ItemDef == Item.parsnip { SetModel(Ori.alias.model.cabbage) }
3803
+ else if ItemDef == Item.wood { SetModel(Ori.alias.model.trunk) }
3804
+ else if ItemDef == Item.stone { SetModel(Ori.alias.model.flat_rock_slab) }
3805
+ else if ItemDef == Item.potato { SetModel(Ori.alias.model.carrot) }
3806
+ else if ItemDef == Item.bean { SetModel(Ori.alias.model.grape) }
3807
+ SetColor(ItemTint(ItemDef))
3808
+ Say(ItemName(ItemDef) + " x" + Count + "\nClick to pick up")
3809
+ }
3810
+
3811
+ event OnSpawned {
3812
+ AddTag(Tag.Pickup)
3813
+ SetSizeTiles(0.35, 0.35, 0.35)
3814
+ RefreshDrop()
3815
+ var startPos = GetPosition()
3816
+ var restPos = Position(startPos.X, startPos.Y, 9)
3817
+ forever {
3818
+ if GetPosition().DistanceTo(restPos) >= 2 { MoveTowardContinuous(restPos, 260) }
3819
+ var p = NearestPlayer()
3820
+ if p.Exists() and SurfaceGapXYTo(p) < 160 {
3821
+ Say(ItemName(ItemDef) + " x" + Count + "\nClick to pick up")
3822
+ } else {
3823
+ ClearSay()
3824
+ }
3825
+ }
3826
+ }
3827
+
3828
+ event ClickThisObject {
3829
+ var p = Event.Presser
3830
+ if p.FarmAddItem(ItemDef, Count) {
3831
+ p.SpawnFloatingText("Picked up " + ItemName(ItemDef), ItemTint(ItemDef))
3832
+ Destroy()
3833
+ } else {
3834
+ SpawnFloatingText("Inventory full", Vector(1, 0, 0))
3835
+ }
3836
+ }
3837
+ #end
3838
+
3839
+ #class StardewRoomBuilder
3840
+ event OnSpawned {
3841
+ var farmhouseWalls = BuildRoom(Position(-400, 400, 0), Vector(400, 300, 0), 20, 150, Vector(0, -1, 0), 400)
3842
+ for wall in farmhouseWalls {
3843
+ wall.SetColor(Vector(0.65, 0.35, 0.2))
3844
+ }
3845
+
3846
+ var shopWalls = BuildRoom(Position(500, 600, 0), Vector(500, 400, 0), 18, 150, Vector(0, -1, 0), 150)
3847
+ for wall in shopWalls {
3848
+ wall.SetColor(Vector(0.55, 0.30, 0.16))
3849
+ }
3850
+ }
3851
+ #end
3852
+
3853
+ material GroundMat { base = "StylizedGrass" }
3854
+ object ground {
3855
+ model = "cube"
3856
+ material = "GroundMat"
3857
+ position = (0, 0, -100)
3858
+ size = (10000, 10000, 100)
3859
+ }
3860
+ display ___StardewRoomBuilder___ : StardewRoomBuilder { visible = false }
3861
+
3862
+ // farm plots
3863
+ display plot_1_1 : FarmPlot { model="alias.model.soil_tile" position=(-100, 0, 1) }
3864
+ display plot_1_2 : FarmPlot { model="alias.model.soil_tile" position=(0, 0, 1) }
3865
+ display plot_1_3 : FarmPlot { model="alias.model.soil_tile" position=(100, 0, 1) }
3866
+ display plot_2_1 : FarmPlot { model="alias.model.soil_tile" position=(-100, 100, 1) }
3867
+ display plot_2_2 : FarmPlot { model="alias.model.soil_tile" position=(0, 100, 1) }
3868
+ display plot_2_3 : FarmPlot { model="alias.model.soil_tile" position=(100, 100, 1) }
3869
+ display plot_3_1 : FarmPlot { model="alias.model.soil_tile" position=(-100, 200, 1) }
3870
+ display plot_3_2 : FarmPlot { model="alias.model.soil_tile" position=(0, 200, 1) }
3871
+ display plot_3_3 : FarmPlot { model="alias.model.soil_tile" position=(100, 200, 1) }
3872
+
3873
+ object shipBin : ShippingBin {
3874
+ model="alias.model.chest"
3875
+ position_tiles=(3, 0)
3876
+ size_tiles=(0.8, 0.8, 0.8)
3877
+ }
3878
+
3879
+ object pond {
3880
+ model="alias.model.pond"
3881
+ position_tiles=(4, 3)
3882
+ size_tiles=(2, 1.5, 0.04)
3883
+ color=(0.1, 0.35, 0.8)
3884
+ tag=Tag.Pond
3885
+ }
3886
+ object farmhouseFloor {
3887
+ model="alias.model.farmhouse_floor"
3888
+ position_tiles=(-4, 4)
3889
+ size_tiles=(4, 3, 0.08)
3890
+ color=(0.45, 0.28, 0.16)
3891
+ }
3892
+ object bed {
3893
+ model="alias.model.bed"
3894
+ position_tiles=(-5, 4.6)
3895
+ size=(112, 80, 64)
3896
+ color=(0.2, 0.35, 0.9)
3897
+ tag=Tag.Bed
3898
+ }
3899
+ object treeA : ToolResource { model="alias.model.green_maple_tree" position_tiles=(-5, -2) height=500 color=(0.25,0.15,0.05) override IsTree=true override DropCount=3 override Label="Tree" }
3900
+ object treeB : ToolResource { model="alias.model.green_maple_tree" position_tiles=(5, -2) height=500 color=(0.25,0.15,0.05) override IsTree=true override DropCount=3 override Label="Tree" }
3901
+ object rockA : ToolResource { model="alias.model.flat_rock_slab" position_tiles=(-3, -2) size_tiles=(0.7,0.7,0.45) color=(0.45,0.45,0.45) override IsTree=false override DropCount=3 override Label="Rock" }
3902
+ object rockB : ToolResource { model="alias.model.grass_rock_cliff" position_tiles=(3, -2) size_tiles=(0.7,0.7,0.45) color=(0.45,0.45,0.45) override IsTree=false override DropCount=3 override Label="Rock" }
3903
+
3904
+ object shopFloor { model="alias.model.shop_floor" position_tiles=(5, 6) size_tiles=(5, 4, 0.08) color=(0.42, 0.30, 0.18) }
3905
+ object shopCounter : ShopCounter {
3906
+ model="alias.model.shop_counter"
3907
+ position=(563, 658, 73)
3908
+ rotation=(0, 0, -90)
3909
+ size=(80, 215, 130)
3910
+ tag=Tag.ShopCounter
3911
+ }
3912
+
3913
+ saved groundItem {
3914
+ object drop : GroundItem { model="alias.model.ground_item" size_tiles=(0.35, 0.35, 0.35) }
3915
+ }
3916
+ saved heldItemVisual {
3917
+ display heldItem { model="alias.model.held_item" size=30 visible=false }
3918
+ }
3919
+ saved cropVisual {
3920
+ display crop { model="alias.model.crop" size=(28,28,10) }
3921
+ }
3922
+
3923
+ screenCanvas FarmUI {
3924
+ image GoldIcon {
3925
+ offset = (20, 18)
3926
+ size = (30, 30)
3927
+ }
3928
+ image EnergyIcon {
3929
+ offset = (20, 51)
3930
+ size = (30, 30)
3931
+ }
3932
+ textLabel DayText {
3933
+ offset = (20, 96)
3934
+ text = "Day 1"
3935
+ color = (1, 1, 0.85)
3936
+ }
3937
+ // vStack auto-stacks the rows (centered); no size = hug to fit children. Center-anchored modal.
3938
+ vStack ShopPanel {
3939
+ visible = false
3940
+ anchor = (center, center)
3941
+ padding = 20
3942
+ gap = 12
3943
+ child_align_x = center
3944
+ color = #14100AF2
3945
+ corner_radius = 14
3946
+ border_width = 1
3947
+ border_color = #7A5A2EFF
3948
+ textLabel ShopTitle { size=(320, 30) align=(center, center) text="Seed Shop" color=(1, 1, 0.8) }
3949
+ button BuyParsnipButton { size=(300, 48) text="5 Parsnip Seeds - 10g" corner_radius=8 }
3950
+ button BuyPotatoButton { size=(300, 48) text="5 Potato Seeds - 20g" corner_radius=8 }
3951
+ button BuyBeanButton { size=(300, 48) text="5 Bean Seeds - 30g" corner_radius=8 }
3952
+ textLabel ShopMessage { size=(320, 24) align=(center, center) text="" color=(1, 0.1, 0.1) }
3953
+ button CloseShopButton { size=(140, 38) text="Close" corner_radius=8 }
3954
+ }
3955
+ // The whole hotbar: ONE empty hStack. BindInventory in player OnSpawned spawns a synced
3956
+ // draggable 88x88 slot (icon + count + selection highlight) per inventory slot.
3957
+ hStack Hotbar {
3958
+ anchor = (center, bottom)
3959
+ offset = (0, -6)
3960
+ color = (0.12, 0.08, 0.04)
3961
+ padding = 8
3962
+ gap = 4
3963
+ }
3964
+ textLabel GoldText {
3965
+ offset = (58, 22)
3966
+ text = "0"
3967
+ color = (1, 1, 1)
3968
+ }
3969
+ textLabel EnergyText {
3970
+ offset = (58, 55)
3971
+ text = "100/100"
3972
+ color = (1, 1, 1)
3973
+ }
3974
+ frame SleepFade {
3975
+ visible = false
3976
+ offset = (0, 0)
3977
+ size = (1600, 900)
3978
+ color = (0, 0, 0)
3979
+ }
3980
+ textLabel DayToast {
3981
+ visible = false
3982
+ offset = (725, 410)
3983
+ text = "Day 1"
3984
+ color = (1, 1, 1)
3985
+ }
3986
+ }
3987
+
3988
+ screenCanvas TutorialUI {
3989
+ textLabel HelpText {
3990
+ offset = (20, 130)
3991
+ text = """Move with WASD or mobile thumbstick
3992
+ Tap hotbar slots to equip
3993
+ Primary action uses the selected item
3994
+ Drag items out of slots to sell or drop them"""
3995
+ color = (1, 1, 1)
3996
+ }
3997
+ }
3998
+
3999
+ </example_stardew_valley>
4000
+
4001
+
4002
+ <example_texture_dsl_desert_forest_demo>
4003
+ // Procedural texture DSL demo: semi-realistic desert wall + forest ground.
4004
+
4005
+ #texture DesertWall
4006
+ tile_m = 2.0;
4007
+ let tile = pattern.tile(uv * vec2(6.0, 4.0), mortar: 0.065, seed: 3);
4008
+ let warp = noise.vec2(uv * 3.0, seed: 11, period: 3) * 0.18;
4009
+ let cells = pattern.voronoi(uv * vec2(12.0, 8.0) + warp, jitter: 0.82, seed: 7, period: vec2(12.0, 8.0));
4010
+ let cracks = stroke(cells.edge, width: 0.055, aa: 0.012);
4011
+ let grain = noise.ridged(uv * 72.0, octaves: 3, seed: 19, period: 72);
4012
+ let broad = noise.fbm(uv * vec2(6.0, 4.0), octaves: 4, seed: 23, period: vec2(6.0, 4.0));
4013
+ let chips = smoothstep(0.65, 0.95, noise.ridged(uv * 36.0, octaves: 3, seed: 29, period: 36)) * (1.0 - smoothstep(0.02, 0.18, tile.edge));
4014
+ let dust = levels(noise.fbm(uv * 5.0 + warp, octaves: 4, seed: 31, period: 5), black: 0.28, white: 0.86, gamma: 1.35);
4015
+ let block = contrast(sat(broad * 0.65 + grain * 0.25 + rand(tile.id, seed: 5) * 0.10), amount: 1.25, pivot: 0.45);
4016
+ let height = mix(-0.045, 0.11 + grain * 0.018 - chips * 0.06, tile.mask) - cracks * 0.07;
4017
+ let stone = ramp(block, [0.0: color("#7d664a"), 0.35: color("#a8875a"), 0.75: color("#c9a66f"), 1.0: color("#ead19c")]);
4018
+ let mortar = color("#75614b") * mix(0.82, 1.08, broad);
4019
+ out.color = mix(mortar, stone, tile.mask) * mix(0.82, 1.06, dust) - cracks * 0.12;
4020
+ out.normal = normal_from_height(height, strength: 1.05);
4021
+ out.orm = orm(occlusion: ao_from_height(height, radius: 2.5), roughness: mix(0.94, 0.82, tile.mask) + dust * 0.03, metallic: 0.0);
4022
+ #end
4023
+
4024
+ #texture ForestGround
4025
+ tile_m = 3.0;
4026
+ let p = uv * vec2(8.0, 8.0);
4027
+ let warp = noise.vec2(uv * 2.0, seed: 41, period: 2) * 0.25;
4028
+ let soil = levels(noise.fbm(p + warp, octaves: 4, seed: 43, period: vec2(8.0, 8.0)), black: 0.18, white: 0.88, gamma: 1.2);
4029
+ let stones = pattern.voronoi(uv * vec2(7.0, 7.0) + warp * 0.6, jitter: 0.78, seed: 47, period: vec2(7.0, 7.0));
4030
+ let stone_shape = 1.0 - smoothstep(0.34, 0.47, stones.dist + noise.fbm(stones.local * 4.0, octaves: 2, seed: 53) * 0.08);
4031
+ let cell_guard = smoothstep(0.025, 0.07, stones.edge);
4032
+ let stone_mask = stone_shape * cell_guard;
4033
+ let moss = levels(noise.fbm(uv * vec2(5.0, 5.0) + warp * 1.4, octaves: 4, seed: 59, period: 5) + (1.0 - stone_mask) * 0.25, black: 0.44, white: 0.92, gamma: 0.85);
4034
+ let wet = contrast(noise.fbm(uv * 3.0 + warp, octaves: 3, seed: 61, period: 3), amount: 1.55, pivot: 0.48);
4035
+ let height = soil * 0.045 + moss * 0.035 + sat(1.0 - stones.dist * 2.1) * stone_mask * 0.10 - (1.0 - soil) * 0.025;
4036
+ let soil_color = ramp(soil, [0.0: color("#211912"), 0.45: color("#3b2b1d"), 1.0: color("#6c5538")]);
4037
+ let moss_color = ramp(moss, [0.0: color("#26351d"), 0.55: color("#43622d"), 1.0: color("#7f9b55")]);
4038
+ let stone_color = ramp(rand(stones.id, seed: 67), [0.0: color("#3e4039"), 0.5: color("#68675b"), 1.0: color("#918a78")]);
4039
+ out.color = mix(mix(soil_color, moss_color, moss * 0.82), stone_color, stone_mask * 0.75);
4040
+ out.normal = normal_from_height(height, strength: 0.95);
4041
+ out.orm = orm(occlusion: ao_from_height(height, radius: 2.0), roughness: mix(0.92, 0.58, wet), metallic: 0.0);
4042
+ #end
4043
+
4044
+ object desert_wall {
4045
+ model = "cube"
4046
+ material = "DesertWall"
4047
+ position = (0, -240, 160)
4048
+ size = (700, 40, 320)
4049
+ }
4050
+
4051
+ object forest_ground {
4052
+ model = "cube"
4053
+ material = "ForestGround"
4054
+ position = (0, 180, -20)
4055
+ size = (900, 520, 40)
4056
+ }
4057
+
4058
+ </example_texture_dsl_desert_forest_demo>
4059
+
4060
+
4061
+ <example_texture_dsl_grunge_demo>
4062
+ // Procedural texture DSL demo: scalar mask morphology for grunge and worn painted metal.
4063
+
4064
+ #texture GrungeMask
4065
+ tile_m = 1.5;
4066
+ wrap = repeat;
4067
+ let p = uv * vec2(8.0, 8.0);
4068
+ let warp = noise.vec2(uv * vec2(3.0, 3.0), seed: 71, period: 3) * 0.35;
4069
+ let cloudy = levels(noise.fbm(p + warp, octaves: 5, seed: 73, period: vec2(8.0, 8.0)), black: 0.24, white: 0.82, gamma: 1.18);
4070
+ let pepper = levels(noise.ridged(uv * vec2(72.0, 72.0), octaves: 3, seed: 79, period: 72), black: 0.58, white: 0.94, gamma: 0.75);
4071
+ let streak = levels(noise.ridged(vec2(uv.x * 160.0, uv.y * 5.0), octaves: 2, seed: 83, period: vec2(160.0, 5.0)), black: 0.66, white: 0.93, gamma: 0.65);
4072
+ let pits = dilate(erode(sat(cloudy * 0.62 + pepper * 0.32 + streak * 0.22), radius: 1.0), radius: 1.5);
4073
+ let softened = blur(pits, radius: 1.25);
4074
+ out.color = vec3(softened, softened, softened);
4075
+ out.normal = normal_from_height(softened * 0.05, strength: 0.45);
4076
+ out.orm = orm(occlusion: 1.0 - softened * 0.18, roughness: mix(0.55, 0.95, softened), metallic: 0.0);
4077
+ #end
4078
+
4079
+ #texture WornPaintedMetal
4080
+ tile_m = 1.0;
4081
+ wrap = repeat;
4082
+ let p = uv * vec2(10.0, 10.0);
4083
+ let warp = noise.vec2(uv * vec2(4.0, 4.0), seed: 89, period: 4) * 0.28;
4084
+ let broad = noise.fbm(uv * vec2(5.0, 5.0) + warp, octaves: 4, seed: 97, period: 5);
4085
+ let cells = pattern.voronoi(uv * vec2(14.0, 14.0) + warp * 0.5, jitter: 0.74, seed: 101, period: 14);
4086
+ let crack = stroke(cells.edge, width: 0.035, aa: 0.010);
4087
+ let scratch = levels(noise.ridged(vec2(uv.x * 180.0, uv.y * 7.0), octaves: 2, seed: 103, period: vec2(180.0, 7.0)), black: 0.68, white: 0.96, gamma: 0.72);
4088
+ let chip_seed = levels(noise.ridged(p + warp, octaves: 4, seed: 107, period: 10), black: 0.62, white: 0.92, gamma: 0.85);
4089
+ let raw_wear = sat(chip_seed * 0.70 + scratch * 0.32 + crack * 0.55 - broad * 0.18);
4090
+ let wear = blur(dilate(erode(raw_wear, radius: 0.75), radius: 2.0), radius: 0.75);
4091
+ let rust = levels(noise.fbm(uv * vec2(18.0, 18.0) + warp, octaves: 3, seed: 109, period: 18), black: 0.30, white: 0.90, gamma: 1.35);
4092
+ let paint = color("#275f6f") * mix(0.70, 1.15, broad);
4093
+ let exposed = mix(color("#6f7473"), color("#8c4a24"), rust * 0.55);
4094
+ let height = broad * 0.035 - wear * 0.070 + scratch * 0.018;
4095
+ out.color = mix(paint, exposed, wear);
4096
+ out.normal = normal_from_height(height, strength: 0.95);
4097
+ out.orm = orm(occlusion: ao_from_height(height, radius: 2.0), roughness: mix(0.62, 0.92, wear), metallic: mix(0.02, 0.70, wear * (1.0 - rust * 0.55)));
4098
+ #end
4099
+
4100
+ object grunge_panel {
4101
+ model = "cube"
4102
+ material = "GrungeMask"
4103
+ position = (-260, 0, 80)
4104
+ size = (260, 40, 180)
4105
+ }
4106
+
4107
+ object worn_painted_metal_panel {
4108
+ model = "cube"
4109
+ material = "WornPaintedMetal"
4110
+ position = (180, 0, 80)
4111
+ size = (360, 40, 220)
4112
+ }
4113
+
4114
+ </example_texture_dsl_grunge_demo>
4115
+
4116
+
4117
+ <example_tree_species>
4118
+ // Curated #tree species gallery. Each recipe is a reference worth copying as a
4119
+ // starting point for that habit; keep the shape keys, adjust seed/size per scene.
4120
+ // Iterate look changes with preview_tree and watch the foliage_coverage stat.
4121
+
4122
+ // European beech: broad asymmetric dome from four overlapping crown lobes, a
4123
+ // trunk that persists partway into the crown (leader) then dissolves into forks,
4124
+ // slender bole, dense cluster cards.
4125
+ #tree Beech
4126
+ seed: 27
4127
+ mode: colonize
4128
+ leader: 62
4129
+ levels: 4
4130
+ children: 7
4131
+ segments: 7
4132
+ trunk_height: 1250
4133
+ trunk_radius: 34
4134
+ points: 1024
4135
+ length_pct: 76
4136
+ radius_pct: 46
4137
+ angle_deg: 46
4138
+ angle_jitter_deg: 20
4139
+ up_bias_pct: 26
4140
+ curve_jitter_pct: 16
4141
+ trunk_noise_pct: 4
4142
+ trunk_lean_deg: 3 // gentle bands: beeches curve, they don't lean hard
4143
+ trunk_sweep_pct: 16
4144
+ trunk_noise_scale_pct: 30 // wavelength as % of height: slow bends, not bark-beat raggedness
4145
+ leaves: 12
4146
+ leaf_size: 178
4147
+ droop_pct: 8
4148
+ root_flare_pct: 34
4149
+ crown_ellipsoid: 0, 0, 1020, 620, 590, 510
4150
+ crown_ellipsoid: -250, 80, 960, 400, 370, 360
4151
+ crown_ellipsoid: 240, -100, 1060, 420, 390, 400
4152
+ crown_ellipsoid: 40, 50, 1380, 380, 350, 300
4153
+ #end
4154
+
4155
+ // Grand open-grown oak: a dominant central trunk visible deep into the tiered
4156
+ // crown, three low limb masses ~120 degrees apart in plan (every viewing angle
4157
+ // shows a reaching limb - lobes on one axis collapse to a blob from the thin
4158
+ // side), offset upper lobes, real sky gaps. An irregular ellipsoid stack, not
4159
+ // one sphere. Open crowns like this read ~35-45% coverage; gaps are the character.
4160
+ #tree Oak
4161
+ seed: 5
4162
+ mode: colonize
4163
+ leader: 35
4164
+ levels: 4
4165
+ children: 6
4166
+ segments: 7
4167
+ trunk_height: 950
4168
+ trunk_radius: 44
4169
+ points: 1024
4170
+ length_pct: 80
4171
+ radius_pct: 52
4172
+ angle_deg: 55
4173
+ angle_jitter_deg: 28
4174
+ up_bias_pct: 22
4175
+ curve_jitter_pct: 26
4176
+ trunk_noise_pct: 7
4177
+ trunk_lean_deg: 5 // low bands: a real oak is never a plumb pole
4178
+ trunk_sweep_pct: 20 // one slow arc; seed picks C vs S
4179
+ trunk_gnarl_pct: 40 // lobed lower-bole silhouette for hero close-ups (display-only)
4180
+ leaves: 13
4181
+ leaf_size: 190
4182
+ droop_pct: 12
4183
+ root_flare_pct: 45
4184
+ crown_ellipsoid: 0, 0, 1120, 700, 660, 560
4185
+ crown_ellipsoid: 435, 115, 930, 480, 430, 340
4186
+ crown_ellipsoid: -320, 320, 900, 470, 420, 330
4187
+ crown_ellipsoid: -115, -435, 1000, 450, 410, 350
4188
+ crown_ellipsoid: -200, -190, 1500, 450, 410, 340
4189
+ crown_ellipsoid: 230, 180, 1620, 390, 360, 300
4190
+ #end
4191
+
4192
+ // Fir: the dense layered conifer - whorl rings of downswept shelf branches
4193
+ // (angle_deg > 90 + up_bias swoop) rendered as whole-branch frond ribbons,
4194
+ // clear bole below crown_base, trunk-only collider. crown_base ~120 instead
4195
+ // gives the ground-sweeping park look.
4196
+ #tree Fir
4197
+ seed: 11
4198
+ mode: whorl
4199
+ leaf_mode: frond
4200
+ levels: 2
4201
+ children: 12
4202
+ rings: 30
4203
+ fill: 10
4204
+ segments: 8
4205
+ trunk_height: 1400
4206
+ trunk_radius: 22
4207
+ trunk_noise_pct: 2
4208
+ trunk_lean_deg: 1 // conifer spires stand near-plumb; kill the default lean/sweep
4209
+ trunk_sweep_pct: 3
4210
+ curve_jitter_pct: 5
4211
+ angle_deg: 106
4212
+ up_bias_pct: 8
4213
+ length_pct: 18
4214
+ length_taper_pct: 70
4215
+ droop_pct: 34
4216
+ leaf_size: 200
4217
+ root_flare_pct: 25
4218
+ crown_base: 300
4219
+ collider_levels: 1
4220
+ #end
4221
+
4222
+ // Weeping willow: spiral limbs arch up (up_bias) then cascade (droop) carrying
4223
+ // gravity-hung strand curtains (leaf_mode weeping). Long falls come from
4224
+ // leaf_size (the curtain card side, cm) - ~300 reaches near the ground from a
4225
+ // stout mid-height fork.
4226
+ #tree Willow
4227
+ seed: 9
4228
+ mode: spiral
4229
+ leaf_mode: weeping
4230
+ levels: 3
4231
+ children: 10
4232
+ segments: 8
4233
+ trunk_height: 750
4234
+ trunk_radius: 46
4235
+ length_pct: 82
4236
+ radius_pct: 50
4237
+ angle_deg: 40
4238
+ angle_jitter_deg: 18
4239
+ up_bias_pct: 34
4240
+ curve_jitter_pct: 18
4241
+ trunk_noise_pct: 5
4242
+ trunk_lean_deg: 4
4243
+ trunk_sweep_pct: 25 // waterside willows arc; pairs with the cascading crown
4244
+ droop_pct: 85
4245
+ leaves: 20
4246
+ leaf_size: 300
4247
+ root_flare_pct: 48
4248
+ #end
4249
+
4250
+ // Umbrella-thorn acacia: crown_dome mushroom cap on a bare fanning scaffold. The
4251
+ // short trunk + high cap forces colonize to bridge with long limbs; the TWO bare
4252
+ // guide ellipsoids make them diverge early and fork again mid-way (a lone
4253
+ // overhead cap pulls one straight vertical reach instead - attraction averages
4254
+ // out). children raises the forks per node. leaf_base at the dome cut with a
4255
+ // short fade keeps every guide bare and the cap underside crisp and flat.
4256
+ #tree Acacia
4257
+ seed: 4
4258
+ mode: colonize
4259
+ children: 7
4260
+ trunk_height: 260
4261
+ trunk_radius: 36
4262
+ points: 1024
4263
+ length_pct: 80
4264
+ radius_pct: 60
4265
+ curve_jitter_pct: 18
4266
+ trunk_noise_pct: 6
4267
+ trunk_lean_deg: 8 // savanna wind-shaped bole; crown_follow keeps the cap on it
4268
+ leaves: 13
4269
+ leaf_size: 185
4270
+ leaf_base: 740
4271
+ leaf_base_fade: 60
4272
+ root_flare_pct: 35
4273
+ crown_ellipsoid: 0, 0, 520, 300, 300, 160
4274
+ crown_ellipsoid: 0, 0, 690, 460, 460, 90
4275
+ crown_dome: 0, 0, 750, 800, 800, 360
4276
+ #end
4277
+
4278
+ object Beech1 { model = "tree.Beech" position = (0, 0, 0) }
4279
+ object Oak1 { model = "tree.Oak" position = (2200, 0, 0) }
4280
+ object Fir1 { model = "tree.Fir" position = (4000, 0, 0) }
4281
+ object Willow1 { model = "tree.Willow" position = (5800, 0, 0) }
4282
+ object Acacia1 { model = "tree.Acacia" position = (8000, 0, 0) }
4283
+
4284
+ </example_tree_species>
4285
+
4286
+
4287
+ <example_vampire_survivor>
4288
+ // Vampire Survivor Lite
4289
+ // Player: movement, auto-attack, level-up choices
4290
+ // Multiplayer: co-op shared arena.
4291
+ // Enemies/gems target the nearest player; per-player auto-attack and leveling.
4292
+ // Caveat: the level-up panel (LevelUpUI) is a single shared canvas and gates all players' attacks while open.
4293
+ alias model.goblin = "ori.model.nattawut_rea_6383.goblina"
4294
+ alias model.fox = "ori.default.fox"
4295
+ alias model.raptor = "ori.default.raptor"
4296
+ alias model.dragon = "ori.default.dragon"
4297
+ alias model.exp_gem // bare: auto-binds a generated mesh from the library
4298
+ alias effect.fireball = "ori.effect.worapat_supa_4983.w_fireball_2821"
4299
+ alias effect.poisonball = "ori.effect.worapat_supa_4983.w_poisonball_6320"
4300
+ alias effect.magicball = "ori.effect.nuttametee_b_2523.w_magicbulle"
4301
+ alias effect.snowball = "ori.effect.worapat_supa_4983.w_snowball_7592"
4302
+ alias effect.fire_area = "ori.effect.worapat_supa_4983.w_fireaura"
4303
+ alias effect.snowball_burst = "ori.effect.akkarawut_sa_2573.w_snowballbu41"
4304
+
4305
+ #class player
4306
+ var HP = 500
4307
+ var MaxHP = 500
4308
+ var XP = 0
4309
+ var NextLevelXP = 5
4310
+
4311
+ var AttackCooldown = 1.0 // seconds
4312
+ var AttackDamage = 35 // base damage
4313
+
4314
+ var WeaponLevel = 1
4315
+ var LevelUpPanel: Object
4316
+ var Choices: List<Object>
4317
+
4318
+ event OnSpawned {
4319
+ SetCameraToTopDown()
4320
+ TeleportToTile(0, 0)
4321
+
4322
+ HP = MaxHP
4323
+ SetHoverHPBar(HP, MaxHP)
4324
+ LevelUpPanel = Scene.LevelUpPanel
4325
+ LevelUpPanel.SetVisible(false)
4326
+ Choices.Add(LevelUpPanel.FindChildUI("Choice1"))
4327
+ Choices.Add(LevelUpPanel.FindChildUI("Choice2"))
4328
+ Choices.Add(LevelUpPanel.FindChildUI("Choice3"))
4329
+
4330
+ var FireballTimer = Timer()
4331
+ var PoisonballTimer = Timer()
4332
+ var MagicballTimer = Timer()
4333
+ var FireFieldTimer = Timer()
4334
+
4335
+ forever {
4336
+ // movement
4337
+ var dir = GetMoveVectorWorld()
4338
+ MoveInDirectionContinuouslyFacing(dir, 400)
4339
+
4340
+ // auto-attack
4341
+ if not LevelUpPanel.IsVisible() {
4342
+ // Fireball shoot nearest
4343
+ if FireballTimer.IsDone() and WeaponLevel >= 1 {
4344
+ FireballTimer.StartCountDown(AttackCooldown)
4345
+ var target = NearestObjectWithTag(Tag.Monster)
4346
+ if target.Exists() {
4347
+ var b = SpawnObject(SavedObject.snowball, GetPosition())
4348
+ b.Target = target
4349
+ b.Damage = AttackDamage
4350
+ }
4351
+ }
4352
+ // Poisonball shoot faster, less damage
4353
+ if PoisonballTimer.IsDone() and WeaponLevel >= 2 {
4354
+ PoisonballTimer.StartCountDown(AttackCooldown * 0.65)
4355
+ var target = NearestObjectWithTag(Tag.Monster)
4356
+ if target.Exists() {
4357
+ var b = SpawnObject(SavedObject.poisonball, GetPosition())
4358
+ b.Target = target
4359
+ b.Speed = 500
4360
+ b.Damage = (AttackDamage * 0.5).Round()
4361
+ }
4362
+ }
4363
+ // Magicball shoot multiple
4364
+ if MagicballTimer.IsDone() and WeaponLevel >= 3 {
4365
+ MagicballTimer.StartCountDown(AttackCooldown * 3.0)
4366
+
4367
+ var picks = GetAllObjectsWithTag(Tag.Monster).GetRandomElements(5)
4368
+ for t in picks {
4369
+ var f = SpawnObject(SavedObject.magicball, GetPosition())
4370
+ f.Target = t
4371
+ f.Speed = 300
4372
+ f.Damage = AttackDamage
4373
+ }
4374
+ }
4375
+ // Fire field on impact: slow cooldown, persistent area damage
4376
+ if FireFieldTimer.IsDone() and WeaponLevel >= 4 {
4377
+ FireFieldTimer.StartCountDown(AttackCooldown * 5.5)
4378
+
4379
+ var target = NearestObjectWithTag(Tag.Monster)
4380
+ if target.Exists() {
4381
+ var s = SpawnObject(SavedObject.fireball, GetPosition())
4382
+ s.Target = target
4383
+ s.AreaObject = SavedObject.fireField
4384
+ }
4385
+ }
4386
+ }
4387
+
4388
+ // level-up check
4389
+ if not LevelUpPanel.IsVisible() and XP >= NextLevelXP {
4390
+ XP -= NextLevelXP
4391
+ NextLevelXP += 5
4392
+ LevelUpPanel.SetVisible(true)
4393
+ }
4394
+ }
4395
+ }
4396
+ // take damage
4397
+ event WasAttacked {
4398
+ HP -= Event.Damage
4399
+ SetHoverHPBar(HP, MaxHP)
4400
+ if HP <= 0 {
4401
+ SpawnFloatingText("YOU DIED!", Vector(1,0,0))
4402
+ Destroy()
4403
+ } else {
4404
+ SpawnFloatingText("-" + Event.Damage, Vector(1,0,0))
4405
+ }
4406
+ }
4407
+ // handle level-up via UI buttons
4408
+ event ClickButton {
4409
+ var choice = Choices.IndexOf(Event.ClickedButton) // -1 when not one of ours
4410
+ if choice == 0 {
4411
+ AttackCooldown = AttackCooldown * 0.9
4412
+ SpawnFloatingText("Cooldown -> " + AttackCooldown + " s", Vector(0, 1, 0))
4413
+ } else if choice == 1 {
4414
+ if WeaponLevel == 1 {
4415
+ SpawnFloatingText("Unlocked: Lv 2 Poison Orb", Vector(0, 1, 0))
4416
+ } else if WeaponLevel == 2 {
4417
+ SpawnFloatingText("Unlocked: Lv 3 Magic Orb", Vector(0, 1, 0))
4418
+ } else {
4419
+ SpawnFloatingText("Unlocked: Lv 4 Fire Field", Vector(0, 1, 0))
4420
+ }
4421
+ WeaponLevel = WeaponLevel + 1
4422
+ } else if choice == 2 {
4423
+ HP = (HP + 100).Min(MaxHP)
4424
+ SetHoverHPBar(HP, MaxHP)
4425
+ SpawnFloatingText("Healed! HP " + HP + "/" + MaxHP, Vector(0, 1, 0))
4426
+ } else {
4427
+ return
4428
+ }
4429
+ LevelUpPanel.SetVisible(false)
4430
+ }
4431
+ #end
4432
+
4433
+
4434
+ #class FireField
4435
+ var Radius = 150
4436
+ var Duration = 5.0
4437
+ var TickInterval = 0.5
4438
+ var DamagePerTick = 18
4439
+
4440
+ event OnSpawned {
4441
+ SnapToGroundWithTag(Tag.Ground)
4442
+ SetSize(Radius, Radius, 1)
4443
+ var LifeTimer = StartCountDownWithNewTimer(Duration)
4444
+ var TickTimer = Timer()
4445
+ forever {
4446
+ if LifeTimer.IsDone() { Destroy() }
4447
+ if TickTimer.IsDone() {
4448
+ TickTimer.StartCountDown(TickInterval)
4449
+ for m in GetObjectsWithinXYDistanceWithTag(Radius, Tag.Monster) {
4450
+ Attack(m, DamagePerTick)
4451
+ }
4452
+ }
4453
+ }
4454
+ }
4455
+ #end
4456
+
4457
+ #class Projectile
4458
+ var Target : Object
4459
+ var Speed = 800
4460
+ var Damage = 1 // set later
4461
+ var AreaObject: Object
4462
+ event OnSpawned {
4463
+ forever {
4464
+ if not Target.Exists() { Destroy() }
4465
+ else {
4466
+ MoveTowardContinuousFacing(Target.GetPosition(), Speed)
4467
+ if SurfaceGapTo(Target) < 25 {
4468
+ if AreaObject != None {
4469
+ SpawnEffectWithAutoDestroy(Ori.alias.effect.snowball_burst, GetPosition())
4470
+ SpawnObject(AreaObject, GetPosition())
4471
+ } else {
4472
+ Attack(Target, Damage)
4473
+ }
4474
+ Destroy()
4475
+ }
4476
+ }
4477
+ }
4478
+ }
4479
+ #end
4480
+
4481
+
4482
+ #class ExpGem
4483
+ var Value = 1
4484
+ var MagnetRange = 120
4485
+ var PullSpeed = 500
4486
+ var PickupRange = 40
4487
+ var TargetPlayer : Object
4488
+ var Pulled = false
4489
+
4490
+ event OnSpawned {
4491
+
4492
+ forever {
4493
+ if not Pulled {
4494
+ var p = NearestPlayer()
4495
+ if p.Exists() and SurfaceGapTo(p) < MagnetRange {
4496
+ Pulled = true
4497
+ TargetPlayer = p
4498
+ }
4499
+ } else {
4500
+ if not TargetPlayer.Exists() {
4501
+ TargetPlayer = NearestPlayer()
4502
+ if not TargetPlayer.Exists() { Pulled = false; continue }
4503
+ }
4504
+
4505
+ MoveTowardContinuousHorizontal(TargetPlayer.GetPosition(), PullSpeed)
4506
+
4507
+ if SurfaceGapTo(TargetPlayer) < PickupRange {
4508
+ TargetPlayer.XP += Value
4509
+ TargetPlayer.SpawnFloatingText("+XP", Vector(0,1,0))
4510
+ Destroy()
4511
+ }
4512
+ }
4513
+ }
4514
+ }
4515
+ #end
4516
+
4517
+
4518
+ #class HealPickup
4519
+ var HealAmount = 50
4520
+ var MagnetRange = 120
4521
+ var PullSpeed = 500
4522
+ var PickupRange = 40
4523
+ var TargetPlayer : Object
4524
+ var Pulled = false
4525
+
4526
+ event OnSpawned {
4527
+ forever {
4528
+ if not Pulled {
4529
+ var p = NearestPlayer()
4530
+ if p.Exists() and SurfaceGapTo(p) < MagnetRange {
4531
+ Pulled = true
4532
+ TargetPlayer = p
4533
+ }
4534
+ } else {
4535
+ if not TargetPlayer.Exists() {
4536
+ TargetPlayer = NearestPlayer()
4537
+ if not TargetPlayer.Exists() {
4538
+ Pulled = false
4539
+ continue
4540
+ }
4541
+ }
4542
+ MoveTowardContinuousHorizontal(TargetPlayer.GetPosition(), PullSpeed)
4543
+
4544
+ if SurfaceGapTo(TargetPlayer) < PickupRange {
4545
+ TargetPlayer.HP = (TargetPlayer.HP + HealAmount).Min(TargetPlayer.MaxHP)
4546
+ TargetPlayer.SpawnFloatingText("+HP", Vector(0,1,0))
4547
+ TargetPlayer.SetHoverHPBar(TargetPlayer.HP, TargetPlayer.MaxHP)
4548
+ Destroy()
4549
+ }
4550
+ }
4551
+ }
4552
+ }
4553
+ #end
4554
+
4555
+ #class MeleeEnemy
4556
+ var HP = 80
4557
+ var MaxHP = 80
4558
+ var Damage = 12
4559
+ var MeleeRange = 75
4560
+ var Speed = 70
4561
+ var AttackCooldown = 1.0
4562
+ var ExpDropCount = 1
4563
+ var DropHealChancePercent = 0
4564
+
4565
+ event OnPreSpawn {
4566
+ AddTag(Tag.Monster)
4567
+ }
4568
+ event OnSpawned {
4569
+ HP = MaxHP // in case of override
4570
+ SetHoverHPBar(HP, MaxHP)
4571
+
4572
+ var AttackTimer = Timer()
4573
+ forever {
4574
+ var p = NearestPlayer()
4575
+ if p.Exists() {
4576
+ MoveTowardContinuousHorizontalFacing(p.GetPosition(), Speed)
4577
+ if AttackTimer.IsDone() and SurfaceGapTo(p) < MeleeRange {
4578
+ AttackTimer.StartCountDown(AttackCooldown)
4579
+ PlayAttackAnimation()
4580
+ Attack(p, Damage)
4581
+ }
4582
+ }
4583
+ }
4584
+ }
4585
+ event WasAttacked {
4586
+ HP -= Event.Damage
4587
+ SpawnFloatingText("-" + Event.Damage, Vector(1,0,0))
4588
+ SetHoverHPBar(HP, MaxHP)
4589
+ if HP <= 0 {
4590
+ if RandomChancePercent(DropHealChancePercent) {
4591
+ SpawnObject(SavedObject.healPickup, GetPosition())
4592
+ }
4593
+ for i in 0..ExpDropCount {
4594
+ SpawnObject(SavedObject.expGem, GetPosition().ShiftX(i * 10))
4595
+ }
4596
+ Die()
4597
+ }
4598
+ }
4599
+ #end
4600
+
4601
+ #class EnemyManager
4602
+ var SpawnInterval = 2
4603
+ var MaxAlive = 30
4604
+ var DespawnDistance = 1500
4605
+
4606
+ event OnSpawned {
4607
+ var SpawnTimer = StartCountDownWithNewTimer(SpawnInterval)
4608
+ var DespawnTmr = StartCountDownWithNewTimer(2)
4609
+ forever {
4610
+ // Despawn monsters too far from every player
4611
+ if DespawnTmr.IsDone() {
4612
+ for m in GetAllObjectsWithTag(Tag.Monster) {
4613
+ var nearest = 999999
4614
+ for p in GetAllPlayers() {
4615
+ nearest = nearest.Min(m.DistanceTo(p))
4616
+ }
4617
+ if nearest > DespawnDistance { m.Destroy() }
4618
+ }
4619
+ DespawnTmr.StartCountDown(2)
4620
+ }
4621
+
4622
+ // Spawn new monsters but respect cap
4623
+ if SpawnTimer.IsDone() {
4624
+ var p = NearestPlayer()
4625
+ if p.Exists() {
4626
+ if MaxAlive - GetAllObjectsWithTag(Tag.Monster).Count() > 0 {
4627
+ var pos = p.GetPosition()
4628
+ .ShiftX(RandomNumber(-600,600))
4629
+ .ShiftY(RandomNumber(-600,600))
4630
+ var roll = RandomNumber(0,100)
4631
+ if roll < 30 { SpawnObject(SavedObject.goblin, pos) }
4632
+ else if roll < 60 { SpawnObject(SavedObject.raptor, pos) }
4633
+ else if roll < 90 { SpawnObject(SavedObject.fox , pos) }
4634
+ else { SpawnObject(SavedObject.dragon, pos) }
4635
+ }
4636
+ }
4637
+ SpawnTimer.StartCountDown(SpawnInterval)
4638
+ }
4639
+ }
4640
+ }
4641
+ #end
4642
+
4643
+ saved fireball {
4644
+ effect fireball : Projectile {
4645
+ effect = "alias.effect.fireball"
4646
+ }
4647
+ }
4648
+ saved poisonball {
4649
+ effect poisonball : Projectile {
4650
+ effect = "alias.effect.poisonball"
4651
+ }
4652
+ }
4653
+ saved magicball {
4654
+ effect magicball : Projectile {
4655
+ effect = "alias.effect.magicball"
4656
+ }
4657
+ }
4658
+ saved snowball {
4659
+ effect snowball : Projectile {
4660
+ effect = "alias.effect.snowball"
4661
+ }
4662
+ }
4663
+ saved fireField {
4664
+ effect fireField : FireField {
4665
+ effect = "alias.effect.fire_area"
4666
+ }
4667
+ }
4668
+ saved expGem {
4669
+ display gem : ExpGem { model="alias.model.exp_gem" size=14 color=(0,1,1) tag=Tag.ExpGem }
4670
+ }
4671
+ saved healPickup {
4672
+ display chicken : HealPickup { model="alias.model.heal_pickup" size=12 color=(1,0.8,0.3) tag=Tag.Item }
4673
+ }
4674
+ saved goblin {
4675
+ crowd goblin : MeleeEnemy {
4676
+ model = "alias.model.goblin",
4677
+ height = 50,
4678
+ override MaxHP = 80,
4679
+ override Damage = 12,
4680
+ override MeleeRange = 75,
4681
+ override Speed = 70,
4682
+ override AttackCooldown = 1.0
4683
+ }
4684
+ }
4685
+ saved fox {
4686
+ crowd fox : MeleeEnemy {
4687
+ model = "alias.model.fox"
4688
+ height = 60
4689
+ override MaxHP = 150
4690
+ override Damage = 8
4691
+ override MeleeRange = 60
4692
+ override Speed = 140
4693
+ override AttackCooldown = 0.8
4694
+ override DropHealChancePercent = 20
4695
+ override ExpDropCount = 2
4696
+ }
4697
+ }
4698
+ saved raptor {
4699
+ crowd raptor : MeleeEnemy {
4700
+ model = "alias.model.raptor"
4701
+ height = 70
4702
+ override MaxHP = 120
4703
+ override Damage = 20
4704
+ override MeleeRange = 80
4705
+ override Speed = 180
4706
+ override AttackCooldown = 0.8
4707
+ override DropHealChancePercent = 10
4708
+ override ExpDropCount = 2
4709
+ }
4710
+ }
4711
+ saved dragon {
4712
+ crowd dragon : MeleeEnemy {
4713
+ model = "alias.model.dragon"
4714
+ height = 200
4715
+ override MaxHP = 1000
4716
+ override Damage = 80
4717
+ override MeleeRange = 120
4718
+ override Speed = 50
4719
+ override AttackCooldown = 1.5
4720
+ override DropHealChancePercent = 100
4721
+ override ExpDropCount = 10
4722
+ }
4723
+ }
4724
+
4725
+ material GroundMat { base = "StylizedGrass" }
4726
+ object ground { model="cube" material="GroundMat" position=(0, 0, -100) size=(10000, 10000, 100) tag = Tag.Ground}
4727
+ display manager: EnemyManager { visible = false }
4728
+
4729
+ screenCanvas LevelUpUI {
4730
+ // vStack with no size hugs its children (200x260 here); children set explicit sizes. Center-anchored modal.
4731
+ vStack LevelUpPanel {
4732
+ anchor = (center, center)
4733
+ padding = 10
4734
+ gap = 10
4735
+ color = #1A1024F0
4736
+ corner_radius = 14
4737
+ border_width = 1
4738
+ border_color = #6A3FB0FF
4739
+ textLabel Title { size=(180, 30) align=(center, center) text="Leveled Up!" }
4740
+ button Choice1 { size=(180, 60) text="-10% Cooldown" corner_radius=10 }
4741
+ button Choice2 { size=(180, 60) text="+Elemental Orb" corner_radius=10 }
4742
+ button Choice3 { size=(180, 60) text="Heal 100 HP" corner_radius=10 }
4743
+ }
4744
+ }
4745
+ screenCanvas TutorialUI {
4746
+ textLabel Text {
4747
+ offset = (20, 80)
4748
+ text = "Move with WASD or mobile thumbstick"
4749
+ }
4750
+ }
4751
+
4752
+ </example_vampire_survivor>
4753
+
4754
+
2045
4755
 
2046
4756
  </world_description_examples>
2047
4757
 
@@ -2060,6 +4770,7 @@ Worlds may carry `#doc_version N` = the doc version they were last authored/upgr
2060
4770
  - Cannot do ClassName() to construct object
2061
4771
  - No emoticon in Text and comments
2062
4772
  - only use '//' for comments
4773
+ - Destroy() also ends the current function body (a value-returning func returns its type default); the caller keeps running this frame.
2063
4774
  - Object member calls (including Destroy()) on a None or already-despawned Object are runtime errors (a second Destroy() before the queued despawn runs is fine). Avoid spamming Exists(); prefer fail-fast. Use Exists() only when absence is expected (mouse/crosshair miss, destroyed mid-loop). Note: Object.Exists() is safe on None (returns false), so `o == None or not o.Exists()` is redundant; write `not o.Exists()`.
2064
4775
  - Reference a named scene object with Scene.X (e.g. Scene.Hotbar, Scene.EnemyBase.SetVisible(true)). It resolves once at world load; Exists() is false if no such object. Runtime-spawned objects are not reachable this way - keep the SpawnObject result in a var.
2065
4776
  - Never write saved_origin inside a saved clause (wrong: saved BrickBlock { object BrickBlock { saved_origin = "BrickBlock" } })
@@ -2077,10 +4788,10 @@ Weave2 is the only engine: a world with '#set Weave2 0' at the top refuses to lo
2077
4788
  - Remove the '#set Weave2 0' line, recompile, and fix diagnostics; the language is the same, typing is stricter.
2078
4789
  - A saved asset stored under Weave1 fails spawn with "saved clone VARS reference a retired Weave1 code chunk": open the world that owns it, fix its class diagnostics, and re-save the asset under Weave2.
2079
4790
  - W2C0605 unknown function, or W2C0614 whose message says the simcall is not in the allowlist: that simcall isn't ported to Weave2 (most have a ported replacement — see below). W2C0614 is also used for other type/usage errors; read the message, not just the code.
2080
- - "retired in Weave2" errors name the replacement in the message; apply it at the call site. Common ones: HasArrivedXY -> DistanceXYTo(goal) <= radius; RotateTowardContinuous(Horizontal) -> LookAtHorizontal or MoveTowardContinuousHorizontalFacing.
4791
+ - "retired in Weave2" errors name the replacement in the message; apply it at the call site. Common ones: HasArrivedXY -> DistanceXYTo(goal) <= radius; RotateTowardContinuous(Horizontal) -> LookAtHorizontal or MoveTowardContinuousHorizontalFacing; ResetLogic/ResetAllLogic -> your own re-init func (no engine call re-runs OnSpawned).
2081
4792
  - Never work around a missing or retired simcall by defining a local func with its name (e.g. an empty 'func SetCastShadow(enabled: Bool) { }'): it silently disables engine behavior and breaks again when the engine call changes. Migrate the call site, or stop and report the gap.
2082
4793
  - Over-time controllers (MoveToOverTime etc.) and Move*Continuously directional variants are retired: drive movement from an Update loop or MoveInDirectionContinuously.
2083
- - TeleportUp/Down/Left/Right/Forward/Backward, TeleportCenterTo, TeleportOnTopOf, TeleportGroupWithPivot are retired: use TeleportTo with a computed Position, e.g. TeleportTo(GetPosition().ShiftUp(300)) or TeleportTo(Base.GetPosition().ShiftUp(Base.GetSizeZ())).
4794
+ - TeleportUp/Down/Left/Right/Forward/Backward, TeleportCenterTo, TeleportOnTopOf, TeleportGroupWithPivot are retired: use TeleportTo with a computed Position, e.g. TeleportTo(GetPosition().ShiftUp(300)) or TeleportTo(Base.GetPosition().ShiftUp(Base.GetHeight())).
2084
4795
  - SetDisplayPosition/SetDisplayCenterPosition are legacy and DisplayObject-only (worldCanvas tags etc.); on any other receiver they hard-error at spawn. Prefer SetPosition/SetCenterPosition (any object); for a visual-only offset that leaves the collider in place, use SetRenderOffset.
2085
4796
  - Untyped 'var X: List' now infers element type; annotate 'List<T>' if inference errors (W2C0620/W2C0621). Lists can now hold Vector/Position/Timer/class types directly (un-unroll old parallel-array workarounds).
2086
4797
  - A migration pass must be behavior-preserving: port call sites 1:1 and verify, then do architecture cleanups (pooling, query changes, debug removal) as a separate pass.