|
| 1 | +<!-- WURST_AGENTS_TEMPLATE_VERSION: 2026-06-10 --> |
1 | 2 | # AGENTS.md - WurstScript Map Project Notes |
2 | 3 |
|
3 | 4 | WurstScript Warcraft III map project notes for editing `.wurst` code, dependencies, generated objects, tests, or map build logic. |
@@ -137,6 +138,83 @@ Common operators: `+`, `-`, `*`, `/`, `div`, `%`, `mod`, `and`, `or`, `not`, `== |
137 | 138 | let label = count == 1 ? "unit" : "units" |
138 | 139 | ``` |
139 | 140 |
|
| 141 | +## WurstScript Production Pitfalls |
| 142 | + |
| 143 | +These are recurring real-world Wurst/Warcraft III failure modes. Treat this section as a pre-edit checklist for any non-trivial Wurst change. |
| 144 | + |
| 145 | +### Closure capture is by value |
| 146 | + |
| 147 | +Wurst closures capture locals by value. If a closure assigns to a local from an outer scope, the outer local is not updated. |
| 148 | + |
| 149 | +Bug pattern: |
| 150 | + |
| 151 | +```wurst |
| 152 | +framehandle clicked = null |
| 153 | +dialog.build() -> |
| 154 | + clicked = textButton("OK", 0.08, 0.024) |
| 155 | +clicked.onClick() -> // clicked is still null outside the build closure |
| 156 | + doThing() |
| 157 | +``` |
| 158 | + |
| 159 | +Safer pattern: |
| 160 | + |
| 161 | +```wurst |
| 162 | +dialog.build() -> |
| 163 | + let clicked = textButton("OK", 0.08, 0.024) |
| 164 | + clicked.onClick() -> |
| 165 | + doThing() |
| 166 | +``` |
| 167 | + |
| 168 | +Use `reference(value)` only when a value really must be read or mutated across closure boundaries, and destroy the reference when the owner is done with it: |
| 169 | + |
| 170 | +```wurst |
| 171 | +let clickedRef = reference(null) |
| 172 | +dialog.build() -> |
| 173 | + clickedRef.val = textButton("OK", 0.08, 0.024) |
| 174 | +clickedRef.val.onClick() -> |
| 175 | + doThing() |
| 176 | +destroy clickedRef |
| 177 | +``` |
| 178 | + |
| 179 | +Prefer avoiding the cross-boundary mutable reference entirely when the handler can be registered inside the closure that creates the frame. |
| 180 | + |
| 181 | +### Object generation base IDs carry baggage |
| 182 | + |
| 183 | +Generated object-editor definitions must use real Warcraft III melee objects as base objects, not custom objects generated elsewhere in the map. Custom-object bases can compile into invalid or order-dependent object data. |
| 184 | + |
| 185 | +Because melee bases carry their own fields, always audit and intentionally clear inherited side effects when creating a generated unit, building, ability, upgrade, or item. Common inherited baggage includes: |
| 186 | + |
| 187 | +- repair gold/lumber costs and repair time |
| 188 | +- melee upgrades used / researches available / tech requirements |
| 189 | +- stock, dependency, bounty, collision, food, race, target, and classification fields |
| 190 | +- default abilities, autocast/order strings, buffs, art, missile, sound, and tooltip fields |
| 191 | + |
| 192 | +Prefer local helper presets that explicitly null known-dangerous inherited fields for each object family, then layer the intended fields afterwards. Regression tests for generated object config should assert the absence of known inherited side effects, not only the presence of the new feature. |
| 193 | + |
| 194 | +### Wurst object lifetime is manual |
| 195 | + |
| 196 | +Lua output is garbage-collected at the runtime level, but Wurst class lifetimes and destructors are still explicit. Objects created with `new`, closure/listener objects, timers/callbacks, references, collections, layout reports, and many helper wrappers usually need `destroy` when their owner is done. |
| 197 | + |
| 198 | +Do not rely on "Lua will GC it" if an `ondestroy` cleans up important state, callbacks, frame listeners, arrays, or nested objects. Conversely, do not double-destroy. Wurst instance ids can be reused, so a stale reference may point at a different future object and there is no reliable generic "is this destroyed?" check. Owners must clear stale references themselves after destroy: |
| 199 | + |
| 200 | +```wurst |
| 201 | +if watcher != null |
| 202 | + destroy watcher |
| 203 | + watcher = null |
| 204 | +``` |
| 205 | + |
| 206 | +### Table UI and layout dependencies |
| 207 | + |
| 208 | +If a project uses `wurst-table-layout` / `TableUi`, read that dependency's `AGENTS.md`, `AI_USAGE.md`, and `WC3_FRAMEHANDLE_GUIDE.md` before editing UI. Prefer the provided helpers over raw frame code. |
| 209 | + |
| 210 | +- Load TOC files in `init` when needed, but do not create, move, size, show/hide, reparent, or otherwise manipulate custom frames during blocking map-load init. Delay actual frame work with `doAfter(0.)` or later. |
| 211 | +- Build frames under their eventual parent (`withParent(...)` or `dialogFrame(...).build() ->`) rather than creating under a global parent and re-parenting later; WC3 can desync visual and clickable areas after `setParent`. |
| 212 | +- Keep root panels, dialogs, dropdowns, and sidecars in the 4:3 safe band with `placeSafe(...)` and declared dimensions. Do not size or place UI from `BlzGetLocalClientWidth()` / `BlzGetLocalClientHeight()` unless guarded against zero/invalid values; minimized clients can report unusable dimensions. |
| 213 | +- Avoid on-demand complex frame creation during gameplay when players may be alt-tabbed/minimized. Prefer creating reusable hidden frame trees after map load, then only owner-show/owner-hide/update them. |
| 214 | +- Do not move Blizzard default chat/message frames with arbitrary sizes/coords to make room for custom UI. Bad coordinates and default-frame refreshes can crash/desync; create map-owned UI in a safe area instead. |
| 215 | +- Register button handlers inside the same build callback that creates the button, or pass the button into a helper immediately. Do not assign a button/frame to an outer local inside a build callback and call `.onClick()` on that outer local afterwards. |
| 216 | +- Prefer table-wide defaults for repeated alignment, such as `layout.defaultHalign(Align.CENTER)`, instead of writing `..center()` on every row. Use per-row alignment calls only for exceptions. |
| 217 | +- Hide and reuse multiplayer UI frame trees. Do not destroy/recreate framehandles during gameplay cleanup. |
140 | 218 | ## Packages and API Shape |
141 | 219 |
|
142 | 220 | - Package members are private by default; use `public` for exports. |
@@ -182,7 +260,7 @@ doAfter(1.) -> |
182 | 260 | print("later") |
183 | 261 | ``` |
184 | 262 |
|
185 | | -Closures capture locals by value. Stored/object-backed closures often need cleanup. Lambdas used as `code` cannot take parameters or capture locals. |
| 263 | +Closures capture locals by value. Stored/object-backed closures often need cleanup. Use `reference(...)` for intentional cross-closure mutation, and destroy the reference when finished. Lambdas used as `code` cannot take parameters or capture locals. |
186 | 264 |
|
187 | 265 | ## Classes, Tuples, Generics |
188 | 266 |
|
@@ -223,7 +301,7 @@ Old `T` generics erase through integer casts and can share storage. |
223 | 301 |
|
224 | 302 | ## Compiletime and Objects |
225 | 303 |
|
226 | | -Use compiletime generation for object-editor data. Prefer wrappers and ID generators so IDs stay stable and collision-free. |
| 304 | +Use compiletime generation for object-editor data. Prefer wrappers and ID generators so IDs stay stable and collision-free. Generated objects must use melee base objects, then explicitly clear inherited fields that would create unwanted side effects. |
227 | 305 |
|
228 | 306 | ```wurst |
229 | 307 | let value = compiletime(fac(5)) |
|
0 commit comments