
Msw Ui System
- 4.2k installs
- 33 repo stars
- Updated July 29, 2026
- msw-git/msw-ai-coding-plugins-official
msw-ui-system is an agent skill that designs and mutates MapleStory Worlds .ui files through UIBuilder with component APIs, layout recipes, and mlua bindings.
About
MSW UI System is the single entry skill for MapleStory Worlds .ui design, component APIs, UIBuilder workflows, and runtime mlua patterns. It routes requests to references for anchors and RectTransform fundamentals, UIGroup hierarchy, component selection, layout recipes for HUD popups toasts menus and inventory grids, runtime patterns, and UI sound integration. The workflow clarifies intent, checks design guides, runs UIBuilder preflight, matches layout recipes, invokes msw_ui_builder.cjs, injects mlua UUID bindings, auto-lints on write, previews layouts, and refreshes Maker. Global rules forbid direct .ui JSON editing or shell reads, require anchoredPosition instead of Position, separate popups into their own UIGroup, enforce UpperLeft text alignment defaults, and mandate 88 by 88 minimum button touch targets with optional click and hover SFX. Scripts include ui_lint.cjs, preview_ui_layout.cjs, and ui_recipe.cjs scaffolding. Developers use it for any MSW UI mutation, anchor debugging, scroll lists, GridView patterns, and client-only runtime caveats where server scripts see nil UI components.
- Single entry for MSW .ui design, component API tables, UIBuilder workflows, and runtime patterns.
- All .ui reads and writes must go through UIBuilder, never raw JSON or grep.
- Layout recipes cover HUD, popup, toast, menu, inventory, and scroll list patterns.
- write() auto-runs ui_lint.cjs and supports injectBindings for mlua UUID defaults.
- Button touch targets must be at least 88x88 with optional ui-sound SFX wiring.
Msw Ui System by the numbers
- 4,191 all-time installs (skills.sh)
- +540 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #117 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 2, 2026 (Skillselion catalog sync)
msw-ui-system capabilities & compatibility
- Capabilities
- uibuilder mutations · anchor layout · component api · layout recipes · runtime patterns · binding injection · ui lint · touch targets
- Use cases
- frontend · ui design
What msw-ui-system says it does
Do not directly edit `.ui` JSON** — `.ui` creation/modification **must** go through `scripts/msw_ui_builder.cjs`.
Button touch target ≥ 88×88 (mobile support)
Separate popups and toasts into their **own UIGroup**, standalone show/hide
npx skills add https://github.com/msw-git/msw-ai-coding-plugins-official --skill msw-ui-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.2k |
|---|---|
| repo stars | ★ 33 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 29, 2026 |
| Repository | msw-git/msw-ai-coding-plugins-official ↗ |
How do I create or fix MSW UI layouts without breaking UUID consistency or anchor positioning rules?
Design, build, and mutate MapleStory Worlds .ui files through UIBuilder with anchors, components, recipes, and mlua runtime bindings.
Who is it for?
MSW developers building HUDs, popups, inventory grids, and scroll lists who must use the official UIBuilder workflow.
Skip if: Map .model or .tileset authoring, accessibility-focused UI patterns, or raw .ui JSON hand editing.
When should I use this skill?
Use for any MSW .ui mutation, anchor or pivot debugging, component property questions, layout recipes, or runtime mlua UI patterns.
What you get
Linted .ui files with correct anchors, component patches, mlua UUID bindings, and optional preview validation.
- .ui component definitions
- .mlua runtime UI scripts
- UUID binding injections
By the numbers
- Documents 9 core MSW UI components with full API tables
- Includes 6 layout recipes: HUD, popup, toast, menu, inventory, scroll-list
Files
msw-ui-system
MSW .ui single entry point — design guide + component API + builder invocation + runtime patterns bundled into one skill.
Role division with existing skills:
| Skill | Responsibility |
|---|---|
msw-ui-system (this skill) | Everything .ui — design (which/when/why), component API/enum (what), builder invocation (how to mutate), runtime mlua patterns. `.ui` mutations must always go through this skill's builder |
references/templates/ | Pre-built style bundles — .ui + ruid-map + button handler packages |
---
0. Routing
Branch to sub-references based on request keywords.
| Trigger | Reference Document |
|---|---|
| "anchor/pivot/coordinates/why is the position wrong", "RectTransform", "stretch" | `references/ui-fundamentals.md` §1–§8 |
| "mobile", "safe area", "1920", "MobileOnly", "ActivePlatform", "touch size", "PC reserved zone", "font size by device" | `references/ui-fundamentals.md` §9 |
| "UIGroup", "above popup", "z-order", "displayOrder", "CanvasGroup", "opacity propagation", "Enable vs Visible" | `references/ui-hierarchy.md`; for runtime sibling reorder also read `references/runtime-patterns.md` §7 |
| "which component", "Sprite vs Text vs Button", "9-slice", "scroll list", "GridView vs ScrollLayoutGroup" | `references/component-api.md` §"Component Selection Guide" |
| "make a HUD", "popup placement", "toast", "menu", "inventory grid", "scroll list" | `references/layout-recipes.md` |
| "connect .mlua after building with .ui builder", "property default UUID", "binding without drag" | `../msw-general/references/builder-protocol.md` §3.6 Binding Injection (unified entry point) |
Runtime UI component field read/write, component property name/type (ButtonComponent.Colors, TextComponent.Overflow, SpriteGUIRendererComponent.FillAmount…) | `references/component-api.md` required before every `.mlua` access to UI component fields |
Enum values (AlignmentType, OverflowType, ImageType, UIBasicParticleType…) | `references/component-api.md` §Enums |
| Runtime mlua patterns (popup open/close, toast fade, HP bar, GridView, drag, tab, cooldown), Runtime UI Caveats (client-only, server-side nil, etc.) | `references/runtime-patterns.md` |
| `.ui` builder invocation methods (UIBuilder API, anchor presets, write auto-lint, component add/patch/remove) | `../msw-general/references/builder-protocol.md` §3 UIBuilder (unified entry point — same document as .map MapBuilder / .model ModelBuilder) |
| "sound", "sfx", "click sound", "hover sound", "button audio", "PlaySound" | `references/ui-sound.md` |
---
1. Basic Workflow
(1) Clarify intent Layout sketch (ASCII or verbal) + which group to attach to
(2) Check design guide Match at least one of ui-fundamentals / ui-hierarchy / component-api §Component Selection Guide
(3) Builder Preflight Read ../msw-general/references/builder-protocol.md §3 (unified call-protocol entry point)
(4) Match recipe Select the closest template from layout-recipes.md
(5) Invoke builder Create/patch via scripts/msw_ui_builder.cjs (protocol: builder-protocol.md §3)
(6) Inject bindings Auto-inject .mlua property default UUIDs via b.write(path, { bind: {...} }) or b.injectBindings(...) (builder-protocol.md §3.6 Binding Injection)
(7) Self-verify write() auto-runs scripts/ui_lint.cjs (strict ON by default)
(8) Preview Visual check via scripts/preview_ui_layout.cjs
(9) Sound pass For any interactive button, offer click/hover SFX wiring (references/ui-sound.md)
(10) Maker Refresh Apply to engine2. Global Rules
NEVER
1. Do not directly edit `.ui` JSON — .ui creation/modification must go through scripts/msw_ui_builder.cjs. Manual editing breaks UUID·ValueType·@components consistency and causes silent drops. 2. Read existing `.ui` files through the builder too — Query via UIBuilder.read(filepath) / .find() / .listEntities(). Do not directly grep/parse raw JSON.
.uidirectReadand shell commands such ascat/type/Get-Content/rg/grep/sed/awk/cp/mvare blocked by the registered guard. UseUIBuilder.read/load/snapshotfor reads andb.write()for writes. Deleting an entire.uifile is a separate explicit deletion action, not a builder mutation.
3. Set Position directly — Use only anchoredPosition (Position is engine-managed) 4. Express size via OffsetMin/Max on fixed anchors (AnchorsMin == AnchorsMax) while also using anchoredPosition — Do not mix the two modes 5. Builder creates new UUIDs but .mlua property defaults are not updated — Binding breaks
ALWAYS
1. Builder Protocol Preflight — read [`../msw-general/references/builder-protocol.md`](../msw-general/references/builder-protocol.md) §3 every turn before any `.ui` mutation (UIBuilder API, write auto-lint, pos / anchor rules, binding injection, coverage gaps). It lives in the same document as .map MapBuilder / .model ModelBuilder — a unified entry point because the cross-flow is interlocked. 2. Check at least one design guide before invoking the builder (ui-fundamentals / ui-hierarchy / component-api §Component Selection Guide) 3. Match a recipe first; build from scratch only as a last resort 4. For edge placement use the formula: pos = ±(margin + size/2) 5. Separate popups and toasts into their own UIGroup, standalone show/hide 6. Verify text Alignment default is UpperLeft(0) — 95% of "I centered it but it sticks to the left" issues 7. Button touch target ≥ 88×88 (mobile support) 8. After creating any interactive button — proactively suggest wiring click/hover SFX via `references/ui-sound.md` (default UI SFX RUIDs available). Skip only if the user explicitly opts out or the button is purely decorative.
---
3. Sub-documents
- `references/ui-fundamentals.md` — Coordinate system, RectTransform 3 elements, anchor mode determination (§1–§8) + Resolution·safe area·PC reserved zones·touch targets·font sizes·platform separation (§9)
- `references/ui-hierarchy.md` — UIGroup / displayOrder / CanvasGroup / Enable vs Visible
- `references/component-api.md` — §"Component Selection Guide" (which/when/why) + full component property/method/event tables (what) + all UI-related enum values (§Enums)
- `references/layout-recipes.md` — Layout template collection
- `references/runtime-patterns.md` —
.mluaruntime patterns (popup/toast/HP/grid/drag…) + Runtime UI Caveats - `references/ui-sound.md` — UI sound integration (
_SoundService:PlaySound, click/hover hook, default UI SFX RUIDs) - `../msw-general/references/builder-protocol.md` §3 — `.ui` CJS builder call protocol (unified entry point) — same document as
.mapMapBuilder /.modelModelBuilder. panel / text / sprite / button / slider / scroll / script / group / mask / grid / avatar / touchReceive / skeleton / areaParticle / basicParticle, component CRUD, anchor presets, write auto-lint, and.mluaproperty UUID auto-binding all live in §3 + §3.6. - `references/templates/templates.md` — Pre-built style bundle index (
style-N-*.ui, `ruid-map.md`,Popupbutton.mlua)
4. Scripts
scripts/msw_ui_builder.cjs—.uibuilder core (UIBuilder class). Read `../msw-general/references/builder-protocol.md` §3 (unified entry point) before use.scripts/preview_ui_layout.cjs—.uilayout visual check + touch target warningsscripts/ui_lint.cjs—.uifile self-verification (auto-called bywrite())scripts/ui_recipe.cjs— Recipe-based scaffolding
---
Out of Scope
.map/.model/.tilesetbuilders — Outside this skill's scope.uiJSON schema (raw field shapes,@type/@componentswrapping, AlignmentOption 0–15 mapping, etc.) — Handled internally by the builder. Users/AI do not need to know directly- Accessibility patterns (alt text, screen-reader hints, focus order) — Not covered
- Error-state UI patterns (disabled-button styling beyond
Transition.Disabled, validation messages, loading spinners) — Not covered; design ad-hoc per project - Automated UI testing / layout assertions beyond
ui_lint.cjsandpreview_ui_layout.cjs— Not provided - Custom shader materials (
MaterialId) — Field is exposed but authoring shaders is outside this skill's scope
UI Component API Reference
Full list of properties, methods, and events per UI component. Use as a lookup when calling builder patchComponent(...) / addComponent(...), or when accessing a component from .mlua runtime code (property ButtonComponent btn = "uuid" → self.btn.Enable = false).
Before reading or writing a UI component field in .mlua, verify the exact field name here. Do not infer field names from Unity, UGUI, HTML, or other UI frameworks.
Authoring `.ui` files: this reference describes what fields exist. To set them, call the builder (scripts/msw_ui_builder.cjs; protocol in `../../msw-general/references/builder-protocol.md` §3 — unified call entry point). Do not hand-edit.uiJSON.
---
Component Selection Guide
Selection criteria (which/when/why). For exact field/method/event tables, jump to the component sections below.
Quick Decision Tree
What do you want to display?
├── Text → TextComponent
├── Image → SpriteGUIRendererComponent
├── Avatar → AvatarGUIRendererComponent
├── Input field → TextInputComponent
└── Group items → Empty Panel with only UITransform (arrange as children)
What do you want the user to interact with?
├── Simple click (including hover color change) → ButtonComponent
├── Press, drag, multi-touch → UITouchReceiveComponent
├── Slider → SliderComponent
├── Directional input (mobile) → JoystickComponent
└── Progress bar (display only) → SliderComponent(Interactable=false) or SpriteGUIRenderer(ImageType=Filled)
Do you need to display a list?
├── 10 or fewer items, simple → Manual placement + reuse empty Panels
├── Tens to hundreds, structured → ScrollLayoutGroupComponent
└── Thousands, performance-critical → GridViewComponent (virtualized)
Clipping or shape masking?
└── MaskComponent (e.g., circular avatar frame)
Opacity or grouped interaction control?
└── CanvasGroupComponentEntity selection priority:
- Colored or imaged background + centered text + click handling → use one
b.button(...)entity. - Use separate
b.sprite(...)+b.text(...)only when the text/background must be independent children, or when no click handling is needed. - Repeated same-shape tiles such as cards, board cells, inventory slots, and menu tabs should default to
b.button(...).
Choosing Between Similar Components
ButtonComponent vs UITouchReceiveComponent
| Criteria | ButtonComponent | UITouchReceiveComponent |
|---|---|---|
| Hover/pressed visual feedback | Automatic (via Transition settings) | Must implement manually |
| Event types | Click / StateChange / Pressed | Down/Up/Drag/Enter/Exit and 7 more |
| Drag | Not supported | Supported |
| Multi-touch | Not supported | Supported (distinguishes by TouchId) |
| Keyboard mapping | Supported (KeyCode) | Not supported |
How to choose:
- "Click a button to execute something" → Button
- "Drag to move an item" → UITouchReceive
- "Scroll a map" → UITouchReceive
- "Skill button that also triggers with keyboard R" → Button +
KeyCode
ScrollLayoutGroupComponent vs GridViewComponent
| Criteria | ScrollLayoutGroup | GridView |
|---|---|---|
| Item count | Up to hundreds | Unlimited (virtualized) |
| Render cost | Renders all items | Only renders visible items |
| Item composition | Place children directly in .ui | Clones a single ItemEntity template |
| Fixed size required | Only for Grid type | Always |
| Implementation difficulty | Easy | Requires OnRefresh callback |
Decision criteria: If the item count exceeds 100 or may grow dynamically, always use GridView. Inventory, chat logs, and rankings should default to GridView. Use ScrollLayoutGroup only for static lists of 10 or fewer items, such as settings page tabs.
SliderComponent vs SpriteGUIRendererComponent(Filled)
Both can express a "fill" effect, but they serve different purposes.
| Criteria | Slider | Sprite(Filled) |
|---|---|---|
| User interaction (drag) | Supported | Not supported |
| Value event | SliderValueChangedEvent | None |
| Direction | 4 directions | Horizontal/Vertical/Radial |
| Circular gauge | Not supported | Supported (Radial) |
| 9-slice support | Limited | Built-in |
How to choose:
- Volume/sensitivity control → Slider
- HP/MP bar (read-only) → Sprite(Filled Horizontal)
- Cooldown circular gauge → Sprite(Filled Radial)
- Experience bar → Sprite(Filled Horizontal) + Tween on value change
TextComponent — Alignment Pitfall ⚠️
The default value of TextComponent.Alignment is UpperLeft(0). A common mistake is assuming text is centered when it actually sticks to the upper-left. Always specify alignment explicitly:
- Title/centered text →
MiddleCenter(4) - Left-aligned description →
UpperLeft(0)orMiddleLeft(3) - Right-aligned numbers (e.g., scores) →
MiddleRight(5)
Additional considerations:
BestFit = true+MinSize/MaxSize→ Automatically adjusts font size to fit the Rect. Useful for handling text length variations across languages.Overflow:Truncate(1)clips text /Ellipsis(2)shows.../Overflow(0)lets text flow outside the RectSizeFit = true→ Rect automatically resizes to fit text length. Be careful with dynamic text + background Sprite (the background won't resize along with it)
SpriteGUIRenderer — ImageType Selection
| Type | Value | Use Case |
|---|---|---|
| Simple | 0 | Regular image. Stretching causes distortion |
| Sliced | 1 | 9-slice. Recommended for button/panel backgrounds |
| Tiled | 2 | Repeating pattern backgrounds |
| Filled | 3 | Gauges and cooldowns |
Button backgrounds, dialog backgrounds, and panels should almost always use Sliced. The sprite asset must have 9-slice borders configured for this to work.
Combination Patterns (Common Component Groupings)
Summary of component combinations to attach per entity. For builder call code and ASCII trees, see `layout-recipes.md`. For runtime code, see `runtime-patterns.md`.
| Pattern | Core Structure | Builder Recipe | Runtime Code |
|---|---|---|---|
| Button (icon + text) | Single entity with Sprite(Sliced) + Button. Separate Icon/Label as children — hover/pressed colors apply only to the background Sprite, keeping text stable | `layout-recipes.md` Recipe 1 | `runtime-patterns.md` §1, §6 |
| Clickable tile/card | Single b.button(...) entity with SpriteGUIRendererComponent + TextComponent + ButtonComponent; runtime changes use SpriteGUIRendererComponent.Color/ImageRUID and TextComponent.Text | `layout-recipes.md` Recipe 8 | `runtime-patterns.md` §1, §6 |
| HP/MP bar | Background Sprite + child Fill (stretch anchor, SpriteGUIRenderer Type=Filled, FillMethod=Horizontal, FillOrigin=Left) | `layout-recipes.md` Recipe 1 | `runtime-patterns.md` §3 (fillSprite.FillAmount = hp/maxHp — one line) |
| Avatar profile (circular) | Sprite(circular border) + Mask(Shape=Circle) + child AvatarGUIRenderer | — | — |
| Modal popup | Root: UITransform(stretch) + UIGroup(GroupType=2, Order=10) + CanvasGroup(BlocksRaycasts=true). Children: semi-transparent Dimmer (raycast=true, blocks input to HUD behind) + Panel(middle-center) | `layout-recipes.md` Recipe 2 | `runtime-patterns.md` §1 |
| Scroll list (~50 items) | ScrollLayoutGroup(Type=Vertical/Horizontal/Grid) + Mask(Shape=Rect). Children are auto-arranged | `layout-recipes.md` Recipe 6 | `runtime-patterns.md` §4, §8 |
| Large list (virtualized) | GridView + ItemEntity = reference to child template entity + OnRefresh = fn(index, entity). Template entity has enable=False. | `layout-recipes.md` Recipe 5 | `runtime-patterns.md` §5 |
GridView caution — OnRefresh is called frequently during scrolling. Do not call DataStorage; read only from cached tables.Rarely Used Components
| Component | When to Use |
|---|---|
JoystickComponent | Mobile movement controls only. Not needed for PC-only games |
ChatComponent | When using the MSW built-in chat UI. For custom chat, use a Text+Input combination |
UILogic methods | World↔UI coordinate conversion. Needed for damage floating text and nametags |
| Individual element alpha | Do not use. For group-level fading, use CanvasGroup.GroupAlpha |
Component Attachment Checklist
When creating a new entity:
- [ ]
UITransformComponentis required (for all UI entities) - [ ] If it's a root, add
UIGroupComponent + CanvasGroupComponent - [ ] If it's an image, add
SpriteGUIRendererComponentand setImageRUID(invisible if left empty) - [ ] If it's a button, add
ButtonComponent+ background Sprite on the same entity; Label as a child - [ ] If it's a list, decide the scroll type first (hundreds or more → GridView)
- [ ] To block input, use
CanvasGroup.InteractableorBlocksRaycasts - [ ] If it's text, specify
Alignmentexplicitly (defaultUpperLeftis a common pitfall)
---
UITransformComponent
Manages position, size, anchors, rotation, and scale. Required on every UI entity.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
anchoredPosition | Vector2 | (0, 0) | Offset relative to the anchor (use only this for UI positioning) |
RectSize | Vector2 | (100, 100) | UI size |
AlignmentOption | AlignmentType | Center(0) | Anchor preset (0~15; see `ui-fundamentals.md` §6 for full mapping) |
AnchorsMin | Vector2 | (0.5, 0.5) | Bottom-left anchor (normalized) |
AnchorsMax | Vector2 | (0.5, 0.5) | Top-right anchor (normalized) |
OffsetMin | Vector2 | (0.5, 0.5) | Offset relative to AnchorsMin |
OffsetMax | Vector2 | (0.5, 0.5) | Offset relative to AnchorsMax |
Pivot | Vector2 | (0.5, 0.5) | Reference for rotation / scale |
UIScale | Vector3 | (1, 1, 1) | Scale |
UIRotation | Vector3 | (0, 0, 0) | Euler-angle rotation |
UIMode | UIModeType | None(0) | Screen(1) or World(2) |
Position | Vector3 | (0, 0, 0) | Coordinates relative to parent (do not set directly in UI) |
WorldPosition | Vector3 | -- | World coordinates (read-only) |
ActivePlatform | PlatformType | All | Active platform |
Methods
| Method | Returns | Description |
|---|---|---|
Rotate(float angle) | void | Counterclockwise rotation |
Translate(float deltaX, float deltaY) | void | Relative translation |
ToWorldPoint(Vector3 local) | Vector3 | Local -> world coordinate conversion |
ToLocalPoint(Vector3 world) | Vector3 | World -> local coordinate conversion |
ToWorldDirection(Vector3 local) | Vector3 | Local -> world direction conversion |
ToLocalDirection(Vector3 world) | Vector3 | World -> local direction conversion |
GetWorldCorners() | Vector2[] | World coordinates of the rectangle's four corners (BL, TL, TR, BR) |
---
UIGroupComponent
Defines a UI screen (group). Attach to the root entity.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
DefaultShow | boolean | true | Whether to show at start |
GroupOrder | int32 | 0 | Z order (higher is on top) |
GroupType | UIGroupType | UIType(2) | DefaultType(1), UIType(2) |
---
CanvasGroupComponent
Controls the group's overall transparency and interaction.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
GroupAlpha | float | 1.0 | Transparency including children (0-1) |
Interactable | boolean | true | Whether to respond to input |
BlocksRaycasts | boolean | true | Block touches on UI behind |
---
Commonly Mistaken Unity Analogs
| Intended action | Do not assume | MSW field / pattern |
|---|---|---|
| Disable a specific UI component or button | Interactable on ButtonComponent | ButtonComponent.Enable = false for the component, or Entity.Enable = false for the whole entity/tree |
| Disable a whole popup/panel/tree | gameObject.SetActive(...) / isActive | Entity.Enable = false / true |
| Block or allow interaction for a group | ButtonComponent.Interactable | CanvasGroupComponent.Interactable and CanvasGroupComponent.BlocksRaycasts |
| Change text string | text | TextComponent.Text |
| Change text color | color | TextComponent.FontColor |
| Change sprite tint | color | SpriteGUIRendererComponent.Color |
Enable is inherited from the base component and is valid on UI components. Interactable is a CanvasGroupComponent property, not a ButtonComponent property.
---
ButtonComponent
Interactive button. Supports state-transition effects.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Transition | TransitionType | ColorTint(1) | None(0), ColorTint(1), SpriteSwap(2) |
Colors | TransitionColorSet | -- | Per-state colors (Normal/Highlighted/Pressed/Selected/Disabled) |
ImageRUIDs | TransitionRUIDSet | -- | Per-state images (when SpriteSwap) |
KeyCode | KeyboardKey | -- | Keyboard binding |
OrderInLayer | int32 | 0 | Render priority |
OverrideSorting | boolean | false | Whether to manually use SortingLayer / OrderInLayer |
Selectable | boolean | true | Whether the selected state can be maintained |
SortingLayer | string | "UI" | Render layer |
Events
| Event | Description |
|---|---|
ButtonClickEvent | Click (carries Entity property) |
ButtonStateChangeEvent | State change (state: ButtonState) |
ButtonPressedEvent | Enter pressed state |
---
TextComponent
Displays text. Supports font, alignment, overflow, drop shadow, and outline.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Text | string | "" | Text to display |
FontSize | int32 | 14 | Font size |
FontColor | Color | white | Text color |
Font | FontType | Default(0) | Default(0), Maple(1), Bazzi(2), Football(3) |
Alignment | TextAlignmentType | UpperLeft(0) | 9 alignment options — ⚠️ default sticks to upper-left; see §"Component Selection Guide → TextComponent — Alignment Pitfall" |
Bold | boolean | false | Bold |
IsRichText | boolean | false | Rich-text support |
Overflow | OverflowType | Overflow(0) | Overflow(0), Truncate(1), Ellipsis(2) |
BestFit | boolean | false | Auto-fit size |
MinSize | int32 | 10 | BestFit minimum size |
MaxSize | int32 | 40 | BestFit maximum size |
LineSpacing | float | 1.0 | Line spacing |
Padding | RectOffset | 0,0,0,0 | Inner padding |
SizeFit | boolean | false | Auto-fit to content size |
DropShadow | boolean | false | Drop shadow |
DropShadowColor | Color | -- | Shadow color |
DropShadowDistance | float | -- | Shadow distance |
DropShadowAngle | float | -- | Shadow angle |
UseOutLine | boolean | false | Outline |
OutlineColor | Color | -- | Outline color |
OutlineWidth | float | -- | Outline thickness |
IsLocalizationKey | boolean | false | Treat Text as a locale key (looked up at runtime) |
AllowAutomaticTranslation | boolean | true | Enable automatic translation while playing |
UseConstraintX | boolean | false | Constrain text width to ConstraintX |
ConstraintX | float | 100 | Max text width when UseConstraintX |
UseConstraintY | boolean | false | Constrain text height to ConstraintY |
ConstraintY | float | 100 | Max text height when UseConstraintY |
Methods
| Method | Returns | Description |
|---|---|---|
GetLocalizedText() | string | Text in the current language |
GetPreferredHeight(string text, float width) | float | Compute required height |
GetPreferredWidth(string text) | float | Compute required width |
---
SpriteGUIRendererComponent
Renders 2D images / sprites.
See themsw-sprite-ruidskill forImageRUIDnative type support (sprite/animationclip),animationclipanimated UI, and thethumbnail://prefix for renderingskeleton/avataritemthumbnails (especially useful for avatar item icons).
Properties
| Name | Type | Default | Description |
|---|---|---|---|
ImageRUID | DataRef | -- | Image resource reference |
Color | Color | white | Tint color |
Type | ImageType | Simple(0) | Simple(0), Sliced(1), Tiled(2), Filled(3) |
FillAmount | float | 1.0 | Fill amount (Filled type, 0-1) |
FillMethod | FillMethodType | Horizontal(0) | Fill direction |
FillOrigin | int32 | 0 | Fill origin |
FillClockWise | boolean | true | Clockwise fill |
FlipX | boolean | false | Flip horizontally |
FlipY | boolean | false | Flip vertically |
RaycastTarget | boolean | true | Receive touch / click |
PlayRate | float | 1.0 | Animation speed |
StartFrameIndex | int32 | 0 | Animation start frame |
EndFrameIndex | int32 | -1 | Animation end frame |
OrderInLayer | int32 | 0 | Render priority |
PreserveAspect | boolean | false | Lock image to its native aspect ratio |
MaterialId | string | "" | Custom material id (advanced shader effects) |
Methods
| Method | Returns | Description |
|---|---|---|
SetAlpha(float alpha) | void | Set transparency |
SetNativeSize() | void | Reset to native size |
ChangeMaterial(string materialId) | void | Apply a material |
Events
| Event | Description |
|---|---|
SpriteGUIAnimPlayerStartEvent | Animation start |
SpriteGUIAnimPlayerChangeFrameEvent | Frame change |
SpriteGUIAnimPlayerEndEvent | Animation end |
---
ScrollLayoutGroupComponent
Scrollable list / grid layout.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Type | LayoutGroupType | Vertical(1) | Horizontal(0), Vertical(1), Grid(2) |
Spacing | float | 0 | Item spacing (H/V) |
GridSpacing | Vector2 | (0, 0) | Item spacing (Grid) |
Padding | RectOffset | 0,0,0,0 | Outer padding |
CellSize | Vector2 | (100, 100) | Fixed item size (Grid) |
ConstraintCount | int32 | 0 | Fixed row / column count |
ScrollBarVisible | ScrollBarVisibility | AlwaysShow(0) | AlwaysShow(0), AutoHide(1), Hide(2) |
ScrollBarThickness | float | 20.0 | Scrollbar thickness |
ScrollBarHandleColor | Color | (0.5, 0.5, 0.5, 1) | Handle color |
ScrollBarHandleImageRUID | DataRef | -- | Handle image |
ScrollBarBackgroundColor | Color | (1, 1, 1, 0.4) | Background color |
ScrollBarBgImageRUID | DataRef | -- | Background image (note: NOT ScrollBarBackgroundImageRUID) |
HorizontalScrollBarDirection | HorizontalScrollBarDirection | LeftToRight(0) | Horizontal scrollbar direction |
VerticalScrollBarDirection | VerticalScrollBarDirection | BottomToTop(2) | Vertical scrollbar direction |
ChildAlignment | ChildAlignmentType | UpperLeft(0) | Child alignment for Horizontal/Vertical types |
ReverseArrangement | boolean | false | Reverse child order for Horizontal/Vertical types |
GridChildAlignment | ChildAlignmentType | UpperLeft(0) | Child alignment for Grid type |
StartCorner | GridLayoutCorner | UpperLeft(0) | Grid start corner |
StartAxis | GridLayoutAxis | Horizontal(0) | Grid child add direction |
Constraint | GridLayoutConstraint | Flexible(0) | Grid constraint mode |
Methods
| Method | Returns | Description |
|---|---|---|
GetScrollNormalizedPosition() | Vector2 | Current scroll position (0-1) |
SetScrollNormalizedPosition(UITransformAxis, float) | void | Set scroll position |
SetScrollPositionByItemIndex(int32) | void | Scroll to an item |
ResetScrollPosition(UITransformAxis) | void | Reset to initial position |
Events
| Event | Description |
|---|---|
ScrollPositionChangedEvent | On scroll (NormalizedPosition: Vector2) |
---
GridViewComponent
Virtualization for large lists. Renders only items visible on screen.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
ItemEntity | Entity | -- | Clone template |
TotalCount | int32 | 0 | Total item count |
CellSize | Vector2 | (100, 100) | Item size |
FixedCount | int32 | 1 | Fixed row / column count |
FixedType | GridViewFixedType | ColumnCountFixed(0) | Fixed axis |
Spacing | Vector2 | (0, 0) | Item spacing |
Padding | RectOffset | 0,0,0,0 | Outer padding |
UseScroll | boolean | true | Enable scrolling |
OnRefresh | func<int32, Entity> | -- | Item-display callback |
OnClear | func<int32, Entity> | -- | Item-hide callback |
Methods
| Method | Returns | Description |
|---|---|---|
Refresh(boolean resetPos, boolean force) | void | Full refresh |
RefreshIndex(int32 index) | void | Refresh a specific item |
SetScrollPositionByItemIndex(int32) | void | Scroll to an item |
SetScrollNormalizedPosition(UITransformAxis, float) | void | Set scroll position |
---
TextInputComponent
Text input field.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Text | string | "" | Entered text |
PlaceHolder | string | "" | Placeholder |
PlaceHolderColor | Color | gray | Placeholder color |
CharacterLimit | int32 | 0 | Max characters (0 = unlimited) |
ContentType | InputContentType | Standard | Input type |
LineType | InputLineType | SingleLine | Single / multi-line |
AutoClear | boolean | false | Auto-clear after submit |
IsLocalizationKey | boolean | false | Treat PlaceHolder as a locale key |
AllowAutomaticTranslation | boolean | true | Enable automatic translation while playing |
IsFocused | boolean | -- | Focus state (read-only) |
Methods
| Method | Returns | Description |
|---|---|---|
ActivateInputField() | void | Set focus |
Events
| Event | Description |
|---|---|
TextInputValueChangeEvent | While typing (text: string) |
TextInputEndEditEvent | Edit ended (text: string) |
TextInputSubmitEvent | Submit (text: string) |
TextInputKeyDownEvent | Key down |
TextInputKeyUpEvent | Key up |
---
SliderComponent
Slider / progress bar.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Value | float | 0 | Current value |
MinValue | float | 0 | Minimum |
MaxValue | float | 1 | Maximum |
UseIntegerValue | boolean | false | Allow integers only |
Direction | SliderDirection | -- | Slider direction |
HandleSize | Vector2 | -- | Handle size |
HandleColor | Color | -- | Handle color |
UseHandle | boolean | true | Show handle |
FillRectColor | Color | (1, 1, 1, 1) | Fill area color |
FillRectImageRUID | DataRef | -- | Fill area image |
FillRectPadding | RectOffset | (10, 10, 10, 10) | Inner padding of the fill rect |
HandleAreaPadding | RectOffset | 0, 0, 0, 0 | Inner padding of the handle area |
HandleImageRUID | DataRef | -- | Handle image |
Events
| Event | Description |
|---|---|
SliderValueChangedEvent | Value changed (Value: float) |
---
UITouchReceiveComponent
Receives touch / mouse input. Just attaching it makes events fire.
Events
| Event | Properties | Description |
|---|---|---|
UITouchDownEvent | Entity, TouchId, TouchPoint | Touch / click start |
UITouchUpEvent | Entity, TouchId, TouchPoint | Touch / click end |
UITouchDragEvent | Entity, TouchDelta, TouchId, TouchPoint | Drag |
UITouchBeginDragEvent | Entity | Drag start |
UITouchEndDragEvent | Entity | Drag end |
UITouchEnterEvent | Entity | Pointer enter |
UITouchExitEvent | Entity | Pointer exit |
---
MaskComponent
Clips child UI to a specific shape.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Shape | MaskShape | Rect | Mask shape (Rect, Circle, etc.) |
Padding | RectOffset | 0,0,0,0 | Soft edge |
Softness | Vector2Int | (0, 0) | Blur amount |
---
JoystickComponent
Virtual joystick (mobile). Builder: joystick(name, options) — anchors to bottom-left at (200, 200) with a 300x300 rect by default.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
DynamicStick | boolean | true | Track touch position |
Axis | AxisType | Axis_8(1) | Axis_4(0), Axis_8(1) |
UpArrow | KeyboardKey | UpArrow(273) | Up-direction key mapping |
DownArrow | KeyboardKey | DownArrow(274) | Down-direction key mapping |
LeftArrow | KeyboardKey | LeftArrow(276) | Left-direction key mapping |
RightArrow | KeyboardKey | RightArrow(275) | Right-direction key mapping |
---
ChatComponent
In-game chat UI. Builder: chat(name, options).
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Expand | boolean | true | Expandable |
UseChatBalloon | boolean | false | Show speech balloons |
UseChatEmotion | boolean | true | Emotion support |
ChatEmotionDuration | float | 5.0 | Emotion display duration (seconds) |
EnableVoiceChat | boolean | true | Allow voice-chat button |
HideWorldChatButton | boolean | false | Hide the world-chat button |
MessageAlignBottom | boolean | false | Anchor newest message to the bottom |
Events
| Event | Description |
|---|---|
ChatEvent | Chat event |
---
SoftMaskComponent
Soft-edged clipping mask (UGUI SoftMask style). Builder: softMask(name, options). Attach to a sprite entity; child sprites/raw images are clipped with anti-aliased edges. Note: gated by the EnableUnpublishFeature maker authority.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
InvertMask | boolean | false | Invert the alpha mask |
InvertOutsides | boolean | false | Invert the mask outside its bounds |
---
LineGUIRendererComponent
Draws a polyline (HUD lines, guides). Builder: line(name, options) — options.points is an array of { pos: [x, y], color: "#RRGGBB" | Color, width: float }.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Points | LinePoint[] | [] | Vertex list; each entry has Position, Color, Width |
IsFlexible | boolean | true | Smooth corners using Flexibility |
Flexibility | float | 3.0 | Curvature factor (>=1.0) |
IsSmooth | boolean | false | Anti-aliased rendering |
Loop | boolean | false | Close the path back to the first point |
MaterialId | string | "" | Custom material id |
---
PolygonGUIRendererComponent
Draws an arbitrary polygon (speech-balloon tails, custom shapes). Builder: polygon(name, options) — options.points is an array of [x, y]; optional options.uvs for custom UV mapping when use_custom_uvs: true.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Points | Vector2[] | [] | Polygon vertices (counter-clockwise) |
Color | Color | white | Fill color |
UseCustomUVs | boolean | false | Use the UVs list instead of auto UV |
UVs | Vector2[] | [] | Custom UV coordinates (same length as Points) |
MaterialId | string | "" | Custom material id |
Methods
| Method | Returns | Description |
|---|---|---|
IsDrawable() | boolean | Whether the polygon can be triangulated |
---
UISpriteParticleComponent
Sprite-textured particle effect (extends UI particle base). Builder: spriteParticle(name, options).
Properties
| Name | Type | Default | Description |
|---|---|---|---|
ParticleType | UISpriteParticleType | None(0) | Preset id (see enum below) |
SpriteRUID | string | "" | Sprite resource RUID |
ApplySpriteColor | boolean | false | Tint the sprite with Color |
Color | Color | (0.5, 0.25, 0.25, 1) | Particle tint |
LocalScale | Vector2 | (1, 1) | Per-particle scale |
Loop | boolean | true | Loop emission |
PlayOnEnable | boolean | true | Auto-play on enable |
Prewarm | boolean | false | Pre-simulate one cycle before showing |
PlaySpeed | float | 1.0 | Animation speed (0-10) |
ParticleSize | float | 1.0 | Per-particle size (0-10) |
ParticleSpeed | float | 1.0 | Per-particle speed (-10..10) |
ParticleCount | float | 1.0 | Emit-count multiplier (0-3) |
ParticleLifeTime | float | 1.0 | Lifetime seconds (1/120..10) |
AutoRandomSeed | boolean | true | Pick a new seed on each emit |
RandomSeed | int32 | 0 | Manual seed when AutoRandomSeed is false |
UISpriteParticleType: None=0, BurstBig=1, SpawnField=2, BurstNova=3, SimpleSpawn=4, Burst=5, Stream=6, StreamSharp=7, AdditiveColor=8.
---
AvatarGUIRendererComponent
Renders avatars in UI.
Properties
| Name | Type | Default | Description |
|---|---|---|---|
Color | Color | white | Avatar tint |
FlipX | boolean | false | Flip horizontally |
FlipY | boolean | false | Flip vertically |
PlayRate | float | 1.0 | Animation speed |
RaycastTarget | boolean | true | Receive input |
Methods
| Method | Returns | Description |
|---|---|---|
GetAvatarRootEntity() | Entity | Avatar root |
GetBodyEntity() | Entity | Body part |
GetFaceEntity() | Entity | Face part |
SetAvatarPartColor(category, r, g, b, a) | void | Change part color |
PlayEmotion(EmotionalType type, float duration) | void | Play emotion |
---
UILogic
UI coordinate-conversion utility (singleton).
Properties
| Name | Type | Description |
|---|---|---|
ScreenWidth | int32 | Current screen width |
ScreenHeight | int32 | Current screen height |
Methods
| Method | Returns | Description |
|---|---|---|
ScreenToUIPosition(Vector2) | Vector2 | Screen -> UI coords |
UIToWorldPosition(Vector2) | Vector2 | UI -> world coords |
ScreenToWorldPosition(Vector2) | Vector2 | Screen -> world coords |
WorldToScreenPosition(Vector2) | Vector2 | World -> screen coords |
LocalUIToWorldPosition(Vector2, UITransformComponent) | Vector2 | Local UI -> world |
ScreenToLocalUIPosition(Vector2, UITransformComponent) | Vector2 | Screen -> local UI |
GetSiblingIndex(UITransformComponent) | int32 | Get sibling index |
SetSiblingIndex(UITransformComponent, int32) | void | Set sibling index |
---
WorldUI Sort Fields (Common)
ButtonComponent, TextComponent, SliderComponent, SpriteGUIRendererComponent, ScrollLayoutGroupComponent, and TextInputComponent all expose the same 4-field sorting block. The fields are only meaningful when the parent UITransformComponent.UIMode is World(2).
| Name | Type | Default | Description |
|---|---|---|---|
OverrideSorting | boolean | false | Detach this entity's render order from its UI group (World UI only) |
SortingLayer | string | "UI" | Sorting layer name (World UI only; gated by OverrideSorting) |
OrderInLayer | int32 | 0 | Order within the sorting layer (higher draws on top) |
IgnoreMapLayerCheck | boolean | false | Bypass automatic map-layer to sorting-layer remap |
Builder shortcut: pass world_ui: true to sprite() / text() / button() / slider() / scrollLayout() / textInput() to set override_sorting=true with sorting_layer="UI". Override individual values with sorting_layer="World", order_in_layer=10, ignore_map_layer_check=true.
---
Common Types
Color
Color(r, g, b, a) — 0-1 floats. Static factories: Color.FromHexCode("#RRGGBB[AA]"), Color.FromRGBAInt(0xRRGGBBAA). Static values: Color.red, Color.white, Color.black, etc.
TransitionColorSet
NormalColor, HighlightedColor, PressedColor, SelectedColor, DisabledColor, ColorMultiplier, FadeDuration
TransitionRUIDSet
HighlightedSprite, PressedSprite, SelectedSprite, DisabledSprite
DataRef
{ DataId = "32-char hex" } -- image resource reference.
RectOffset
{ left, right, top, bottom } -- int32 rectangular margins.
---
Enums
All values are int32. Pass numeric values to builder patchComponent(...) or use the enum identifier in .mlua runtime code (e.g. TextAlignmentType.MiddleCenter).
Two "alignment" enums exist — do not confuse them:
- `AlignmentType` (0~15) — anchor presets forUITransformComponent.AlignmentOption. Builder string mapping ("top-left"↔ 4, etc.) is in `ui-fundamentals.md` §6. Not duplicated here.
- `TextAlignmentType` / `ChildAlignmentType` (0~8) — 9-cell text/child alignment, defined below. Used byTextComponent.Alignmentand anyChildAlignmentfield. Different enum from anchors above.
TextAlignmentType / ChildAlignmentType -- 9-cell alignment (0~8)
Used by TextComponent.Alignment and any ChildAlignment field. Same value mapping for both.
| Name | Value | Description |
|---|---|---|
| UpperLeft | 0 | Top-left |
| UpperCenter | 1 | Top center |
| UpperRight | 2 | Top-right |
| MiddleLeft | 3 | Left middle |
| MiddleCenter | 4 | Center (builder default for text()) |
| MiddleRight | 5 | Right middle |
| LowerLeft | 6 | Bottom-left |
| LowerCenter | 7 | Bottom center |
| LowerRight | 8 | Bottom-right |
FontType -- Font
| Name | Value | Description |
|---|---|---|
| Default | 0 | Default font |
| Maple | 1 | MapleStory font |
| Bazzi | 2 | Bazzi font |
| Football | 3 | Football Gothic font |
FontStyleType -- Font Style (bit flags, combinable)
| Name | Value | Description |
|---|---|---|
| Normal | 0 | Default |
| Bold | 1 | Bold |
| Italic | 2 | Italic |
| Underline | 4 | Underline |
| LowerCase | 8 | Lowercase |
| UpperCase | 16 | Uppercase |
| SmallCaps | 32 | Small caps |
| Strikethrough | 64 | Strikethrough |
OverflowType -- Text Overflow
Used by TextComponent.Overflow.
| Name | Value | Description |
|---|---|---|
| Overflow | 0 | Show outside the area |
| Truncate | 1 | Truncate |
| Ellipsis | 2 | Ellipsis (...) |
ImageType -- Image Rendering
Used by SpriteGUIRendererComponent.Type.
| Name | Value | Description |
|---|---|---|
| Simple | 0 | Original image |
| Sliced | 1 | 9-slice (corners stay intact when size changes) |
| Tiled | 2 | Tiled repeat |
| Filled | 3 | Partial fill (progress bar) |
FillMethodType -- Fill Direction (for ImageType.Filled)
| Name | Value | Description |
|---|---|---|
| Horizontal | 0 | Horizontal |
| Vertical | 1 | Vertical |
| Radial90 | 2 | 90-degree radial |
| Radial180 | 3 | 180-degree radial |
| Radial360 | 4 | 360-degree radial |
TransitionType -- Button Transition Effect
| Name | Value | Description |
|---|---|---|
| None | 0 | No effect |
| ColorTint | 1 | Color change |
| SpriteSwap | 2 | Image swap |
ButtonState
| Name | Value | Description |
|---|---|---|
| Normal | 0 | Default |
| Hover | 1 | Mouse over |
| Pressed | 2 | Pressed |
| Released | 3 | Released |
| Clicked | 4 | Short click |
LayoutGroupType -- Layout Direction
Used by ScrollLayoutGroupComponent.Type.
| Name | Value | Description |
|---|---|---|
| Horizontal | 0 | Horizontal layout |
| Vertical | 1 | Vertical layout |
| Grid | 2 | Grid layout |
ScrollBarVisibility
| Name | Value | Description |
|---|---|---|
| AlwaysShow | 0 | Always shown |
| AutoHide | 1 | Shown only when scrollable |
| Hide | 2 | Always hidden |
UITransformAxis
| Name | Value | Description |
|---|---|---|
| Horizontal | 0 | Horizontal axis |
| Vertical | 1 | Vertical axis |
GridLayoutAxis / GridLayoutConstraint / GridLayoutCorner
GridLayoutAxis: Horizontal=0, Vertical=1 (child add direction). GridLayoutConstraint: Flexible=0, FixedColumnCount=1, FixedRowCount=2. GridLayoutCorner: UpperLeft=0, UpperRight=1, LowerLeft=2, LowerRight=3 (grid start position).
GridViewFixedType
Used by GridViewComponent.FixedType.
| Name | Value | Description |
|---|---|---|
| ColumnCountFixed | 0 | Fixed column count (vertical scroll) |
| RowCountFixed | 1 | Fixed row count (horizontal scroll) |
UIModeType -- UI Drawing Mode
Used by UITransformComponent.UIMode.
| Name | Value | Description |
|---|---|---|
| None | 0 | Initial state |
| Screen | 1 | 2D screen coordinates (HUD/popup/menu — default) |
| World | 2 | World coordinates (nametag, floating damage) |
UIGroupType -- UI Group Type
Used by UIGroupComponent.GroupType.
| Name | Value | Description |
|---|---|---|
| None | 0 | Unused |
| DefaultType | 1 | Default group (HUD layer) |
| UIType | 2 | UI editor group (popup/menu layer) |
| EditorType | 3 | Editor-only group |
MaskShape
Used by MaskComponent.Shape.
| Name | Value | Description |
|---|---|---|
| Rect | 0 | Rectangle |
| Circle | 1 | Circle |
GradientModes
| Name | Value | Description |
|---|---|---|
| Single | 0 | Single color |
| Horizontal | 1 | Horizontal gradient |
| Vertical | 2 | Vertical gradient |
| FourCorners | 3 | Four-corner gradient |
HorizontalScrollBarDirection / VerticalScrollBarDirection
HorizontalScrollBarDirection: LeftToRight=0, RightToLeft=1. VerticalScrollBarDirection: BottomToTop=2, TopToBottom=3.
UIAreaParticleType / UIBasicParticleType
UI particle preset enums. The builder handles preset names directly — pass numeric particle_type=... to areaParticle() / basicParticle(). Full numeric tables live in `../../msw-general/references/builder-protocol.md` §3.5. From runtime, use the enum identifiers (UIAreaParticleType.FogCalm, UIBasicParticleType.Firework).
Layout Recipes
A collection of layout templates based on this skill's CJS UIBuilder (scripts/msw_ui_builder.cjs; see `../../msw-general/references/builder-protocol.md` §3 for the call protocol — unified entry point).
Each recipe is copy-ready from file creation to placement. In actual use, change only path / size / color / RUID values to match your project.
---
Common Preparation
const { UIBuilder } = require("./scripts/msw_ui_builder.cjs");- Coordinates: center origin, 1920x1080, Y-axis positive upward
- Default pivot: anchor-matched when omitted (
top-left->[0, 1],middle-center->[0.5, 0.5]) - Edge formula with omitted pivot:
pos = [margin, margin]with signs based on the anchor side
---
Recipe 1 — Basic HUD (Top-Left Score, Top-Right Minimap, Bottom-Left HP)
const b = new UIBuilder("DefaultGroup", 1, true);
b.panel("ScoreBox", { anchor: "top-left", pos: [20, -20], rect_size: [200, 60] });
b.text("ScoreBox/Label", "Score", {
size: 24,
color: "#FFFFFF",
anchor: "middle-left",
pos: [16, 0],
alignment: 3,
});
b.text("ScoreBox/Value", "0", {
size: 40,
color: "#FFD700",
bold: true,
anchor: "middle-right",
pos: [-16, 0],
alignment: 5,
});
b.panel("MiniMap", { anchor: "top-right", pos: [-20, -20], rect_size: [180, 180] });
b.sprite("MiniMap/Frame", { anchor: "stretch", image_ruid: "<minimap-frame-ruid>" });
b.panel("HPBar", { anchor: "bottom-left", pos: [20, 20], rect_size: [220, 30] });
b.sprite("HPBar/Bg", { anchor: "stretch", color: "#1A1A1A", alpha: 0.8 });
b.sprite("HPBar/Fill", { anchor: "stretch", color: "#E53935" });
b.patchComponent("HPBar/Fill", "MOD.Core.SpriteGUIRendererComponent", {
Type: 3,
FillMethod: 0,
FillOrigin: 0,
FillAmount: 1.0,
});
b.write("ui/DefaultGroup.ui");HP Update at Runtime
See `runtime-patterns.md` §3 HP Bar (Progress Bar). Key point: a single line self.fillImage.FillAmount = hp / maxHp. Property binding is auto-injected via b.write(path, { bind: {...} }).
---
Recipe 2 — Modal Confirmation Popup (Title + Message + OK/Cancel)
const b = new UIBuilder("PopupGroup", 10, false);
b.sprite("Dimmer", { anchor: "stretch", color: "#000000", alpha: 0.6, raycast: true });
b.panel("Panel", { anchor: "middle-center", pos: [0, 0], rect_size: [600, 400] });
b.sprite("Panel/Bg", { anchor: "stretch", color: "#2C2C2C" });
b.text("Panel/Title", "Confirm", {
size: 48,
color: "#FFFFFF",
bold: true,
anchor: "top-center",
pos: [0, -50],
rect_size: [560, 60],
alignment: 4,
});
b.text("Panel/Message", "Are you sure?", {
size: 28,
color: "#DDDDDD",
anchor: "middle-center",
pos: [0, 20],
rect_size: [520, 160],
alignment: 4,
});
b.button("Panel/BtnOk", "OK", {
rect_size: [180, 60],
pos: [-110, 40],
anchor: "bottom-center",
font_size: 28,
});
b.button("Panel/BtnCancel", "Cancel", {
rect_size: [180, 60],
pos: [110, 40],
anchor: "bottom-center",
font_size: 28,
});
b.write("ui/PopupGroup.ui", {
bind: {
mlua: "RootDesk/MyDesk/UIPopup.mlua",
props: {
popupGroup: "Panel",
btnOk: "Panel/BtnOk",
btnCancel: "Panel/BtnCancel",
message: "Panel/Message",
},
},
});---
Recipe 3 — Toast (Auto-Dismissing Notification)
const b = new UIBuilder("ToastGroup", 20, false);
b.panel("Toast", { anchor: "bottom-center", pos: [0, 140], rect_size: [600, 80] });
b.sprite("Toast/Bg", { anchor: "stretch", color: "#1E1E1E", alpha: 0.9 });
b.text("Toast/Message", "", { size: 28, color: "#FFFFFF", anchor: "stretch", alignment: 4 });
b.write("ui/ToastGroup.ui");For the mlua-side logic, use the Toast pattern from `runtime-patterns.md`.
---
Recipe 4 — Top Menu Bar (3 Tabs)
const b = new UIBuilder("MenuGroup", 5, false);
b.sprite("Dimmer", { anchor: "stretch", color: "#000000", alpha: 0.7, raycast: true });
b.panel("TopTabs", { anchor: "top-center", pos: [0, -60], rect_size: [1200, 80] });
b.sprite("TopTabs/Bg", { anchor: "stretch", color: "#1A1A1A" });
["Character", "Inventory", "Settings"].forEach((name, i) => {
const x = -400 + i * 400;
b.button(`TopTabs/Tab${i}`, name, { rect_size: [380, 70], pos: [x, 0], font_size: 24 });
b.panel(`Content${i}`, { anchor: "middle-center", pos: [0, -40], rect_size: [1400, 800] });
});
b.write("ui/MenuGroup.ui");Tab switching logic in mlua: for i, content in ipairs(self.contents) do content.Enable = (i == activeIdx) end.
---
Recipe 5 — Inventory Grid (GridView Virtualization)
const b = new UIBuilder("InventoryGroup", 7, false);
b.sprite("Dimmer", { anchor: "stretch", color: "#000000", alpha: 0.7, raycast: true });
b.panel("Window", { anchor: "middle-center", pos: [0, 0], rect_size: [900, 700] });
b.sprite("Window/Bg", { anchor: "stretch", color: "#2C2C2C" });
b.text("Window/Title", "Inventory", {
size: 36,
color: "#FFFFFF",
bold: true,
anchor: "top-center",
pos: [0, -40],
rect_size: [800, 50],
alignment: 4,
});
b.button("Window/BtnClose", "X", {
rect_size: [50, 50],
pos: [-30, -30],
anchor: "top-right",
font_size: 24,
});
b.panel("Window/ItemTemplate", { anchor: "top-left", pos: [50, -50], rect_size: [80, 80] });
b.sprite("Window/ItemTemplate/Frame", { anchor: "stretch", image_ruid: "<slot-frame-ruid>" });
b.sprite("Window/ItemTemplate/Icon", { anchor: "middle-center", rect_size: [64, 64] });
b.text("Window/ItemTemplate/Count", "", {
size: 20,
color: "#FFFFFF",
anchor: "bottom-right",
pos: [-8, 8],
rect_size: [40, 24],
alignment: 5,
});
b.patch("Window/ItemTemplate", { enable: false });
b.panel("Window/Grid", { anchor: "stretch", rect_size: [800, 560] });
b.patchComponent("Window/Grid", "MOD.Core.UITransformComponent", {
OffsetMin: { x: 50, y: 50 },
OffsetMax: { x: -50, y: -100 },
});
b.addComponent("Window/Grid", "MOD.Core.GridViewComponent", {
CellSize: { x: 90, y: 90 },
FixedCount: 8,
FixedType: 0,
Spacing: { x: 6, y: 6 },
UseScroll: true,
});
b.write("ui/InventoryGroup.ui");OnRefresh Callback (Runtime)
See `runtime-patterns.md` §5 GridView Large List. Property binding injects two items — grid / itemTemplate — via b.write(path, { bind: {...} }).
Caution: OnRefresh is called frequently during scrolling. Do not call DataStorage; only query the in-memory cache.---
Recipe 6 — Scroll Chat/Log (ScrollLayoutGroup, Small Scale)
const b = new UIBuilder("ChatGroup", 4, true);
b.panel("ChatBox", { anchor: "bottom-left", pos: [220, 220], rect_size: [400, 300] });
b.sprite("ChatBox/Bg", { anchor: "stretch", color: "#000000", alpha: 0.5 });
b.panel("ChatBox/List", { anchor: "stretch" });
b.patchComponent("ChatBox/List", "MOD.Core.UITransformComponent", {
OffsetMin: { x: 10, y: 50 },
OffsetMax: { x: -10, y: -10 },
});
b.addComponent("ChatBox/List", "MOD.Core.ScrollLayoutGroupComponent", {
Type: 1,
Spacing: 6,
ScrollBarVisible: 1,
});
b.addComponent("ChatBox/List", "MOD.Core.MaskComponent", { Shape: 0 });
b.panel("ChatBox/InputArea", { anchor: "bottom-center", pos: [0, 20], rect_size: [380, 40] });
b.sprite("ChatBox/InputArea/Bg", { anchor: "stretch", color: "#222222" });
b.text("ChatBox/InputArea/Text", "", { size: 20, color: "#FFFFFF", anchor: "stretch", alignment: 3 });
b.addComponent("ChatBox/InputArea/Text", "MOD.Core.TextInputComponent", {
PlaceHolder: "Type here...",
LineType: 0,
AutoClear: true,
});
b.write("ui/ChatGroup.ui");Adding messages at runtime: Create Text entities as children of List via SpawnService; ScrollLayoutGroup auto-arranges them. Pin scroll to bottom: layoutGroup:SetScrollNormalizedPosition(1, 0).
---
Recipe 7 — Settings Slider List
const b = new UIBuilder("SettingsGroup", 7, false);
b.sprite("Dimmer", { anchor: "stretch", color: "#000000", alpha: 0.7, raycast: true });
b.panel("Window", { anchor: "middle-center", rect_size: [700, 600] });
b.sprite("Window/Bg", { anchor: "stretch", color: "#2C2C2C" });
b.text("Window/Title", "Settings", {
size: 36,
bold: true,
anchor: "top-center",
pos: [0, -40],
rect_size: [600, 50],
alignment: 4,
});
[
["BGMVol", "BGM Volume", 0, 1, 0.8],
["SFXVol", "SFX Volume", 0, 1, 1.0],
["UIScale", "UI Scale", 0.8, 1.5, 1.0],
].forEach(([key, label, minValue, maxValue, value], i) => {
const y = -140 - i * 100;
b.panel(`Window/Row${i}`, { anchor: "top-center", pos: [0, y], rect_size: [600, 80] });
b.text(`Window/Row${i}/Label`, label, {
size: 24,
anchor: "middle-left",
pos: [20, 0],
rect_size: [180, 40],
alignment: 3,
});
b.slider(`Window/Row${i}/Slider`, {
min_val: minValue,
max_val: maxValue,
value,
anchor: "middle-right",
pos: [-120, 0],
rect_size: [320, 30],
});
b.text(`Window/Row${i}/Value`, value.toFixed(2), {
size: 22,
anchor: "middle-right",
pos: [-20, 0],
rect_size: [80, 40],
alignment: 5,
});
});
b.button("Window/BtnClose", "Close", {
rect_size: [200, 60],
anchor: "bottom-center",
pos: [0, 40],
font_size: 26,
});
b.write("ui/SettingsGroup.ui");---
Recipe 8 — Card-Like Clickable Tile
Use one b.button(...) entity when a repeated tile needs background, label, and click handling. This avoids separate sprite/text entities for every tile.
const b = new UIBuilder("BoardGroup", 3, true);
for (let i = 0; i < 12; i += 1) {
const x = -330 + (i % 6) * 132;
const y = 120 - Math.floor(i / 6) * 160;
b.button(`Tile_${i}`, "", {
rect_size: [104, 144],
pos: [x, y],
anchor: "middle-center",
font_size: 30,
color: "#FFFFFF",
});
b.patchComponent(`Tile_${i}`, "MOD.Core.SpriteGUIRendererComponent", {
Color: { r: 0.05, g: 0.12, b: 0.28, a: 1.0 },
});
}
b.write("ui/BoardGroup.ui");Runtime update pattern:
method void SetTileFace(TextComponent label, SpriteGUIRendererComponent sprite, string text, boolean faceUp)
if faceUp then
sprite.Color = Color(1, 1, 1, 1)
label.Text = text
else
sprite.Color = Color(0.05, 0.12, 0.28, 1)
label.Text = ""
end
endUse this for card games, board cells, inventory slots, quick slots, tabs, and same-shape menu items.
---
Recipe Selection Guide
| Request Keyword | Recipe |
|---|---|
| Score / HP / Minimap / Always-on info | Recipe 1 (HUD) |
| Confirm / Yes/No / Warning | Recipe 2 (Modal Popup) |
| Acquisition / Notification / Result | Recipe 3 (Toast) |
| Tab menu / Top navigation | Recipe 4 (Tabbed Menu) |
| Inventory / Shop / Equipment window / Many slots | Recipe 5 (GridView) |
| Chat / Log / Small list | Recipe 6 (ScrollLayoutGroup) |
| Settings / Volume / Scale | Recipe 7 (Slider List) |
| Card / tile / slot / repeated clickable cell | Recipe 8 (Card-Like Clickable Tile) |
---
Common Finishing Steps
After running any recipe:
1. Binding Injection — Auto-inject entity UUIDs into the corresponding .mlua property defaults via b.write(filepath, { bind: { mlua, props } }) or b.injectBindings(mlua_path, props). See `../../msw-general/references/builder-protocol.md` §3.6 Binding Injection for details. 2. Preview Check — Visualize the layout with scripts/preview_ui_layout.cjs 3. Maker Refresh — Reflect changes in the engine via MCP refresh 4. Play Mode Verification — Verify on actual resolution and mobile scale
Snapshot:
UIBuilder.snapshot("ui/PopupGroup.ui"); // Backup right before writeUI Runtime Patterns
.mlua patterns for controlling UI from scripts. These are runtime code, separate from .ui file authoring (which goes through the builder — see `../../msw-general/references/builder-protocol.md` §3, the unified entry point).
---
1. Popup Dialog
A modal popup with a message and OK / cancel buttons.
@Logic
@ExecSpace("ClientOnly")
script UIPopup extends Logic
property TextComponent message = "uuid-text"
property ButtonComponent btnOk = "uuid-btn-ok"
property ButtonComponent btnCancel = "uuid-btn-cancel"
property Entity popupGroup = "uuid-group"
method void OnBeginPlay()
self.popupGroup.Enable = false
end
method void Open(string msg, any onOk, any onCancel)
self.onOk = onOk
self.onCancel = onCancel
self.message.Text = msg
self.popupGroup.Enable = true
self.okHandler = self.btnOk.Entity:ConnectEvent(ButtonClickEvent, function()
if self.onOk ~= nil then self.onOk() end
self:Close()
end)
self.cancelHandler = self.btnCancel.Entity:ConnectEvent(ButtonClickEvent, function()
if self.onCancel ~= nil then self.onCancel() end
self:Close()
end)
end
method void Close()
self.btnOk.Entity:DisconnectEvent(ButtonClickEvent, self.okHandler)
self.btnCancel.Entity:DisconnectEvent(ButtonClickEvent, self.cancelHandler)
self.popupGroup.Enable = false
end
method void OnEndPlay()
self:Close()
end
end---
2. Toast Message
A notification that fades out after a fixed duration.
@Logic
@ExecSpace("ClientOnly")
script UIToast extends Logic
property TextComponent message = "uuid-text"
property Entity toastGroup = "uuid-group"
property number duration = 2
property number fadeDuration = 0.3
method void OnBeginPlay()
self.toastGroup.Enable = false
end
method void ShowMessage(string msg)
self.message.Text = msg
self.toastGroup.Enable = true
local canvasGroup = self.toastGroup.CanvasGroupComponent
canvasGroup.GroupAlpha = 1
if self.timerId then
_TimerService:ClearTimer(self.timerId)
end
local time = 0
local preTime = _UtilLogic.ElapsedSeconds
self.timerId = _TimerService:SetTimerRepeat(function()
local delta = _UtilLogic.ElapsedSeconds - preTime
time = time + delta
preTime = _UtilLogic.ElapsedSeconds
if time >= self.duration + self.fadeDuration then
canvasGroup.GroupAlpha = 0
self.toastGroup.Enable = false
_TimerService:ClearTimer(self.timerId)
self.timerId = nil
elseif time >= self.duration then
canvasGroup.GroupAlpha = 1 - (time - self.duration) / self.fadeDuration
end
end, 1/60)
end
method void OnEndPlay()
if self.timerId then
_TimerService:ClearTimer(self.timerId)
end
end
end---
3. HP Bar (Progress Bar)
Implement an HP bar with SpriteGUIRenderer's Filled type.
@Component
@ExecSpace("ClientOnly")
script HPBar extends Component
property SpriteGUIRendererComponent fillImage = "uuid-fill"
property TextComponent hpText = "uuid-text"
method void UpdateHP(number current, number max)
local ratio = current / max
ratio = math.max(0, math.min(1, ratio))
self.fillImage.FillAmount = ratio
self.hpText.Text = tostring(math.floor(current)) .. " / " .. tostring(math.floor(max))
-- color transition: green -> yellow -> red
if ratio > 0.5 then
self.fillImage.Color = Color(0, 1, 0, 1)
elseif ratio > 0.2 then
self.fillImage.Color = Color(1, 1, 0, 1)
else
self.fillImage.Color = Color(1, 0, 0, 1)
end
end
endNote: Set the SpriteGUIRenderer Type to Filled(3) and FillMethod to Horizontal(0).
---
4. Scroll List + Item Cloning
Hide a template and add items via Clone.
@Logic
@ExecSpace("ClientOnly")
script ScrollList extends Logic
property Entity itemTemplate = "uuid-template"
property ScrollLayoutGroupComponent scrollLayout = "uuid-scroll"
method void OnBeginPlay()
self.itemTemplate:SetEnable(false) -- hide template
self.items = {}
end
method void AddItem(string text)
local clone = self.itemTemplate:Clone("Item_" .. #self.items)
clone:SetEnable(true)
clone.TextComponent.Text = text
table.insert(self.items, clone)
end
method void ClearAll()
for _, item in ipairs(self.items) do
item:Destroy()
end
self.items = {}
end
method void ScrollToBottom()
self.scrollLayout:SetScrollNormalizedPosition(UITransformAxis.Vertical, 0.0)
end
method void OnEndPlay()
self:ClearAll()
end
end---
5. GridView Large List
For 100+ items, use GridView instead of ScrollLayout.
@Logic
@ExecSpace("ClientOnly")
script InventoryGrid extends Logic
property GridViewComponent gridView = "uuid-gridview"
method void OnBeginPlay()
self.data = {}
-- initialize data
for i = 1, 200 do
table.insert(self.data, "Item " .. tostring(i))
end
self.gridView.TotalCount = #self.data
self.gridView.OnRefresh = function(index, entity)
-- index is 0-based
entity.TextComponent.Text = self.data[index + 1]
entity.SpriteGUIRendererComponent.Color = Color.white
end
self.gridView.OnClear = function(index, entity)
-- clean up items that scrolled off-screen (optional)
end
self.gridView:Refresh(true, true)
end
method void RefreshData()
self.gridView.TotalCount = #self.data
self.gridView:Refresh(false, true)
end
end---
6. Tab UI (Toggle Group)
Activate only one tab at a time.
@Logic
@ExecSpace("ClientOnly")
script TabUI extends Logic
property Entity tab1Content = "uuid-content1"
property Entity tab2Content = "uuid-content2"
property Entity tab3Content = "uuid-content3"
property ButtonComponent tab1Btn = "uuid-btn1"
property ButtonComponent tab2Btn = "uuid-btn2"
property ButtonComponent tab3Btn = "uuid-btn3"
method void OnBeginPlay()
self.tabs = {self.tab1Content, self.tab2Content, self.tab3Content}
self.tab1Btn.Entity:ConnectEvent(ButtonClickEvent, function() self:SelectTab(1) end)
self.tab2Btn.Entity:ConnectEvent(ButtonClickEvent, function() self:SelectTab(2) end)
self.tab3Btn.Entity:ConnectEvent(ButtonClickEvent, function() self:SelectTab(3) end)
self:SelectTab(1)
end
method void SelectTab(number index)
for i, tab in ipairs(self.tabs) do
tab.Enable = (i == index)
end
end
end---
7. Runtime Z-Order / Sibling Reorder
Use _UILogic:SetSiblingIndex(targetUITransform, index) on the client to reorder UI siblings at runtime. There is no entity.SetAsLastSibling() pattern in the public mlua API; reorder through the target entity's UITransformComponent. The index is 1-based; a deliberately high index moves the target to the front among siblings.
@Logic
script UIStackOrder extends Logic
property UITransformComponent draggingCard = nil
@ExecSpace("ClientOnly")
method void BringToFront(UITransformComponent target)
if target == nil then
return
end
_UILogic:SetSiblingIndex(target, 1000000)
end
endUse this when creation order is not enough:
- Card/table stacks where later gameplay changes which card should receive input first.
- Dragging an entity that must render above its siblings while held.
- Popup-over-popup flows within the same UIGroup.
Prefer build-time displayOrder for static layouts. Use runtime sibling reorder only for dynamic overlap.
---
8. Drag and Drop
Implement drag with UITouchReceiveComponent.
@Component
@ExecSpace("ClientOnly")
script Draggable extends Component
method void OnBeginPlay()
self.dragHandler = self.Entity:ConnectEvent(UITouchDragEvent, self.OnDrag)
end
method void OnDrag(UITouchDragEvent event)
local transform = self.Entity.UITransformComponent
local pos = transform.anchoredPosition
transform.anchoredPosition = Vector2(
pos.x + event.TouchDelta.x,
pos.y + event.TouchDelta.y
)
end
method void OnEndPlay()
self.Entity:DisconnectEvent(UITouchDragEvent, self.dragHandler)
end
end---
9. Text Input + Chat
@Logic
@ExecSpace("ClientOnly")
script ChatUI extends Logic
property TextInputComponent chatInput = "uuid-input"
property TextComponent chatLog = "uuid-log"
property ScrollLayoutGroupComponent scrollLayout = "uuid-scroll"
property Entity messageTemplate = "uuid-template"
method void OnBeginPlay()
self.messageTemplate:SetEnable(false)
self.submitHandler = self.chatInput.Entity:ConnectEvent(
TextInputSubmitEvent, self.OnSubmit)
end
method void OnSubmit(TextInputSubmitEvent event)
local text = event.text
if text == "" then return end
local msg = self.messageTemplate:Clone("Msg_" .. self.msgCount)
msg:SetEnable(true)
msg.TextComponent.Text = text
self.msgCount = self.msgCount + 1
-- scroll to bottom
self.scrollLayout:SetScrollNormalizedPosition(UITransformAxis.Vertical, 0.0)
end
method void OnEndPlay()
self.chatInput.Entity:DisconnectEvent(TextInputSubmitEvent, self.submitHandler)
end
end---
10. Cooldown Display (Radial FillAmount)
@Component
@ExecSpace("ClientOnly")
script CooldownUI extends Component
property SpriteGUIRendererComponent cooldownOverlay = "uuid-overlay"
property TextComponent cooldownText = "uuid-text"
method void StartCooldown(number duration)
self.cooldownOverlay.Entity:SetEnable(true)
local time = 0
local preTime = _UtilLogic.ElapsedSeconds
self.timerId = _TimerService:SetTimerRepeat(function()
local delta = _UtilLogic.ElapsedSeconds - preTime
time = time + delta
preTime = _UtilLogic.ElapsedSeconds
local remaining = duration - time
if remaining <= 0 then
self.cooldownOverlay.FillAmount = 0
self.cooldownOverlay.Entity:SetEnable(false)
self.cooldownText.Text = ""
_TimerService:ClearTimer(self.timerId)
return
end
self.cooldownOverlay.FillAmount = remaining / duration
self.cooldownText.Text = tostring(math.ceil(remaining))
end, 1/60)
end
method void OnEndPlay()
if self.timerId then
_TimerService:ClearTimer(self.timerId)
end
end
endSetup: SpriteGUIRenderer Type=Filled(3), FillMethod=Radial360(4), translucent black.
---
11. World UI (Overhead Name Tag)
Place UI at world coordinates with UIModeType.World.
@Component
@ExecSpace("ClientOnly")
script NameTag extends Component
property TextComponent nameText = "uuid-text"
method void OnBeginPlay()
self.nameText.Text = self.Entity.Name
end
method void OnUpdate(number dt)
-- follow the entity position
local worldPos = self.Entity.TransformComponent.WorldPosition
local uiTransform = self.nameText.Entity.UITransformComponent
local screenPos = _UILogic:WorldToScreenPosition(Vector2(worldPos.x, worldPos.y + 1.5))
local uiPos = _UILogic:ScreenToUIPosition(screenPos)
uiTransform.anchoredPosition = uiPos
end
end---
Event Handler Skeletons
Quick skeletons for the most common UI events. Always store the handler return and DisconnectEvent in OnEndPlay.
Button click
property ButtonComponent btnOk = "uuid"
property any clickHandler = nil
@ExecSpace("ClientOnly")
method void OnBeginPlay()
self.clickHandler = self.btnOk.Entity:ConnectEvent(ButtonClickEvent, self.OnClick)
end
method void OnClick() end
method void OnEndPlay()
self.btnOk.Entity:DisconnectEvent(ButtonClickEvent, self.clickHandler)
endText input
property TextInputComponent input = "uuid"
property any submitHandler = nil
method void OnBeginPlay()
self.submitHandler = self.input.Entity:ConnectEvent(TextInputSubmitEvent, self.OnSubmit)
end
method void OnSubmit(TextInputSubmitEvent event)
local text = event.text
endSlider
property SliderComponent slider = "uuid"
property any sliderHandler = nil
method void OnBeginPlay()
self.sliderHandler = self.slider.Entity:ConnectEvent(SliderValueChangedEvent, self.OnValueChanged)
end
method void OnValueChanged(SliderValueChangedEvent event)
local value = event.Value
endTouch / drag
-- attach UITouchReceiveComponent on the entity first (use the builder's touchReceive())
entity:ConnectEvent(UITouchDownEvent, handler)
entity:ConnectEvent(UITouchDragEvent, handler)
entity:ConnectEvent(UITouchUpEvent, handler)---
Runtime UI Caveats
Hard rules that show up as "UI doesn't respond" or "Server can't see UI". Memorize.
Hard constraints (silent failure if broken)
1. UI entities are client-only. If an @Component on a UI entity defines @ExecSpace("Server"), @ExecSpace("ServerOnly"), or @ExecSpace("Multicast") methods, the runtime emits '<entity>' is client only. '<component>.<method>' doesn't work normally. and RPCs do not work. @Sync properties are also not synchronized. Route UI-to-server communication through an @Logic outside the UI entity, or a map entity @Component, then call the Server RPC. 2. No UI entity access from server. Referencing a UI entity in @ExecSpace("Server") / @ExecSpace("ServerOnly") returns nil. For server-to-UI updates, route through an @ExecSpace("Client") RPC. 3. All UI Logic / Component must declare `@ExecSpace("ClientOnly")` — default ExecSpace doesn't guarantee client-only execution. 4. Do not attach UI components (ButtonComponent, etc.) to map / world entities — UI-only. Trying to attach via builder/runtime silently misbehaves. 5. `UIGroup DefaultShow=false` — not visible until Enable=true. Also, if DefaultShow=false AND the group has no controller script outside to flip Enable, scripts inside the group never run OnBeginPlay / OnUpdate (typical symptom: "level-up popup never shows"). See `ui-hierarchy.md` for the standard pattern.
Movement / fade / visibility
6. Move via `anchoredPosition` — never set Position directly (engine treats it as a derived cache, your writes get overwritten). 7. Fade via `CanvasGroupComponent.GroupAlpha` — don't tween individual element alphas. One write covers the whole subtree consistently. 8. Show/hide via `Enable` — popupGroup.Enable = true/false. Visible = false keeps clicks alive and OnUpdate running (see `ui-hierarchy.md` §5).
Resource cleanup
9. Always `DisconnectEvent` in `OnEndPlay` — otherwise event handlers leak across script reloads / popup re-opens. 10. `_TimerService:ClearTimer` in `OnEndPlay` — store the timer ID returned by SetTimerRepeat and clear it.
Animation timing
11. Use a 1/60 repeating timer for per-frame UI animation (_TimerService:SetTimerRepeat(fn, 1/60)). MSW doesn't expose a global UI Update hook. 12. Measure delta with `_UtilLogic.ElapsedSeconds` — diff between frames, never assume the timer interval is exact.
#!/usr/bin/env node
'use strict';
/**
* UI Template RUID Lookup — run this to get RUID values for UI elements.
*
* Usage:
* node ruid-lookup.js # list available styles
* node ruid-lookup.js --style 1 # dump all RUIDs for style 1
* node ruid-lookup.js --style 1 --role button # filter by role keyword
*/
const fs = require('fs');
const path = require('path');
const SCRIPT_DIR = __dirname;
const SKILL_DIR = path.dirname(SCRIPT_DIR);
const STYLES = {
'1': { dir: 'style-1-black', pattern: 'Simple Popups (Black)' },
'2': { dir: 'style-2-diary', pattern: 'Minimal HUD (Diary)' },
'3': { dir: 'style-3-wood', pattern: 'Multi-Tab (Wood)' },
'4': { dir: 'style-4-blue', pattern: 'Transaction Flow (Blue)' },
};
function parseArgs() {
const args = process.argv.slice(2);
const result = { style: null, role: null };
for (let i = 0; i < args.length; i++) {
if ((args[i] === '--style' || args[i] === '-s') && args[i + 1]) {
result.style = args[++i];
} else if ((args[i] === '--role' || args[i] === '-r') && args[i + 1]) {
result.role = args[++i];
}
}
return result;
}
function extractRuids(styleDir) {
const uiDir = path.join(SKILL_DIR, styleDir);
const results = [];
const files = fs.readdirSync(uiDir).filter(f => f.endsWith('.ui')).sort();
for (const fname of files) {
const data = JSON.parse(fs.readFileSync(path.join(uiDir, fname), 'utf8'));
const entities = (data.ContentProto && data.ContentProto.Entities) || [];
for (const ent of entities) {
const js = ent.jsonString || {};
const name = js.name || '';
const comps = js['@components'] || [];
for (const comp of comps) {
const imageRuid = comp.ImageRUID;
if (imageRuid && typeof imageRuid === 'object') {
const did = imageRuid.DataId || '';
if (did) {
results.push({ file: fname, entity: name, ruid: did });
}
}
}
}
}
return results;
}
function classifyRole(entityName, fileName) {
const n = entityName.toLowerCase();
const f = fileName.toLowerCase();
if (f.includes('toast')) return 'toast';
if (f.includes('default')) return 'infrastructure';
if (['btn', 'button', 'ok', 'cancel', 'close', 'exit'].some(k => n.includes(k))) return 'button';
if (['slot', 'item', 'equip', 'inven'].some(k => n.includes(k))) return 'slot/item';
if (n.includes('icon')) return 'icon';
if (['panel', 'bg', 'popup', 'dim', 'paper'].some(k => n.includes(k))) return 'panel/background';
if (['fill', 'gauge', 'bar', 'hp', 'mp', 'exp', 'progress'].some(k => n.includes(k))) return 'gauge/bar';
if (['money', 'coin', 'meso', 'reward', 'gold'].some(k => n.includes(k))) return 'currency/reward';
if (['title', 'deco', 'line', 'pattern', 'effect'].some(k => n.includes(k))) return 'decoration';
if (['hud', 'match', 'timer', 'score'].some(k => n.includes(k))) return 'hud';
return 'other';
}
function main() {
const args = parseArgs();
if (!args.style) {
console.log('Available styles:');
for (const [k, v] of Object.entries(STYLES)) {
console.log(` --style ${k} → ${v.dir} (${v.pattern})`);
}
console.log('\nRoles: button, panel/background, slot/item, icon, gauge/bar, currency/reward, decoration, hud, toast');
return;
}
if (!STYLES[args.style]) {
console.error(`Error: style must be 1-4, got '${args.style}'`);
process.exit(1);
}
const styleInfo = STYLES[args.style];
const results = extractRuids(styleInfo.dir);
const seen = new Map();
for (const r of results) {
const role = classifyRole(r.entity, r.file);
const key = `${r.ruid}\0${role}`;
if (!seen.has(key)) {
seen.set(key, { ruid: r.ruid, role, entities: [] });
}
seen.get(key).entities.push(r.entity);
}
const grouped = {};
for (const info of seen.values()) {
if (!grouped[info.role]) grouped[info.role] = [];
grouped[info.role].push(info);
}
const roleFilter = args.role ? args.role.toLowerCase() : null;
console.log(`=== Style ${args.style}: ${styleInfo.pattern} (${styleInfo.dir}) ===\n`);
for (const role of Object.keys(grouped).sort()) {
if (roleFilter && !role.includes(roleFilter)) continue;
const items = grouped[role];
console.log(`[${role}]`);
for (const item of items) {
const unique = [...new Set(item.entities)];
let entitiesStr = unique.slice(0, 4).sort().join(', ');
if (unique.length > 4) entitiesStr += ` (+${unique.length - 4} more)`;
console.log(` ${item.ruid} ← ${entitiesStr}`);
}
console.log();
}
}
main();
#!/usr/bin/env node
'use strict';
/**
* UI Template Structure Viewer — shows entity hierarchy, layout, and key properties.
*
* Usage:
* node ui-structure.js --style 1 # show all .ui files in style
* node ui-structure.js --style 1 --file ButtonGroup.ui # show specific file
* node ui-structure.js --style 1 --file PopupGroup.ui --depth 2 # limit hierarchy depth
* node ui-structure.js --style 1 --entity BasicPopup # dump full JSON for entity
* node ui-structure.js --style 1 --grep ExitButton # search entity name across all files
*/
const fs = require('fs');
const path = require('path');
const SCRIPT_DIR = __dirname;
const SKILL_DIR = path.dirname(SCRIPT_DIR);
const STYLES = {
'1': 'style-1-black',
'2': 'style-2-diary',
'3': 'style-3-wood',
'4': 'style-4-blue',
};
function parseArgs() {
const argv = process.argv.slice(2);
const result = { style: null, file: null, depth: null, entity: null, grep: null };
for (let i = 0; i < argv.length; i++) {
if ((argv[i] === '--style' || argv[i] === '-s') && argv[i + 1]) {
result.style = argv[++i];
} else if ((argv[i] === '--file' || argv[i] === '-f') && argv[i + 1]) {
result.file = argv[++i];
} else if ((argv[i] === '--depth' || argv[i] === '-d') && argv[i + 1]) {
result.depth = parseInt(argv[++i], 10);
} else if ((argv[i] === '--entity' || argv[i] === '-e') && argv[i + 1]) {
result.entity = argv[++i];
} else if ((argv[i] === '--grep' || argv[i] === '-g') && argv[i + 1]) {
result.grep = argv[++i];
}
}
return result;
}
function getAlignmentName(val) {
const names = {
0: 'Center', 1: 'Left', 2: 'Right',
3: 'TopCenter', 4: 'TopLeft', 5: 'TopRight',
6: 'BottomCenter', 7: 'BottomLeft', 8: 'BottomRight',
9: 'HStretchTop', 10: 'HStretchCenter', 11: 'HStretchBottom',
12: 'VStretchLeft', 13: 'VStretchCenter', 14: 'VStretchRight',
15: 'StretchAll',
};
return names[val] || String(val);
}
function summarizeEntity(js) {
const info = {};
info.name = js.name || '?';
info.enable = js.enable !== undefined ? js.enable : true;
info.displayOrder = js.displayOrder || 0;
const comps = js['@components'] || [];
const compTypes = [];
for (const comp of comps) {
let t = (comp['@type'] || '').replace('MOD.Core.', '');
compTypes.push(t);
if (t === 'UITransformComponent') {
const align = comp.AlignmentOption || 0;
info.align = getAlignmentName(align);
const pos = comp.anchoredPosition || {};
info.pos = `(${Math.round(pos.x || 0)}, ${Math.round(pos.y || 0)})`;
const size = comp.RectSize || {};
info.size = `${Math.round(size.x || 0)}x${Math.round(size.y || 0)}`;
} else if (t === 'UIGroupComponent') {
info.groupOrder = comp.GroupOrder || 0;
info.defaultShow = comp.DefaultShow !== undefined ? comp.DefaultShow : true;
} else if (t === 'ButtonComponent') {
compTypes[compTypes.length - 1] = 'Button';
} else if (t === 'TextComponent') {
const text = comp.Text || '';
if (text) info.text = text.slice(0, 30);
} else if (t === 'ScrollLayoutGroupComponent') {
compTypes[compTypes.length - 1] = 'ScrollLayout';
}
}
info.components = compTypes.filter(c => c !== 'UITransformComponent');
return info;
}
function printTree(entities, maxDepth) {
const sorted = entities.slice().sort((a, b) => {
const aJs = a.jsonString || {};
const bJs = b.jsonString || {};
const pathCmp = (aJs.path || '').localeCompare(bJs.path || '');
if (pathCmp !== 0) return pathCmp;
return (aJs.displayOrder || 0) - (bJs.displayOrder || 0);
});
for (const ent of sorted) {
const js = ent.jsonString || {};
const entPath = js.path || '';
const depth = (entPath.match(/\//g) || []).length - 2;
if (maxDepth !== null && depth > maxDepth) continue;
const indent = ' '.repeat(Math.max(0, depth));
const info = summarizeEntity(js);
const parts = [info.name];
if (info.groupOrder !== undefined) parts.push(`GroupOrder=${info.groupOrder}`);
if (info.defaultShow === false) parts.push('hidden');
if (info.align) parts.push(info.align);
if (info.pos && info.pos !== '(0, 0)') parts.push(info.pos);
if (info.size && info.size !== '0x0') parts.push(info.size);
const notableComps = (info.components || []).filter(
c => c !== 'CanvasGroupComponent' && c !== 'SpriteGUIRendererComponent'
);
if (notableComps.length) parts.push(`[${notableComps.join(', ')}]`);
if (info.text) parts.push(`"${info.text}"`);
if (!info.enable) parts.push('(disabled)');
console.log(`${indent}${parts.join(' | ')}`);
}
}
function findEntity(styleDir, entityName, targetFile) {
const files = targetFile
? [targetFile]
: fs.readdirSync(styleDir).filter(f => f.endsWith('.ui')).sort();
let found = false;
for (const fname of files) {
const filepath = path.join(styleDir, fname);
if (!fs.existsSync(filepath)) continue;
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
const entities = (data.ContentProto && data.ContentProto.Entities) || [];
for (const ent of entities) {
const js = ent.jsonString || {};
const name = js.name || '';
if (name.toLowerCase() === entityName.toLowerCase()) {
found = true;
console.log(`\n--- ${fname} / ${js.path || ''} ---`);
console.log(JSON.stringify(ent, null, 2));
}
}
}
if (!found) {
console.log(`Entity '${entityName}' not found.`);
console.log('Tip: use --grep to search partial names.');
}
}
function grepEntities(styleDir, keyword, targetFile) {
const files = targetFile
? [targetFile]
: fs.readdirSync(styleDir).filter(f => f.endsWith('.ui')).sort();
const results = [];
for (const fname of files) {
const filepath = path.join(styleDir, fname);
if (!fs.existsSync(filepath)) continue;
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
const entities = (data.ContentProto && data.ContentProto.Entities) || [];
for (const ent of entities) {
const js = ent.jsonString || {};
const name = js.name || '';
const entPath = js.path || '';
if (name.toLowerCase().includes(keyword.toLowerCase()) ||
entPath.toLowerCase().includes(keyword.toLowerCase())) {
const info = summarizeEntity(js);
results.push({ file: fname, path: entPath, info });
}
}
}
if (!results.length) {
console.log(`No entities matching '${keyword}' found.`);
return;
}
console.log(`Found ${results.length} entities matching '${keyword}':\n`);
for (const { file, path: p, info } of results) {
const parts = [info.name];
if (info.align) parts.push(info.align);
if (info.size && info.size !== '0x0') parts.push(info.size);
const notable = (info.components || []).filter(
c => c !== 'CanvasGroupComponent' && c !== 'SpriteGUIRendererComponent'
);
if (notable.length) parts.push(`[${notable.join(', ')}]`);
if (!info.enable) parts.push('(disabled)');
console.log(` ${file} ${p}`);
console.log(` ${parts.join(' | ')}`);
}
console.log(`\nUse --entity <name> to dump full JSON for a specific entity.`);
}
function processFile(filepath, maxDepth) {
const data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
const filename = path.basename(filepath);
const entities = (data.ContentProto && data.ContentProto.Entities) || [];
console.log(`\n${'='.repeat(60)}`);
console.log(` ${filename} (${entities.length} entities)`);
console.log(`${'='.repeat(60)}`);
printTree(entities, maxDepth);
}
function main() {
const args = parseArgs();
if (!args.style) {
console.error("Error: --style is required. Use --style 1-4.");
process.exit(1);
}
if (!STYLES[args.style]) {
console.error(`Error: style must be 1-4, got '${args.style}'`);
process.exit(1);
}
const styleDir = path.join(SKILL_DIR, STYLES[args.style]);
if (args.entity) {
findEntity(styleDir, args.entity, args.file);
return;
}
if (args.grep) {
grepEntities(styleDir, args.grep, args.file);
return;
}
if (args.file) {
const filepath = path.join(styleDir, args.file);
if (!fs.existsSync(filepath)) {
console.error(`Error: ${filepath} not found`);
const available = fs.readdirSync(styleDir).filter(f => f.endsWith('.ui'));
console.error(`Available: ${available.join(', ')}`);
process.exit(1);
}
processFile(filepath, args.depth);
} else {
const files = fs.readdirSync(styleDir).filter(f => f.endsWith('.ui')).sort();
for (const fname of files) {
processFile(path.join(styleDir, fname), args.depth);
}
}
}
main();
{
"Id": "",
"GameId": "",
"EntryKey": "ui://c0bbe0ec-770a-4147-9f6f-a38cc4a53ca6",
"ContentType": "x-mod/ui",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Binary",
"Entities": [
{
"id": "c0bbe0ec-770a-4147-9f6f-a38cc4a53ca6",
"path": "/ui/DefaultGroup",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.UIGroupComponent,MOD.Core.CanvasGroupComponent",
"jsonString": {
"name": "DefaultGroup",
"path": "/ui/DefaultGroup",
"nameEditable": true,
"enable": false,
"visible": true,
"localize": true,
"displayOrder": 1,
"pathConstraints": "//",
"revision": 0,
"origin": {
"type": "Model",
"entry_id": "uigroup",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uigroup",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 15,
"AnchorsMax": {
"x": 1.0,
"y": 1.0
},
"AnchorsMin": {
"x": 0.0,
"y": 0.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 0.0,
"y": 0.0
},
"OffsetMin": {
"x": 0.0,
"y": 0.0
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 1920.0,
"y": 1080.0
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": 0.0
},
"Position": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.UIGroupComponent",
"DefaultShow": true,
"GroupOrder": 1,
"GroupType": 1,
"Enable": true
},
{
"@type": "MOD.Core.CanvasGroupComponent",
"BlocksRaycasts": true,
"GroupAlpha": 1.0,
"Interactable": true,
"Enable": true
}
],
"@version": 1
}
},
{
"id": "2f850b97-b76e-432a-a22e-09f62343f7c7",
"path": "/ui/DefaultGroup/UIJoystick",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.JoystickComponent",
"jsonString": {
"name": "UIJoystick",
"path": "/ui/DefaultGroup/UIJoystick",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 0,
"pathConstraints": "///",
"revision": 0,
"origin": {
"type": "Model",
"entry_id": "UISprite",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uisprite",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 2,
"AlignmentOption": 7,
"AnchorsMax": {
"x": 0.0,
"y": 0.0
},
"AnchorsMin": {
"x": 0.0,
"y": 0.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 410.0,
"y": 330.0001
},
"OffsetMin": {
"x": 210.0,
"y": 129.999908
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 200.0,
"y": 200.0002
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 310.0,
"y": 230.0
},
"Position": {
"x": -650.0,
"y": -310.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": ""
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.0
},
"DropShadow": false,
"DropShadowAngle": 120.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 3.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": false,
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.JoystickComponent",
"Axis": 0,
"DynamicStick": true,
"Enable": true
}
],
"@version": 1
}
},
{
"id": "7c0f8bb5-cf19-437c-9bb8-97e81f836dce",
"path": "/ui/DefaultGroup/UIChat",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.ChatComponent",
"jsonString": {
"name": "UIChat",
"path": "/ui/DefaultGroup/UIChat",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 1,
"pathConstraints": "///",
"revision": 0,
"origin": {
"type": "Model",
"entry_id": "UIEmpty",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uiempty",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 4,
"AnchorsMax": {
"x": 0.0,
"y": 1.0
},
"AnchorsMin": {
"x": 0.0,
"y": 1.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 717.8341,
"y": -32.4276428
},
"OffsetMin": {
"x": 20.1109619,
"y": -471.1826
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 697.723145,
"y": 438.754944
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 368.972534,
"y": -251.805115
},
"Position": {
"x": -591.027466,
"y": 288.1949,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": ""
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.0
},
"DropShadow": false,
"DropShadowAngle": 120.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 3.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": false,
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.ChatComponent",
"Expand": true,
"UseChatBalloon": true,
"UseChatEmotion": true,
"Enable": true
}
],
"@version": 1
}
}
]
}
}@Component
script Popupbutton extends Component
@ExecSpace("ClientOnly")
@EventSender("Entity", "8f7a6453-8e9b-4098-af83-030daa451cbf")
handler HandleButtonClickEvent(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="1" then return end
--AchieveUI
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/AchieveUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/AchieveUI/TitlePanel/ExitButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "ee07ee91-e610-487c-a1b0-d2b0fcca6b57")
handler HandleButtonClickEvent2(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="2" then return end
--MonsterDexUI
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/MonsterDexUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/MonsterDexUI/TitlePanel/ExitButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "9328525a-9459-4237-a55b-af54e5109b59")
handler HandleButtonClickEvent3(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="3" then return end
--QuestUI"
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/QuestUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/QuestUI/QuestUITitle/ExitButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "b2a64e84-8997-4a02-9508-0ea893a672e0")
handler HandleButtonClickEvent4(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="4" then return end
--InventoryUI
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/InventoryUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/InventoryUI/TopPanel/ExitButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "bdca5b4a-d7f7-44d5-bce9-ec579e9ec739")
handler HandleButtonClickEvent5(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="5" then return end
--BasicPopup
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/ShopUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/ShopUI/TopPanel/ExitButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "e1bbf199-6f58-4989-9934-6c013bcd5f95")
handler HandleButtonClickEvent6(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="6" then return end
--BasicPopup
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup/PopupBtnOK")
local cancelbutton2=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup/PopupBtnCancel")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
cancelbutton2:ConnectEvent(ButtonClickEvent,cancel)
end
endStyle 1 — RUID Map
Use these ImageRUID.DataId values when applying this template's visual style.
UI Frame / Panel Backgrounds
| RUID | Used As | Entity Examples |
|---|---|---|
785cacd8c36b4e1bb9040df26683f949 | Main popup background (large panels) | AchieveUI, InventoryUI, MonsterDexBG, QuestUI, ShopUI |
b94f57f5db1646998b726cee0aaefaac | Secondary panel background | QuestHUD, UIMatch, BasicPopup |
129f02486c2baef49a41b31ce16171f6 | Content panel (inner area) | AchievePanel |
a48d4d4656454e3e872288317a8466ad | Top/bottom border strip | img_top, img_bottom |
84447f698ccc4b1aa8bacd96e1608223 | Top panel header | TopPanel |
ab150cd6b7f148d6b90e093ea754f716 | Title background / pattern bar | bg_title, img_pattern |
c2c6e3d00c4340ce97f91a50b7d89fb8 | Quest/popup title bar | QuestUITitle, txt_title |
Buttons
| RUID | Used As | Entity Examples |
|---|---|---|
6efba31a09bb434f833edaceac5fd12a | Primary action button (popup-opener) | ButtonGroup 1-6, GetRewardButton |
ebf5e286d16447ff8f01a44f009723e8 | Confirm button (OK) | PopupBtnOK |
ace4c89669454cc49e2750b5a1e0513a | Cancel button | PopupBtnCancel |
3fa84442296e4a59bcc5ed6e7ba632f6 | Exit/close button (X) | ExitButton |
658a49cc805c4d1b8440e58915ba25dd | Quest cancel button | QuestCancelButton |
c0d9d7fe51214dd4a25f78b85338b577 | Quest complete button | QuestCompleteButton |
fab46ad80f7340b88b23216a67e176d3 | Category/tab button | CategoryButton |
bf5103e7f62948999e355c322dfbff4c | Match start button | matchBtn |
c96a2755dff64f6385f689bd9eff397a | Match cancel button | matchCancelBtn |
Icons & Decorations
| RUID | Used As | Entity Examples |
|---|---|---|
af09c427a2a846e7a85896316d17726e | Decorative image | img_deco |
dca918f7a5a64890976a5044e1c62dbd | User/profile icon | icon_user |
c6ac7299e3174a9e9e1fff0de8adfdec | X mark icon (close indicator) | icon_x |
6fc91a7377b94070a60cb35d3d8ed7b7 | Title decoration | Title |
57cb5bfed8ac48b38566103fbdc729a2 | Timer icon | timer |
6212be928ee44932a246cb7e318989ad | Info text background | textWaitInfo |
e408ed1d6be94f0ca53d538bf47cb314 | Background effect | bg_effect |
Items & Slots
| RUID | Used As | Entity Examples |
|---|---|---|
d9d1070e225f46e9b445ada1bffc49ce | Item slot frame | ItemSlot, ItemSlot_1, Slot |
1aadfa1e39c3c4e4ea0339287bca2961 | Currency/money icon | MoneySprite, MoneySprite_1 |
1e5e576fa39554b4a9d4b356354219c6 | Shop item slot | ShopItemSlot |
53fcad1ac7dd4d96a03cac8f429a1da1 | Generic item sprite | Sprite, itemSprite |
09caabbe7ddd41c4ab37061d5669b312 | Specific item sprite | itemSprite |
b467d65ecabb1ae4d9770f6a7a629231 | Achievement sprite | AchieveSprite |
f86992ba9c41487c8480fcb893fcbda6 | Monster sprite placeholder | MonsterSprite, Sprite |
00a68650f2564aae9f4719d2e734bc3f | Reward item icon | RewardItemSprite |
f01ff6c1c0d25014baca8f27ba2609a0 | Reward title decoration | RewardTitle |
Selection & Indicators
| RUID | Used As | Entity Examples |
|---|---|---|
8cacae54ab5f4e2d81770f3e984c13cc | Selected state highlight | line_selected |
497f780086744a43b217af42b790b8f7 | Slot border line | slot_line |
02a3d8b1628d4f0cbbc69feaab59e365 | Description/quest info background | QuestDescription, Title |
Toast
| RUID | Used As | Entity Examples |
|---|---|---|
7d614552ba7843049bb48ebd4509fb8f | Toast message background | Toast_message |
Style 1 (Black) — UI Structure
Format: Name | Alignment | (x, y) | WxH | [Components] | (state)
============================================================
ButtonGroup.ui (13 entities)
============================================================
ButtonGroup | GroupOrder=3 | StretchAll | 1920x1080 | [UIGroupComponent]
1 | Center | (-829, 196) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 1"
2 | Center | (-829, 97) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 2"
3 | Center | (-829, -4) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 3"
4 | Center | (-829, -104) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 4"
5 | Center | (-829, -201) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 5"
6 | Center | (-829, -295) | 235x88 | [Button, script.Popupbutton]
UIText | Center | (-1, 7) | 100x100 | [TextComponent] | "Popup 6"
============================================================
DefaultGroup.ui (3 entities)
============================================================
DefaultGroup | GroupOrder=1 | StretchAll | 1920x1080 | [UIGroupComponent] | (disabled)
UIChat | TopLeft | (369, -252) | 698x439 | [ChatComponent]
UIJoystick | BottomLeft | (310, 230) | 200x200 | [JoystickComponent]
============================================================
HUDGroup.ui (22 entities)
============================================================
HUDGroup | GroupOrder=0 | StretchAll | 1920x1080 | [UIGroupComponent]
QuestHUD | TopCenter | (-486, -25) | 465x220 | [Button]
QuestDescription | StretchAll | (1, -28) | 463x165 | [TextComponent] | "Defeat Orange Mushroom 0/10
Defeat Slime 0/10"
Title | HStretchTop | 465x57 | [TextComponent] | "Main Quest"
bg_title | HStretchTop | (-0, -0) | 465x55
UIMatch | TopCenter | (90, -38) | 540x340
Title | HStretchTop | 510x70 | [TextComponent] | "Mini Game Open!"
img_pattern | StretchAll | 510x310
matchBtn | HStretchBottom | (81, 38) | 300x70 | [Button]
matchCancelBtn | HStretchBottom | (81, 38) | 300x70 | [Button]
textMatchGame | HStretchTop | (0, -103) | 510x50 | [TextComponent] | "Mini Game Name"
textMatchInfo | HStretchTop | (0, -148) | 510x50 | [TextComponent] | "Max Players: 1~10"
textRecommendedLevel | HStretchTop | (0, -193) | 510x50 | [TextComponent] | "Recommended Level: LV 5 ↑"
textWaitInfo | BottomLeft | (43, 43) | 157x61 | [TextComponent] | "0"
timer | TopLeft | (31, -20) | 82x91
============================================================
PopupGroup.ui (131 entities)
============================================================
PopupGroup | GroupOrder=4 | StretchAll | 1920x1080 | [UIGroupComponent]
AchieveUI | Center | (0, -0) | 1408x680 | (disabled)
Panel_slot | StretchAll | (0, -35) | 1328x530 | [GridViewComponent]
TitlePanel | HStretchTop | 1408x100
BasicPopup | Center | (8, 5) | 981x508 | (disabled)
PopupBtnCancel | Center | (206, -155) | 415x105 | [Button, TextComponent] | "Cancel"
PopupBtnOK | Center | (-206, -155) | 415x105 | [Button, TextComponent] | "OK"
PopupMessage | Center | (0, 80) | 780x260 | [TextComponent] | "Popup Message"
deco_line | HStretchCenter | (0, -70) | 945x2
img_deco | StretchAll | (0, -0) | 955x482
img_pattern | StretchAll | 951x478
InventoryUI | Center | (-32, -11) | 740x1046 | (disabled)
BottomPanel | HStretchBottom | (0, 13) | 714x80
CategoryPanel | HStretchTop | (0, -124) | 646x88 | [ScrollLayout]
ContentPanel | StretchAll | (40, 93) | 660x718 | [ScrollLayout]
TopPanel | HStretchTop | 732x97
MonsterDexUI | Center | (0, -18) | 1455x1066 | (disabled)
AchieveBG | HStretchBottom | (0, 60) | 1351x17
CategoryPanel | TopLeft | (62, -124) | 602x86 | [ScrollLayout]
MonsterDexBG | StretchAll | (0, 10) | 1455x1046
Panel_info | TopRight | (-62, -124) | 700x829
Panel_slot | StretchAll | (40, 103) | 615x728
TitlePanel | HStretchTop | 1455x100
QuestUI | Center | (0, -26) | 1376x954 | (disabled)
LeftPanel | StretchAll | (-344, -38) | 623x811 | [ScrollLayout]
QuestUITitle | HStretchTop | 1376x97
RightPanel | VStretchRight | (-366, -48) | 706x831
ShopUI | Center | (-14, 31) | 1600x800 | (disabled)
CategoryPanel | VStretchLeft | (43, -115) | 200x642 | [ScrollLayout]
ContentsPanel | StretchAll | (-43, -115) | 1277x642 | [ScrollLayout]
TopPanel | HStretchTop | 1592x97
============================================================
ToastGroup.ui (2 entities)
============================================================
ToastGroup | GroupOrder=2 | hidden | StretchAll | 1920x1080 | [UIGroupComponent] | (disabled)
Toast_message | TopCenter | (0, -64) | 210x79 | [TextComponent] | "message"{
"Id": "",
"GameId": "",
"EntryKey": "ui://0bc398f9-29cb-4d89-a78d-f24742b117e8",
"ContentType": "x-mod/ui",
"Content": "",
"Usage": 0,
"UsePublish": 1,
"UseService": 0,
"CoreVersion": "26.5.0.0",
"StudioVersion": "0.1.0.0",
"DynamicLoading": 0,
"ContentProto": {
"Use": "Binary",
"Entities": [
{
"id": "0bc398f9-29cb-4d89-a78d-f24742b117e8",
"path": "/ui/ToastGroup",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.UIGroupComponent,MOD.Core.CanvasGroupComponent",
"jsonString": {
"name": "ToastGroup",
"path": "/ui/ToastGroup",
"nameEditable": true,
"enable": false,
"visible": true,
"localize": true,
"displayOrder": 2,
"pathConstraints": "//",
"revision": 0,
"origin": {
"type": "Model",
"entry_id": "uigroup",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uigroup",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 15,
"AnchorsMax": {
"x": 1.0,
"y": 1.0
},
"AnchorsMin": {
"x": 0.0,
"y": 0.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 0.0,
"y": 0.0
},
"OffsetMin": {
"x": 0.0,
"y": 0.0
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 1920.0,
"y": 1080.0
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": 0.0
},
"Position": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.UIGroupComponent",
"DefaultShow": false,
"GroupOrder": 2,
"GroupType": 1,
"Enable": true
},
{
"@type": "MOD.Core.CanvasGroupComponent",
"BlocksRaycasts": true,
"GroupAlpha": 1.0,
"Interactable": true,
"Enable": true
}
],
"@version": 1
}
},
{
"id": "7af9e538-1713-49f7-a83e-711c0f4b4a3c",
"path": "/ui/ToastGroup/Toast_message",
"componentNames": "MOD.Core.UITransformComponent,MOD.Core.SpriteGUIRendererComponent,MOD.Core.TextComponent,MOD.Core.CanvasGroupComponent",
"jsonString": {
"name": "Toast_message",
"path": "/ui/ToastGroup/Toast_message",
"nameEditable": true,
"enable": true,
"visible": true,
"localize": true,
"displayOrder": 0,
"pathConstraints": "///",
"revision": 0,
"origin": {
"type": "Model",
"entry_id": "UISprite",
"sub_entity_id": null,
"root_entity_id": null,
"replaced_model_id": null
},
"modelId": "uisprite",
"@components": [
{
"@type": "MOD.Core.UITransformComponent",
"ActivePlatform": 255,
"AlignmentOption": 3,
"AnchorsMax": {
"x": 0.5,
"y": 1.0
},
"AnchorsMin": {
"x": 0.5,
"y": 1.0
},
"MobileOnly": false,
"OffsetMax": {
"x": 105.221237,
"y": -24.353981
},
"OffsetMin": {
"x": -105.221237,
"y": -103.646019
},
"Pivot": {
"x": 0.5,
"y": 0.5
},
"RectSize": {
"x": 210.442474,
"y": 79.29204
},
"UIMode": 1,
"UIScale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"UIVersion": 2,
"anchoredPosition": {
"x": 0.0,
"y": -64.0
},
"Position": {
"x": 0.0,
"y": 476.0,
"z": 0.0
},
"QuaternionRotation": {
"x": 0.0,
"y": 0.0,
"z": 0.0,
"w": 1.0
},
"Scale": {
"x": 1.0,
"y": 1.0,
"z": 1.0
},
"Enable": true
},
{
"@type": "MOD.Core.SpriteGUIRendererComponent",
"AnimClipPlayType": 0,
"EndFrameIndex": 2147483647,
"ImageRUID": {
"DataId": "7d614552ba7843049bb48ebd4509fb8f"
},
"LocalPosition": {
"x": 0.0,
"y": 0.0
},
"LocalScale": {
"x": 1.0,
"y": 1.0
},
"OverrideSorting": false,
"PlayRate": 1.0,
"PreserveSprite": 0,
"StartFrameIndex": 0,
"Color": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 0.6
},
"DropShadow": false,
"DropShadowAngle": 120.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 3.0,
"FillAmount": 1.0,
"FillCenter": true,
"FillClockWise": true,
"FillMethod": 0,
"FillOrigin": 0,
"FlipX": false,
"FlipY": false,
"FrameColumn": 1,
"FrameRate": 0,
"FrameRow": 1,
"Outline": false,
"OutlineColor": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 1.0
},
"OutlineWidth": 3.0,
"RaycastTarget": true,
"Type": 1,
"Enable": true
},
{
"@type": "MOD.Core.TextComponent",
"Alignment": 4,
"Bold": false,
"DropShadow": false,
"DropShadowAngle": 120.0,
"DropShadowColor": {
"r": 0.0,
"g": 0.0,
"b": 0.0,
"a": 0.72
},
"DropShadowDistance": 3.0,
"Font": 0,
"FontColor": {
"r": 1.0,
"g": 1.0,
"b": 1.0,
"a": 1.0
},
"FontSize": 27,
"MaxSize": 40,
"MinSize": 10,
"OutlineColor": {
"r": 0.7,
"g": 0.7,
"b": 0.7,
"a": 1.0
},
"OutlineDistance": {
"x": 1.0,
"y": -1.0
},
"OutlineWidth": 1.0,
"Overflow": 0,
"OverrideSorting": false,
"Padding": {
"left": 50,
"right": 50,
"top": 20,
"bottom": 20
},
"SizeFit": true,
"Text": "message",
"UseOutLine": false,
"Enable": true
},
{
"@type": "MOD.Core.CanvasGroupComponent",
"BlocksRaycasts": true,
"GroupAlpha": 1.0,
"Interactable": true,
"Enable": true
}
],
"@version": 1
}
}
]
}
}@Component
script Popupbutton extends Component
@ExecSpace("ClientOnly")
@EventSender("Entity", "a721af74-fd2c-4c59-aacf-9733620d1d6e")
handler HandleButtonClickEvent(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="1" then return end
--GameResultPanel
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/GameResultPanel")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/GameResultPanel/Btn_Ok_1")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "02210bfa-a0b8-485e-9e0c-17062d93b17f")
handler HandleButtonClickEvent2(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="2" then return end
--InventoryUI
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/InventoryUI")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/InventoryUI/CloseButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "5ec12c2a-4125-46a7-8d58-ce8f68155fd4")
handler HandleButtonClickEvent3(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="3" then return end
--BasicPopup2
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup2")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup2/CloseButton")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
end
@ExecSpace("ClientOnly")
@EventSender("Entity", "e07d430e-5837-4582-91e7-3e5bf45af75b")
handler HandleButtonClickEvent4(ButtonClickEvent event)
-- Parameters
local Entity = event.Entity
--------------------------------------------------------
if Entity.Name~="4" then return end
--BasicPopup
local panel=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup")
panel.Enable=true
local cancelbutton=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup/Btn_Ok")
local cancelbutton2=_EntityService:GetEntityByPath("/ui/PopupGroup/BasicPopup/Btn_No")
local cancel=function()
panel.Enable=false
end
cancelbutton:ConnectEvent(ButtonClickEvent,cancel)
cancelbutton2:ConnectEvent(ButtonClickEvent,cancel)
end
endStyle 2 — RUID Map
Use these ImageRUID.DataId values when applying this template's visual style.
UI Frame / Panel Backgrounds
| RUID | Used As | Entity Examples |
|---|---|---|
870b7dd654e74c319e30b5a7ef970c93 | Main popup background | BasicPopup, GameResultPanel |
c45f4cb1bc674d05861f94d8ca98d97f | Secondary popup background | BasicPopup2, InventoryUI |
1e5e576fa39554b4a9d4b356354219c6 | Filter/main panel background | FilterPanel, MainPanel |
c58c6f4200d4b7341ba11994d157ce12 | Index/info panel | IndexPanel, Item_Desc, MyResultPanel |
ea7ba4a4b0c64c1c85e5982b2265a60c | Side background / line | Img_Bg_Left, Img_Bg_Right, Img_Bg_1, Img_Line_1_1_1, Item_Info |
cd907e728d324f6b85eac245495a8178 | Slot panel (left) | Left_SlotPanel |
216760e07f4d4385a7b8a754fb1ee7f1 | Scroll area background | SlotScroll |
3c43d48aaddbad849811698c4e8bf48f | Count panel | CountPanel |
d40eb19e3274438cbeb4885b61c44a2c | Quantity group | Group_Quantity |
HUD
| RUID | Used As | Entity Examples |
|---|---|---|
508fa7a5b759485caff7292ef1b50632 | Player profile panel | PlrprofileUI |
c2b7536965db46099801551b0b31f055 | HUD main panel | Panel |
dcb441051ce34d7ba63961a4d37230c8 | Level/heart icon | Level, img_heart |
4d9d1f67e2e648f3a6329b1cee207207 | Name tag background | Name |
8201d19db2724615931078a647ad3037 | EXP bar panel | Exp_Panel |
9e9a29005c204faf940104e65bb14941 | Profile image frame | profileimage |
900b00b6c24b40bb8a77fa4071127c0e | Arrow icon | img_arrow |
Buttons
| RUID | Used As | Entity Examples |
|---|---|---|
52b4cca09a7d44979c78526b5e7fc30f | Standard button (popup-opener, filter) | Popupbutton 1-4, BtnFilter_1/2/3 |
c11e28b09bde4b688cf7e0f8c5809bff | Confirm/action button (OK, Use) | Btn_Ok, Btn_Ok_1, Btn_Use |
0beee75ea68047e9a33255c0d0c94642 | Negative/junk button | Btn_No, Btn_Junk |
2d4fa57a26834636be1c58968c0ba165 | Close button (X) | CloseButton |
db2f0710e2cc45a5b4df4d7495543741 | Up/down arrow button | Btn_Up, Btn_Down |
d43c317f0abf4626ad82de6ed96ce0fd | Expand/volume button | BtnExpandVolume |
Items & Slots
| RUID | Used As | Entity Examples |
|---|---|---|
254699159ff244a1a5eb20f41092d41b | Item slot frame | Slot_1 |
67d9ae562627455e8b0f6bc6037f6911 | Item icon placeholder | Item_Icon |
6254955eae3f40fabe379fdc95fd14e3 | Generic icon | Icon |
ecb92c04ab7746af92c4912215972d52 | Accessory slot (multiple) | Img_Acc2_* |
e51cb7273c8d48f1b592f1bdd8227117 | Accessory accent | Img_Acc_1_1 |
9087159159fb485ea1b4d389aa90b7c1 | Clip/accessory | Img_Acc_2, Img_Clip_1 |
Decorations & Lines
| RUID | Used As | Entity Examples |
|---|---|---|
217c8901a72e43be8acfde341bf023ea | Horizontal divider line | Img_Line, Img_Line_1_1 |
f7627c11bc0440e4bc4238a8bcceb501 | Line variant | Img_Line |
bbcce9be23084efa9af3dab5107c68cf | Line variant | Img_Line |
4fea64a3307cda641809ad8be0d4890b | Line variant | Img_Line |
3cd6bae06a45467fad6eae8ebe497f30 | Background pattern | Img_Bg_pattern_1 |
c5acf3d23b68443d94c61ff3279ea41d | Clip decoration | Img_Clip |
25e5f99bdc562c241bf1b96a0d76f493 | Title panel decoration | TitlePanel |
Info & Status
| RUID | Used As | Entity Examples |
|---|---|---|
92efbba98ce14172a96e7ff874cfe799 | Currency/gold display background | UserGoldPanel, CurVolumeText |
17b4d7cdaf3c4d3f8b52465c9350d840 | Secondary info panel | Info2 |
0c09153ba47f4202a2db1858c227b329 | Right side background | Bg_Right |
Toast
| RUID | Used As | Entity Examples |
|---|---|---|
7d614552ba7843049bb48ebd4509fb8f | Toast message background | Toast_message |
Related skills
How it compares
Pick msw-ui-system over generic frontend skills when building MapleStory Worlds .ui/.mlua client interfaces with engine-specific components.
FAQ
Can agents edit .ui JSON directly?
No. All .ui creation and modification must go through scripts/msw_ui_builder.cjs.
What alignment default fixes centered text sticking left?
Verify text Alignment default is UpperLeft(0) to avoid most centering layout bugs.
What is the minimum mobile button touch target?
Button touch targets must be at least 88 by 88 for mobile support.
Is Msw Ui System safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.