Compare commits
2 Commits
cdab525858
...
a0550daf4b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0550daf4b | ||
|
|
9425f9b19a |
86
UI.md
86
UI.md
@ -26,6 +26,7 @@ These properties are available on every node type.
|
|||||||
| `height` | float \| `"match_parent"` | `0` | Height in virtual pixels |
|
| `height` | float \| `"match_parent"` | `0` | Height in virtual pixels |
|
||||||
| `horizontal_gravity` | `"left"` \| `"center"` \| `"right"` | `"left"` | Positions the node horizontally inside a **FrameLayout** parent |
|
| `horizontal_gravity` | `"left"` \| `"center"` \| `"right"` | `"left"` | Positions the node horizontally inside a **FrameLayout** parent |
|
||||||
| `vertical_gravity` | `"bottom"` \| `"center"` \| `"top"` | `"bottom"` | Positions the node vertically inside a **FrameLayout** parent |
|
| `vertical_gravity` | `"bottom"` \| `"center"` \| `"top"` | `"bottom"` | Positions the node vertically inside a **FrameLayout** parent |
|
||||||
|
| `visible` | bool | `true` | Whether the node (and all its children) are rendered and interactive. Can be toggled at runtime via `setNodeVisible` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -343,6 +344,42 @@ A non-interactive image. Supports optional fade-in and pulse-scale animations.
|
|||||||
| `pulse.maxScale` | float | `1.1` | Maximum scale during the pulse cycle |
|
| `pulse.maxScale` | float | `1.1` | Maximum scale during the pulse cycle |
|
||||||
| `pulse.periodMs` | float | `1000` | Duration of one full pulse cycle in milliseconds |
|
| `pulse.periodMs` | float | `1000` | Duration of one full pulse cycle in milliseconds |
|
||||||
|
|
||||||
|
**C++ pop-in animation** (scales the node from 0 → 1, ease-out quad):
|
||||||
|
```cpp
|
||||||
|
uiManager.startPopIn("background", 300.0f); // duration in milliseconds
|
||||||
|
```
|
||||||
|
Typically called immediately after making a node visible. The node is automatically removed from the animation list when the scale reaches 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Touch / Click Priority
|
||||||
|
|
||||||
|
All `Button` and `TextButton` nodes in a layout are collected into a single ordered list during `collectButtonsAndSliders` (depth-first traversal of the node tree, which matches JSON declaration order). When a touch or mouse-down event arrives, the list is scanned **in reverse** — later-declared nodes are checked first — and the **first hit wins**. At most one element (button or textButton, regardless of type) fires per touch.
|
||||||
|
|
||||||
|
**Practical rule:** place background "catch-all" elements (e.g. a full-screen transparent exit button) **early** in the JSON, and foreground interactive elements **later**. The later-declared element will always win when they overlap.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "FrameLayout",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "phoneExitButton", // declared first → lowest priority
|
||||||
|
"width": "match_parent",
|
||||||
|
"height": "match_parent",
|
||||||
|
"textures": { "normal": "resources/transparent.png", ... }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextButton",
|
||||||
|
"name": "chat2button", // declared later → wins over phoneExitButton
|
||||||
|
...
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This behaviour is consistent across `Button` and `TextButton` — there is no inherent type priority, only declaration order matters.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Animations
|
## Animations
|
||||||
@ -433,9 +470,58 @@ uiManager.setNodeVisible("hint5", false);
|
|||||||
bool visible = uiManager.getNodeVisible("hint5");
|
bool visible = uiManager.getNodeVisible("hint5");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Pop-in animation
|
||||||
|
|
||||||
|
Scales a node from 0 to 1 using an ease-out curve. Useful for chat bubble reveals and similar "appear" effects.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
uiManager.startPopIn("messageBubble", 300.0f); // node name, duration ms
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the node's `scaleX`/`scaleY` to `0` and call `setNodeVisible` before calling `startPopIn` to avoid a one-frame flash at full size.
|
||||||
|
|
||||||
|
### Dynamic node repositioning
|
||||||
|
|
||||||
|
`node->localY` (and `localX`) can be modified directly on a node pointer, then a layout recalculation applied:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto node = uiManager.findNode("messageBubble");
|
||||||
|
node->localY = 350.0f; // new bottom-Y (for vertical_gravity: bottom nodes)
|
||||||
|
uiManager.updateAllLayouts(); // recomputes screenRect and rebuilds meshes
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how the phone chat manager repositions bubbles as new messages arrive.
|
||||||
|
|
||||||
### Per-frame update
|
### Per-frame update
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
uiManager.update(deltaMs); // advance animations and fade-ins
|
uiManager.update(deltaMs); // advance animations and fade-ins
|
||||||
uiManager.draw(renderer); // render everything
|
uiManager.draw(renderer); // render everything
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dialogue → UI integration (phone chat bubbles)
|
||||||
|
|
||||||
|
Dialogue nodes in JSON can carry a `"bubbleSlot"` field naming a `StaticImage` UI node. When the dialogue runtime presents that line, it fires the `onBubbleSlotReady` callback with the slot name, which the game uses to reveal the corresponding bubble image.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"text": "...",
|
||||||
|
"next": "line_2",
|
||||||
|
"bubbleSlot": "message01in"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lines without `"bubbleSlot"` (or with an empty value) do not trigger any UI change — useful for internal monologue lines that have no corresponding chat image.
|
||||||
|
|
||||||
|
**C++ wiring:**
|
||||||
|
```cpp
|
||||||
|
dialogueSystem.setOnBubbleSlotReady([](const std::string& slotName) {
|
||||||
|
// slotName == "message01in" etc.
|
||||||
|
menuManager.revealPhoneChatBubble(slotName);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|||||||
@ -35,6 +35,143 @@
|
|||||||
"type": "End"
|
"type": "End"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dialog_chat_parents001",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Отец",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Бекзат, мы тебе отправили немного денег, постарайся прожить на эти деньги до конца недели!",
|
||||||
|
"next": "line_2",
|
||||||
|
"bubbleSlot": "message01in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Бекзат",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Спасибо!",
|
||||||
|
"next": "end_1",
|
||||||
|
"bubbleSlot": "message02out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dialog_chat_aiperi001",
|
||||||
|
"start": "line_1",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "line_1",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Бекзат, помнишь мы скидывались на торт для Аиды Джаныбековой? Я тогда еще приносила скатерть, тарелки и нож для торта. И я до сих пор не получила назад ничего.",
|
||||||
|
"next": "line_2",
|
||||||
|
"bubbleSlot": "message01in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_2",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Бекзат",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Скатерть и тарелки вроде бы лежат в студзоне.",
|
||||||
|
"next": "line_3",
|
||||||
|
"bubbleSlot": "message02out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_3",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "А нож?",
|
||||||
|
"next": "line_4",
|
||||||
|
"bubbleSlot": "message03in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_4",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Бекзат",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Нож, наверное, так и остался в учительской.",
|
||||||
|
"next": "line_5",
|
||||||
|
"bubbleSlot": "message04out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_5",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "А давай не \"наверное\"?",
|
||||||
|
"next": "line_6",
|
||||||
|
"bubbleSlot": "message05in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_6",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "А давай ты приедешь в универ, зайдешь в учительскую, заберешь нож и отдашь мне?",
|
||||||
|
"next": "line_7",
|
||||||
|
"bubbleSlot": "message06in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_7",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "У вас сегодня как раз Аида ведет лекцию. После лекции попросишь у нее ключи от учительской и заберешь нож.",
|
||||||
|
"next": "line_8",
|
||||||
|
"bubbleSlot": "message07in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_8",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Бекзат",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Почему ты сама не можешь забрать?",
|
||||||
|
"next": "line_9",
|
||||||
|
"bubbleSlot": "message08out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_9",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Ты же знаешь, если я встречу Аиду, она 100% даст мне какое-нибудь сложное задание.",
|
||||||
|
"next": "line_10",
|
||||||
|
"bubbleSlot": "message09in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_10",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "И потом, это ты у меня брал нож, с чего я должна ходить искать его по всему универу?",
|
||||||
|
"next": "line_11",
|
||||||
|
"bubbleSlot": "message10in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "line_11",
|
||||||
|
"type": "Line",
|
||||||
|
"speaker": "Айпери",
|
||||||
|
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
|
||||||
|
"text": "Так что жду тебя в универе! Не вздумай прогулять!",
|
||||||
|
"next": "end_1",
|
||||||
|
"bubbleSlot": "message11in"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "end_1",
|
||||||
|
"type": "End"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dialog_no_sleep001",
|
"id": "dialog_no_sleep001",
|
||||||
|
|||||||
BIN
resources/w/ui/img/phone/chat02_09in.png
(Stored with Git LFS)
BIN
resources/w/ui/img/phone/chat02_09in.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/w/ui/img/phone/chat02_10in.png
(Stored with Git LFS)
BIN
resources/w/ui/img/phone/chat02_10in.png
(Stored with Git LFS)
Binary file not shown.
BIN
resources/w/ui/img/phone/chat02_11in.png
(Stored with Git LFS)
Normal file
BIN
resources/w/ui/img/phone/chat02_11in.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -69,9 +69,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 103.6,
|
"height": 103.6,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 100,
|
"y" : 506.4,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "top",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat01_01in.png"
|
"texture": "resources/w/ui/img/phone/chat01_01in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -80,9 +81,10 @@
|
|||||||
"width": 116.2,
|
"width": 116.2,
|
||||||
"height": 43.4,
|
"height": 43.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 203.6,
|
"y" : 453,
|
||||||
"horizontal_gravity": "right",
|
"horizontal_gravity": "right",
|
||||||
"vertical_gravity": "top",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat01_02out.png"
|
"texture": "resources/w/ui/img/phone/chat01_02out.png"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
189
resources/w/ui/screen_phone_chat2 — копия.json
Normal file
189
resources/w/ui/screen_phone_chat2 — копия.json
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"root": {
|
||||||
|
"type": "FrameLayout",
|
||||||
|
"name": "hud_root",
|
||||||
|
"width": "match_parent",
|
||||||
|
"height": "match_parent",
|
||||||
|
"vertical_align": "center",
|
||||||
|
"horizontal_align": "center",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "phoneExitButton",
|
||||||
|
"horizontal_gravity": "center",
|
||||||
|
"vertical_gravity": "center",
|
||||||
|
"y": 0,
|
||||||
|
"width": "match_parent",
|
||||||
|
"height": "match_parent",
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/transparent.png",
|
||||||
|
"hover": "resources/transparent.png",
|
||||||
|
"pressed": "resources/transparent.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Button",
|
||||||
|
"name": "phoneMain",
|
||||||
|
"horizontal_gravity": "center",
|
||||||
|
"vertical_gravity": "center",
|
||||||
|
"y": -60,
|
||||||
|
"width": 617.4,
|
||||||
|
"height": 991.2,
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/w/ui/img/phone/PhoneChat001.png",
|
||||||
|
"hover": "resources/w/ui/img/phone/PhoneChat001.png",
|
||||||
|
"pressed": "resources/w/ui/img/phone/PhoneChat001.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message01in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 148.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 1097,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_01in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message02out",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 64.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 1022.6,
|
||||||
|
"horizontal_gravity": "right",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_02out.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message03in",
|
||||||
|
"width": 103.6,
|
||||||
|
"height": 43.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 969.2,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_03in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message04out",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 64.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 894.8,
|
||||||
|
"horizontal_gravity": "right",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_04out.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message05in",
|
||||||
|
"width": 243.6,
|
||||||
|
"height": 43.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 841.4,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_05in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message06in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 85.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 746,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_06in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message07in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 106.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 629.6,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_07in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message08out",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 64.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 555.2,
|
||||||
|
"horizontal_gravity": "right",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_08out.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message09in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 85.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 459.8,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_09in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message10in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 85.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 364.4,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_10in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message11in",
|
||||||
|
"width": 320.6,
|
||||||
|
"height": 64.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 290,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_11in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "TextButton",
|
||||||
|
"name": "chatTitleButton",
|
||||||
|
"horizontal_gravity": "center",
|
||||||
|
"x": 0.0,
|
||||||
|
"y": 20.0,
|
||||||
|
"width": 446.25,
|
||||||
|
"height": 78.4,
|
||||||
|
"text": "Айпери",
|
||||||
|
"textPaddingY": 16.0,
|
||||||
|
"textPaddingX": 140.0,
|
||||||
|
"fontSize": 32,
|
||||||
|
"fontPath": "resources/fonts/DroidSans.ttf",
|
||||||
|
"textCentered": false,
|
||||||
|
"topAligned": true,
|
||||||
|
"wrap": true,
|
||||||
|
"color": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"textures": {
|
||||||
|
"normal": "resources/w/ui/img/phone/CharHeader001.png",
|
||||||
|
"hover": "resources/w/ui/img/phone/CharHeader001.png",
|
||||||
|
"pressed": "resources/w/ui/img/phone/CharHeader001.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -41,9 +41,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 148.4,
|
"height": 148.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 991.6,
|
"y" : 1097,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_01in.png"
|
"texture": "resources/w/ui/img/phone/chat02_01in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -52,9 +53,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 64.4,
|
"height": 64.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 917.2,
|
"y" : 1022.6,
|
||||||
"horizontal_gravity": "right",
|
"horizontal_gravity": "right",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_02out.png"
|
"texture": "resources/w/ui/img/phone/chat02_02out.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -63,9 +65,10 @@
|
|||||||
"width": 103.6,
|
"width": 103.6,
|
||||||
"height": 43.4,
|
"height": 43.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 863.8,
|
"y" : 969.2,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_03in.png"
|
"texture": "resources/w/ui/img/phone/chat02_03in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -74,9 +77,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 64.4,
|
"height": 64.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 789.4,
|
"y" : 894.8,
|
||||||
"horizontal_gravity": "right",
|
"horizontal_gravity": "right",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_04out.png"
|
"texture": "resources/w/ui/img/phone/chat02_04out.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -85,9 +89,10 @@
|
|||||||
"width": 243.6,
|
"width": 243.6,
|
||||||
"height": 43.4,
|
"height": 43.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 746,
|
"y" : 841.4,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_05in.png"
|
"texture": "resources/w/ui/img/phone/chat02_05in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -96,9 +101,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 85.4,
|
"height": 85.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 650.6,
|
"y" : 746,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_06in.png"
|
"texture": "resources/w/ui/img/phone/chat02_06in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -107,9 +113,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 106.4,
|
"height": 106.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 534.2,
|
"y" : 629.6,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_07in.png"
|
"texture": "resources/w/ui/img/phone/chat02_07in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -118,9 +125,10 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 64.4,
|
"height": 64.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 459.8,
|
"y" : 555.2,
|
||||||
"horizontal_gravity": "right",
|
"horizontal_gravity": "right",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_08out.png"
|
"texture": "resources/w/ui/img/phone/chat02_08out.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -129,21 +137,35 @@
|
|||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
"height": 85.4,
|
"height": 85.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 364.4,
|
"y" : 459.8,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
"texture": "resources/w/ui/img/phone/chat02_09in.png"
|
"texture": "resources/w/ui/img/phone/chat02_09in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "StaticImage",
|
"type": "StaticImage",
|
||||||
"name": "message10in",
|
"name": "message10in",
|
||||||
"width": 320.6,
|
"width": 320.6,
|
||||||
|
"height": 85.4,
|
||||||
|
"x" : 430,
|
||||||
|
"y" : 364.4,
|
||||||
|
"horizontal_gravity": "left",
|
||||||
|
"vertical_gravity": "bottom",
|
||||||
|
"visible": false,
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_10in.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "StaticImage",
|
||||||
|
"name": "message11in",
|
||||||
|
"width": 320.6,
|
||||||
"height": 64.4,
|
"height": 64.4,
|
||||||
"x" : 430,
|
"x" : 430,
|
||||||
"y" : 290,
|
"y" : 290,
|
||||||
"horizontal_gravity": "left",
|
"horizontal_gravity": "left",
|
||||||
"vertical_gravity": "bottom",
|
"vertical_gravity": "bottom",
|
||||||
"texture": "resources/w/ui/img/phone/chat02_10in.png"
|
"visible": false,
|
||||||
|
"texture": "resources/w/ui/img/phone/chat02_11in.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextButton",
|
"type": "TextButton",
|
||||||
|
|||||||
@ -200,7 +200,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "TextView",
|
"type": "TextView",
|
||||||
"name": "chat2msg",
|
"name": "chat3msg",
|
||||||
"x": 100.0,
|
"x": 100.0,
|
||||||
"y": 36.0,
|
"y": 36.0,
|
||||||
"width": 446.25,
|
"width": 446.25,
|
||||||
|
|||||||
24
src/Game.cpp
24
src/Game.cpp
@ -387,6 +387,18 @@ namespace ZL
|
|||||||
menuManager.onItemPickedUp(itemId);
|
menuManager.onItemPickedUp(itemId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Wire phone dialogue start function so MenuManager can trigger dialogues.
|
||||||
|
menuManager.startDialogueFunc = [this](const std::string& id) {
|
||||||
|
if (currentLocation) currentLocation->dialogueSystem.startDialogue(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wire bubble-slot callback so chat bubbles appear as dialogue lines are shown.
|
||||||
|
for (auto& [name, loc] : locations) {
|
||||||
|
loc->dialogueSystem.setOnBubbleSlotReady([this](const std::string& bubbleSlot) {
|
||||||
|
menuManager.revealPhoneChatBubble(bubbleSlot);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
currentLocation = locations["location_dorm"];
|
currentLocation = locations["location_dorm"];
|
||||||
currentLocation->scriptEngine.callLocationEnterCallback();
|
currentLocation->scriptEngine.callLocationEnterCallback();
|
||||||
|
|
||||||
@ -705,6 +717,7 @@ namespace ZL
|
|||||||
break;
|
break;
|
||||||
case SDLK_f:
|
case SDLK_f:
|
||||||
currentLocation->dialogueSystem.startDialogue("dialog_start001");
|
currentLocation->dialogueSystem.startDialogue("dialog_start001");
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case SDLK_e:
|
case SDLK_e:
|
||||||
currentLocation->dialogueSystem.startCutscene("test_cutscene_01"); //.startDialogue("test_cutscene_pan_dialogue");
|
currentLocation->dialogueSystem.startCutscene("test_cutscene_01"); //.startDialogue("test_cutscene_pan_dialogue");
|
||||||
@ -940,8 +953,11 @@ namespace ZL
|
|||||||
|
|
||||||
const int uiX = mx;
|
const int uiX = mx;
|
||||||
const int uiY = Environment::projectionHeight - my;
|
const int uiY = Environment::projectionHeight - my;
|
||||||
|
const bool dialogueActive = currentLocation && currentLocation->dialogueSystem.isActive();
|
||||||
|
if (!dialogueActive) {
|
||||||
menuManager.uiManager.onTouchDown(fingerId, uiX, uiY);
|
menuManager.uiManager.onTouchDown(fingerId, uiX, uiY);
|
||||||
st.capturedByUi = menuManager.uiManager.isUiInteractionForFinger(fingerId);
|
}
|
||||||
|
st.capturedByUi = !dialogueActive && menuManager.uiManager.isUiInteractionForFinger(fingerId);
|
||||||
|
|
||||||
activePointers[fingerId] = st;
|
activePointers[fingerId] = st;
|
||||||
|
|
||||||
@ -972,7 +988,10 @@ namespace ZL
|
|||||||
{
|
{
|
||||||
const int uiX = mx;
|
const int uiX = mx;
|
||||||
const int uiY = Environment::projectionHeight - my;
|
const int uiY = Environment::projectionHeight - my;
|
||||||
|
const bool dialogueActive = currentLocation && currentLocation->dialogueSystem.isActive();
|
||||||
|
if (!dialogueActive) {
|
||||||
menuManager.uiManager.onTouchUp(fingerId, uiX, uiY);
|
menuManager.uiManager.onTouchUp(fingerId, uiX, uiY);
|
||||||
|
}
|
||||||
|
|
||||||
auto it = activePointers.find(fingerId);
|
auto it = activePointers.find(fingerId);
|
||||||
if (it == activePointers.end()) return;
|
if (it == activePointers.end()) return;
|
||||||
@ -1013,7 +1032,10 @@ namespace ZL
|
|||||||
{
|
{
|
||||||
const int uiX = mx;
|
const int uiX = mx;
|
||||||
const int uiY = Environment::projectionHeight - my;
|
const int uiY = Environment::projectionHeight - my;
|
||||||
|
const bool dialogueActive = currentLocation && currentLocation->dialogueSystem.isActive();
|
||||||
|
if (!dialogueActive) {
|
||||||
menuManager.uiManager.onTouchMove(fingerId, uiX, uiY);
|
menuManager.uiManager.onTouchMove(fingerId, uiX, uiY);
|
||||||
|
}
|
||||||
|
|
||||||
auto it = activePointers.find(fingerId);
|
auto it = activePointers.find(fingerId);
|
||||||
if (it != activePointers.end()) {
|
if (it != activePointers.end()) {
|
||||||
|
|||||||
@ -81,8 +81,9 @@ namespace ZL {
|
|||||||
hudStep5aRoot = loadUiFromFile("resources/w/ui/hud_step5a.json", renderer, zipFile);
|
hudStep5aRoot = loadUiFromFile("resources/w/ui/hud_step5a.json", renderer, zipFile);
|
||||||
hudStep5bRoot = loadUiFromFile("resources/w/ui/hud_step5b.json", renderer, zipFile);
|
hudStep5bRoot = loadUiFromFile("resources/w/ui/hud_step5b.json", renderer, zipFile);
|
||||||
hudStep5abRoot = loadUiFromFile("resources/w/ui/hud_step5ab.json", renderer, zipFile);
|
hudStep5abRoot = loadUiFromFile("resources/w/ui/hud_step5ab.json", renderer, zipFile);
|
||||||
//phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
|
phoneChatListRoot = loadUiFromFile("resources/w/ui/screen_phone_chat_list.json", renderer, zipFile);
|
||||||
phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
|
phoneChat1Root = loadUiFromFile("resources/w/ui/screen_phone_chat1.json", renderer, zipFile);
|
||||||
|
phoneChat2Root = loadUiFromFile("resources/w/ui/screen_phone_chat2.json", renderer, zipFile);
|
||||||
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile);
|
newInventoryRoot = loadUiFromFile("resources/w/ui/screen_inventory.json", renderer, zipFile);
|
||||||
questJournalRoot = loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile);
|
questJournalRoot = loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile);
|
||||||
|
|
||||||
@ -196,23 +197,62 @@ namespace ZL {
|
|||||||
void MenuManager::openPhoneScreen() {
|
void MenuManager::openPhoneScreen() {
|
||||||
state = GameState::PhoneScreen;
|
state = GameState::PhoneScreen;
|
||||||
tutorialPhoneScreenOpened = true;
|
tutorialPhoneScreenOpened = true;
|
||||||
// Hide the phone hint on the current HUD so it stays hidden when we return.
|
|
||||||
uiManager.setNodeVisible("hint6a", false);
|
uiManager.setNodeVisible("hint6a", false);
|
||||||
uiManager.pushMenuFromSavedRoot(phoneScreenRoot);
|
uiManager.pushMenuFromSavedRoot(phoneChatListRoot);
|
||||||
|
|
||||||
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
||||||
closePhoneScreen();
|
closePhoneScreen();
|
||||||
});
|
});
|
||||||
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {
|
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
||||||
//Keep the callback
|
|
||||||
});
|
|
||||||
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
||||||
std::cout << "Hello test " << std::endl;
|
openPhoneChatFromList(phoneChat1Root, "dialog_chat_parents001");
|
||||||
|
|
||||||
});
|
});
|
||||||
|
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
||||||
|
openPhoneChatFromList(phoneChat2Root, "dialog_chat_aiperi001");
|
||||||
|
});
|
||||||
|
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
||||||
|
// not yet implemented
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuManager::openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId) {
|
||||||
|
phoneChatVisibleBubbles_.clear();
|
||||||
|
uiManager.pushMenuFromSavedRoot(chatRoot);
|
||||||
|
|
||||||
|
const bool firstOpen = dialogueId.empty() || startedDialogues_.find(dialogueId) == startedDialogues_.end();
|
||||||
|
if (firstOpen) {
|
||||||
|
resetPhoneChatNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
||||||
|
closePhoneScreenFromChat();
|
||||||
|
});
|
||||||
|
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
||||||
|
uiManager.setTextButtonCallback("chatTitleButton", [this](const std::string&) {
|
||||||
|
returnToPhoneChatList();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (firstOpen && startDialogueFunc && !dialogueId.empty()) {
|
||||||
|
startedDialogues_.insert(dialogueId);
|
||||||
|
startDialogueFunc(dialogueId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuManager::returnToPhoneChatList() {
|
||||||
|
phoneChatVisibleBubbles_.clear();
|
||||||
|
uiManager.popMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuManager::closePhoneScreenFromChat() {
|
||||||
|
state = GameState::Gameplay;
|
||||||
|
phoneChatVisibleBubbles_.clear();
|
||||||
|
uiManager.popMenu();
|
||||||
|
uiManager.popMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MenuManager::closePhoneScreen() {
|
void MenuManager::closePhoneScreen() {
|
||||||
state = GameState::Gameplay;
|
state = GameState::Gameplay;
|
||||||
|
phoneChatVisibleBubbles_.clear();
|
||||||
uiManager.popMenu();
|
uiManager.popMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -412,4 +452,54 @@ namespace ZL {
|
|||||||
refreshQuestJournalUi();
|
refreshQuestJournalUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MenuManager::resetPhoneChatNodes() {
|
||||||
|
static const char* kChatNodes[] = {
|
||||||
|
"message01in", "message02out", "message03in", "message04out",
|
||||||
|
"message05in", "message06in", "message07in", "message08out",
|
||||||
|
"message09in", "message10in", "message11in", nullptr
|
||||||
|
};
|
||||||
|
for (int i = 0; kChatNodes[i]; ++i) {
|
||||||
|
uiManager.setNodeVisible(kChatNodes[i], false);
|
||||||
|
auto n = uiManager.findNode(kChatNodes[i]);
|
||||||
|
if (n) { n->scaleX = 1.0f; n->scaleY = 1.0f; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuManager::recomputePhoneChatPositions() {
|
||||||
|
float totalHeight = 0.0f;
|
||||||
|
for (size_t i = 0; i < phoneChatVisibleBubbles_.size(); ++i) {
|
||||||
|
totalHeight += phoneChatVisibleBubbles_[i].height;
|
||||||
|
if (i > 0) totalHeight += CHAT_SPACING;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float available = CHAT_TOP_Y - CHAT_BOTTOM_Y;
|
||||||
|
const float topY = (totalHeight <= available)
|
||||||
|
? CHAT_TOP_Y
|
||||||
|
: CHAT_BOTTOM_Y + totalHeight;
|
||||||
|
|
||||||
|
float cursor = topY;
|
||||||
|
for (auto& bubble : phoneChatVisibleBubbles_) {
|
||||||
|
auto node = uiManager.findNode(bubble.nodeName);
|
||||||
|
if (!node) continue;
|
||||||
|
node->localY = cursor - bubble.height;
|
||||||
|
cursor -= bubble.height + CHAT_SPACING;
|
||||||
|
}
|
||||||
|
uiManager.updateAllLayouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MenuManager::revealPhoneChatBubble(const std::string& slotName) {
|
||||||
|
if (state != GameState::PhoneScreen) return;
|
||||||
|
auto node = uiManager.findNode(slotName);
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
// Zero scale before making visible to avoid a one-frame flash at full size
|
||||||
|
node->scaleX = 0.0f;
|
||||||
|
node->scaleY = 0.0f;
|
||||||
|
|
||||||
|
phoneChatVisibleBubbles_.push_back({slotName, node->height});
|
||||||
|
uiManager.setNodeVisible(slotName, true);
|
||||||
|
recomputePhoneChatPositions();
|
||||||
|
uiManager.startPopIn(slotName, 300.0f);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
|
|
||||||
@ -50,6 +51,10 @@ namespace ZL {
|
|||||||
|
|
||||||
void openPhoneScreen();
|
void openPhoneScreen();
|
||||||
void closePhoneScreen();
|
void closePhoneScreen();
|
||||||
|
void revealPhoneChatBubble(const std::string& slotName);
|
||||||
|
bool isPhoneScreenOpen() const { return state == GameState::PhoneScreen; }
|
||||||
|
|
||||||
|
std::function<void(const std::string&)> startDialogueFunc;
|
||||||
|
|
||||||
void advanceTutorialStep();
|
void advanceTutorialStep();
|
||||||
void onItemPickedUp(const std::string& itemId);
|
void onItemPickedUp(const std::string& itemId);
|
||||||
@ -66,6 +71,11 @@ namespace ZL {
|
|||||||
void selectInventoryItem(int index);
|
void selectInventoryItem(int index);
|
||||||
void refreshItemPickupHud();
|
void refreshItemPickupHud();
|
||||||
void setupStep5Callbacks();
|
void setupStep5Callbacks();
|
||||||
|
void resetPhoneChatNodes();
|
||||||
|
void recomputePhoneChatPositions();
|
||||||
|
void openPhoneChatFromList(std::shared_ptr<UiNode> chatRoot, const std::string& dialogueId);
|
||||||
|
void returnToPhoneChatList();
|
||||||
|
void closePhoneScreenFromChat();
|
||||||
|
|
||||||
GameState state = GameState::Gameplay;
|
GameState state = GameState::Gameplay;
|
||||||
Inventory* inventory = nullptr;
|
Inventory* inventory = nullptr;
|
||||||
@ -85,7 +95,9 @@ namespace ZL {
|
|||||||
std::shared_ptr<UiNode> hudStep5aRoot;
|
std::shared_ptr<UiNode> hudStep5aRoot;
|
||||||
std::shared_ptr<UiNode> hudStep5bRoot;
|
std::shared_ptr<UiNode> hudStep5bRoot;
|
||||||
std::shared_ptr<UiNode> hudStep5abRoot;
|
std::shared_ptr<UiNode> hudStep5abRoot;
|
||||||
std::shared_ptr<UiNode> phoneScreenRoot;
|
std::shared_ptr<UiNode> phoneChatListRoot;
|
||||||
|
std::shared_ptr<UiNode> phoneChat1Root;
|
||||||
|
std::shared_ptr<UiNode> phoneChat2Root;
|
||||||
std::shared_ptr<UiNode> newInventoryRoot;
|
std::shared_ptr<UiNode> newInventoryRoot;
|
||||||
std::shared_ptr<UiNode> questJournalRoot;
|
std::shared_ptr<UiNode> questJournalRoot;
|
||||||
|
|
||||||
@ -96,6 +108,20 @@ namespace ZL {
|
|||||||
|
|
||||||
int selectedQuestIndex = -1;
|
int selectedQuestIndex = -1;
|
||||||
std::vector<std::string> visibleQuestIds;
|
std::vector<std::string> visibleQuestIds;
|
||||||
|
|
||||||
|
// Dialogues that have been started at least once; re-opening a chat won't restart them
|
||||||
|
std::unordered_set<std::string> startedDialogues_;
|
||||||
|
|
||||||
|
// Phone chat state
|
||||||
|
struct PhoneChatBubbleInfo {
|
||||||
|
std::string nodeName;
|
||||||
|
float height;
|
||||||
|
};
|
||||||
|
std::vector<PhoneChatBubbleInfo> phoneChatVisibleBubbles_;
|
||||||
|
|
||||||
|
static constexpr float CHAT_TOP_Y = 610.0f;
|
||||||
|
static constexpr float CHAT_BOTTOM_Y = 290.0f;
|
||||||
|
static constexpr float CHAT_SPACING = 10.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
|
|||||||
@ -429,6 +429,7 @@ namespace ZL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (j.contains("name")) node->name = j["name"].get<std::string>();
|
if (j.contains("name")) node->name = j["name"].get<std::string>();
|
||||||
|
if (j.contains("visible")) node->visible = j["visible"].get<bool>();
|
||||||
|
|
||||||
// 2. Читаем размеры во временные "локальные" поля
|
// 2. Читаем размеры во временные "локальные" поля
|
||||||
// Это критически важно: мы не пишем сразу в screenRect,
|
// Это критически важно: мы не пишем сразу в screenRect,
|
||||||
@ -820,11 +821,13 @@ namespace ZL {
|
|||||||
);
|
);
|
||||||
buttons.clear();
|
buttons.clear();
|
||||||
textButtons.clear();
|
textButtons.clear();
|
||||||
|
allInteractives.clear();
|
||||||
sliders.clear();
|
sliders.clear();
|
||||||
textViews.clear();
|
textViews.clear();
|
||||||
textFields.clear();
|
textFields.clear();
|
||||||
staticImages.clear();
|
staticImages.clear();
|
||||||
pulsingNodes.clear();
|
pulsingNodes.clear();
|
||||||
|
popInNodes.clear();
|
||||||
collectButtonsAndSliders(root);
|
collectButtonsAndSliders(root);
|
||||||
|
|
||||||
nodeActiveAnims.clear();
|
nodeActiveAnims.clear();
|
||||||
@ -1050,12 +1053,25 @@ namespace ZL {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UiManager::startPopIn(const std::string& nodeName, float durationMs) {
|
||||||
|
auto node = findNode(nodeName);
|
||||||
|
if (!node) return;
|
||||||
|
node->scaleX = 0.0f;
|
||||||
|
node->scaleY = 0.0f;
|
||||||
|
node->popInActive = true;
|
||||||
|
node->popInProgress = 0.0f;
|
||||||
|
node->popInDurationMs = durationMs;
|
||||||
|
popInNodes.push_back(node);
|
||||||
|
}
|
||||||
|
|
||||||
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
|
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
|
||||||
if (node->button) {
|
if (node->button) {
|
||||||
buttons.push_back(node->button);
|
buttons.push_back(node->button);
|
||||||
|
allInteractives.push_back(node->button);
|
||||||
}
|
}
|
||||||
if (node->textButton) {
|
if (node->textButton) {
|
||||||
textButtons.push_back(node->textButton);
|
textButtons.push_back(node->textButton);
|
||||||
|
allInteractives.push_back(node->textButton);
|
||||||
}
|
}
|
||||||
if (node->slider) {
|
if (node->slider) {
|
||||||
sliders.push_back(node->slider);
|
sliders.push_back(node->slider);
|
||||||
@ -1178,6 +1194,7 @@ namespace ZL {
|
|||||||
prev.root = root;
|
prev.root = root;
|
||||||
prev.buttons = buttons;
|
prev.buttons = buttons;
|
||||||
prev.textButtons = textButtons;
|
prev.textButtons = textButtons;
|
||||||
|
prev.allInteractives = allInteractives;
|
||||||
prev.sliders = sliders;
|
prev.sliders = sliders;
|
||||||
prev.textViews = textViews;
|
prev.textViews = textViews;
|
||||||
prev.textFields = textFields;
|
prev.textFields = textFields;
|
||||||
@ -1188,6 +1205,7 @@ namespace ZL {
|
|||||||
prev.pressedSliders = pressedSliders;
|
prev.pressedSliders = pressedSliders;
|
||||||
prev.focusedTextField = focusedTextField;
|
prev.focusedTextField = focusedTextField;
|
||||||
prev.path = "";
|
prev.path = "";
|
||||||
|
prev.popInNodes = popInNodes;
|
||||||
|
|
||||||
prev.animCallbacks = animCallbacks;
|
prev.animCallbacks = animCallbacks;
|
||||||
|
|
||||||
@ -1241,11 +1259,13 @@ namespace ZL {
|
|||||||
root = s.root;
|
root = s.root;
|
||||||
buttons = s.buttons;
|
buttons = s.buttons;
|
||||||
textButtons = s.textButtons;
|
textButtons = s.textButtons;
|
||||||
|
allInteractives = s.allInteractives;
|
||||||
sliders = s.sliders;
|
sliders = s.sliders;
|
||||||
textViews = s.textViews;
|
textViews = s.textViews;
|
||||||
textFields = s.textFields;
|
textFields = s.textFields;
|
||||||
staticImages = s.staticImages;
|
staticImages = s.staticImages;
|
||||||
pulsingNodes = s.pulsingNodes;
|
pulsingNodes = s.pulsingNodes;
|
||||||
|
popInNodes = s.popInNodes;
|
||||||
pressedButtons = s.pressedButtons;
|
pressedButtons = s.pressedButtons;
|
||||||
pressedTextButtons = s.pressedTextButtons;
|
pressedTextButtons = s.pressedTextButtons;
|
||||||
pressedSliders = s.pressedSliders;
|
pressedSliders = s.pressedSliders;
|
||||||
@ -1381,6 +1401,26 @@ namespace ZL {
|
|||||||
node->scaleY = s;
|
node->scaleY = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pop-in scale animations (scale 0 → 1 on bubble reveal)
|
||||||
|
for (auto& node : popInNodes) {
|
||||||
|
if (!node || !node->popInActive) continue;
|
||||||
|
node->popInProgress += deltaMs / node->popInDurationMs;
|
||||||
|
if (node->popInProgress >= 1.0f) {
|
||||||
|
node->popInProgress = 1.0f;
|
||||||
|
node->popInActive = false;
|
||||||
|
node->scaleX = 1.0f;
|
||||||
|
node->scaleY = 1.0f;
|
||||||
|
} else {
|
||||||
|
// ease-out quad
|
||||||
|
const float t = node->popInProgress;
|
||||||
|
const float s = 1.0f - (1.0f - t) * (1.0f - t);
|
||||||
|
node->scaleX = s;
|
||||||
|
node->scaleY = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popInNodes.erase(std::remove_if(popInNodes.begin(), popInNodes.end(),
|
||||||
|
[](const auto& n) { return !n || !n->popInActive; }), popInNodes.end());
|
||||||
|
|
||||||
std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
|
std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
|
||||||
std::vector<std::function<void()>> pendingCallbacks;
|
std::vector<std::function<void()>> pendingCallbacks;
|
||||||
|
|
||||||
@ -1611,22 +1651,22 @@ namespace ZL {
|
|||||||
|
|
||||||
|
|
||||||
void UiManager::onTouchDown(int64_t fingerId, int x, int y) {
|
void UiManager::onTouchDown(int64_t fingerId, int x, int y) {
|
||||||
for (auto& b : buttons) {
|
// Iterate allInteractives in reverse DFS order: later-declared elements have higher priority.
|
||||||
if (b->state != ButtonState::Disabled)
|
// At most one button or textButton is pressed per touch.
|
||||||
{
|
for (auto it = allInteractives.rbegin(); it != allInteractives.rend(); ++it) {
|
||||||
if (b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
|
if (std::holds_alternative<std::shared_ptr<UiButton>>(*it)) {
|
||||||
|
auto& b = std::get<std::shared_ptr<UiButton>>(*it);
|
||||||
|
if (b->state != ButtonState::Disabled &&
|
||||||
|
b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
|
||||||
b->state = ButtonState::Pressed;
|
b->state = ButtonState::Pressed;
|
||||||
pressedButtons[fingerId] = b;
|
pressedButtons[fingerId] = b;
|
||||||
if (b->onPress) b->onPress(b->name);
|
if (b->onPress) b->onPress(b->name);
|
||||||
break; // a single finger can only press one button
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
auto& tb = std::get<std::shared_ptr<UiTextButton>>(*it);
|
||||||
|
if (tb->state != ButtonState::Disabled &&
|
||||||
for (auto& tb : textButtons) {
|
tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
|
||||||
if (tb->state != ButtonState::Disabled)
|
|
||||||
{
|
|
||||||
if (tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
|
|
||||||
tb->state = ButtonState::Pressed;
|
tb->state = ButtonState::Pressed;
|
||||||
pressedTextButtons[fingerId] = tb;
|
pressedTextButtons[fingerId] = tb;
|
||||||
if (tb->onPress) tb->onPress(tb->name);
|
if (tb->onPress) tb->onPress(tb->name);
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <variant>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
namespace ZL {
|
namespace ZL {
|
||||||
@ -272,6 +273,11 @@ namespace ZL {
|
|||||||
float pulsePeriodMs = 1000.0f;
|
float pulsePeriodMs = 1000.0f;
|
||||||
float pulseElapsedMs = 0.0f; // runtime, not persisted in JSON
|
float pulseElapsedMs = 0.0f; // runtime, not persisted in JSON
|
||||||
|
|
||||||
|
// Pop-in scale animation (scale 0→1 on first reveal)
|
||||||
|
bool popInActive = false;
|
||||||
|
float popInProgress = 0.0f; // 0..1
|
||||||
|
float popInDurationMs = 300.0f;
|
||||||
|
|
||||||
// Иерархия
|
// Иерархия
|
||||||
std::vector<std::shared_ptr<UiNode>> children;
|
std::vector<std::shared_ptr<UiNode>> children;
|
||||||
|
|
||||||
@ -406,6 +412,7 @@ namespace ZL {
|
|||||||
bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName);
|
bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName);
|
||||||
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
|
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function<void()> cb);
|
||||||
void updateAllLayouts();
|
void updateAllLayouts();
|
||||||
|
void startPopIn(const std::string& nodeName, float durationMs = 300.0f);
|
||||||
|
|
||||||
std::shared_ptr<UiNode> findNode(const std::string& name);
|
std::shared_ptr<UiNode> findNode(const std::string& name);
|
||||||
|
|
||||||
@ -440,14 +447,20 @@ namespace ZL {
|
|||||||
bool stepStarted = false;
|
bool stepStarted = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
using AnyButton = std::variant<std::shared_ptr<UiButton>, std::shared_ptr<UiTextButton>>;
|
||||||
|
|
||||||
std::shared_ptr<UiNode> root;
|
std::shared_ptr<UiNode> root;
|
||||||
std::vector<std::shared_ptr<UiButton>> buttons;
|
std::vector<std::shared_ptr<UiButton>> buttons;
|
||||||
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
||||||
|
// All buttons and textButtons in DFS declaration order.
|
||||||
|
// onTouchDown iterates this in reverse so later-declared elements have higher priority.
|
||||||
|
std::vector<AnyButton> allInteractives;
|
||||||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||||||
std::vector<std::shared_ptr<UiTextView>> textViews;
|
std::vector<std::shared_ptr<UiTextView>> textViews;
|
||||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||||
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
||||||
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
|
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
|
||||||
|
std::vector<std::shared_ptr<UiNode>> popInNodes;
|
||||||
|
|
||||||
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
|
std::map<std::shared_ptr<UiNode>, std::vector<ActiveAnim>> nodeActiveAnims;
|
||||||
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
|
std::map<std::pair<std::string, std::string>, std::function<void()>> animCallbacks; // key: (nodeName, animName)
|
||||||
@ -462,11 +475,13 @@ namespace ZL {
|
|||||||
std::shared_ptr<UiNode> root;
|
std::shared_ptr<UiNode> root;
|
||||||
std::vector<std::shared_ptr<UiButton>> buttons;
|
std::vector<std::shared_ptr<UiButton>> buttons;
|
||||||
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
std::vector<std::shared_ptr<UiTextButton>> textButtons;
|
||||||
|
std::vector<AnyButton> allInteractives;
|
||||||
std::vector<std::shared_ptr<UiSlider>> sliders;
|
std::vector<std::shared_ptr<UiSlider>> sliders;
|
||||||
std::vector<std::shared_ptr<UiTextView>> textViews;
|
std::vector<std::shared_ptr<UiTextView>> textViews;
|
||||||
std::vector<std::shared_ptr<UiTextField>> textFields;
|
std::vector<std::shared_ptr<UiTextField>> textFields;
|
||||||
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
|
||||||
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
|
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
|
||||||
|
std::vector<std::shared_ptr<UiNode>> popInNodes;
|
||||||
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
|
std::map<int64_t, std::shared_ptr<UiButton>> pressedButtons;
|
||||||
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
|
std::map<int64_t, std::shared_ptr<UiTextButton>> pressedTextButtons;
|
||||||
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
|
std::map<int64_t, std::shared_ptr<UiSlider>> pressedSliders;
|
||||||
|
|||||||
@ -105,6 +105,7 @@ Node DialogueDatabase::parseNode(const json& j) {
|
|||||||
node.falseNext = j.value("falseNext", "");
|
node.falseNext = j.value("falseNext", "");
|
||||||
node.cutsceneId = j.value("cutsceneId", "");
|
node.cutsceneId = j.value("cutsceneId", "");
|
||||||
node.luaCallback = j.value("luaCallback", "");
|
node.luaCallback = j.value("luaCallback", "");
|
||||||
|
node.bubbleSlot = j.value("bubbleSlot", "");
|
||||||
|
|
||||||
if (j.contains("conditions") && j["conditions"].is_array()) {
|
if (j.contains("conditions") && j["conditions"].is_array()) {
|
||||||
for (const auto& item : j["conditions"]) {
|
for (const auto& item : j["conditions"]) {
|
||||||
|
|||||||
@ -81,6 +81,10 @@ void DialogueRuntime::setOnCutsceneFadeInComplete(std::function<void(const std::
|
|||||||
onCutsceneFadeInComplete = std::move(cb);
|
onCutsceneFadeInComplete = std::move(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DialogueRuntime::setOnBubbleSlotReady(std::function<void(const std::string&)> cb) {
|
||||||
|
onBubbleSlotReady = std::move(cb);
|
||||||
|
}
|
||||||
|
|
||||||
void DialogueRuntime::stop() {
|
void DialogueRuntime::stop() {
|
||||||
activeDialogue = nullptr;
|
activeDialogue = nullptr;
|
||||||
activeCutscene = nullptr;
|
activeCutscene = nullptr;
|
||||||
@ -431,6 +435,9 @@ void DialogueRuntime::presentLine(const Node& node) {
|
|||||||
if (!node.luaCallback.empty() && onDialogueLineStarted) {
|
if (!node.luaCallback.empty() && onDialogueLineStarted) {
|
||||||
onDialogueLineStarted(node.luaCallback);
|
onDialogueLineStarted(node.luaCallback);
|
||||||
}
|
}
|
||||||
|
if (!node.bubbleSlot.empty() && onBubbleSlotReady) {
|
||||||
|
onBubbleSlotReady(node.bubbleSlot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DialogueRuntime::presentChoices(const Node& node) {
|
void DialogueRuntime::presentChoices(const Node& node) {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ public:
|
|||||||
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
||||||
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
|
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
|
||||||
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
|
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
|
||||||
|
void setOnBubbleSlotReady(std::function<void(const std::string&)> cb);
|
||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
void update(int deltaMs);
|
void update(int deltaMs);
|
||||||
@ -56,6 +57,7 @@ private:
|
|||||||
std::function<void(const std::string&)> onDialogueLineStarted;
|
std::function<void(const std::string&)> onDialogueLineStarted;
|
||||||
std::function<void(const std::string&)> onCutsceneLineStarted;
|
std::function<void(const std::string&)> onCutsceneLineStarted;
|
||||||
std::function<void(const std::string&)> onCutsceneFadeInComplete;
|
std::function<void(const std::string&)> onCutsceneFadeInComplete;
|
||||||
|
std::function<void(const std::string&)> onBubbleSlotReady;
|
||||||
std::string activeCutsceneId;
|
std::string activeCutsceneId;
|
||||||
bool fadeInCallbackFired = false;
|
bool fadeInCallbackFired = false;
|
||||||
|
|
||||||
|
|||||||
@ -140,6 +140,10 @@ void DialogueSystem::setOnCutsceneFadeInComplete(std::function<void(const std::s
|
|||||||
runtime.setOnCutsceneFadeInComplete(std::move(cb));
|
runtime.setOnCutsceneFadeInComplete(std::move(cb));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DialogueSystem::setOnBubbleSlotReady(std::function<void(const std::string&)> cb) {
|
||||||
|
runtime.setOnBubbleSlotReady(std::move(cb));
|
||||||
|
}
|
||||||
|
|
||||||
void DialogueSystem::stopDialogue() {
|
void DialogueSystem::stopDialogue() {
|
||||||
runtime.stop();
|
runtime.stop();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,7 @@ public:
|
|||||||
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
void setOnDialogueLineStarted(std::function<void(const std::string&)> cb);
|
||||||
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
|
void setOnCutsceneLineStarted(std::function<void(const std::string&)> cb);
|
||||||
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
|
void setOnCutsceneFadeInComplete(std::function<void(const std::string&)> cb);
|
||||||
|
void setOnBubbleSlotReady(std::function<void(const std::string&)> cb);
|
||||||
void setOnDialogueAdvanced(std::function<void()> cb);
|
void setOnDialogueAdvanced(std::function<void()> cb);
|
||||||
void stopDialogue();
|
void stopDialogue();
|
||||||
|
|
||||||
|
|||||||
@ -96,6 +96,9 @@ struct Node {
|
|||||||
|
|
||||||
// For CutsceneStart
|
// For CutsceneStart
|
||||||
std::string cutsceneId;
|
std::string cutsceneId;
|
||||||
|
|
||||||
|
// Name of the UI node (StaticImage) to reveal in the phone chat when this line is shown
|
||||||
|
std::string bubbleSlot;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DialogueDefinition {
|
struct DialogueDefinition {
|
||||||
|
|||||||
@ -24,4 +24,5 @@ namespace ZL {
|
|||||||
[&itemId](const Item& item) { return item.id == itemId; }) != items.end();
|
[&itemId](const Item& item) { return item.id == itemId; }) != items.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace ZL
|
} // namespace ZL
|
||||||
Loading…
Reference in New Issue
Block a user