Working on UI improvement

This commit is contained in:
Vladislav Khorev committed 2026-05-27 17:30:41 +03:00
1 parent 0da1ae124c
commit 80af0e61ae
43 files changed
+889 -64

No files matched your search

+5 -4
View File
@@ -1,12 +1,13 @@
//precisionmediump float;
uniform sampler2D Texture;
uniform float uAlpha;
varying vec2 texCoord;
void main()
void main()
{
vec4 color = texture2D(Texture,texCoord).rgba;
//gl_FragColor = vec4(color.rgb*0.1 + vec3(0.9, 0.9, 0.9), 1.0);
gl_FragColor = color;
gl_FragColor = vec4(color.rgb, color.a * uAlpha);
}
+5 -4
View File
@@ -1,12 +1,13 @@
precision mediump float;
uniform sampler2D Texture;
uniform float uAlpha;
varying vec2 texCoord;
void main()
void main()
{
vec4 color = texture2D(Texture,texCoord).rgba;
//gl_FragColor = vec4(color.rgb*0.1 + vec3(0.9, 0.9, 0.9), 1.0);
gl_FragColor = color;
gl_FragColor = vec4(color.rgb, color.a * uAlpha);
}
+43
View File
@@ -0,0 +1,43 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"children": [
{
"type": "Button",
"name": "phoneButton",
"width": 229,
"height": 229,
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
},
{
"type": "Button",
"name": "journalButton",
"width": 229,
"height": 229,
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
}
]
}]
}
}
+8 -1
View File
@@ -13,6 +13,7 @@
"spacing": 0,
"width": "match_parent",
"height": 600,
"x" : 300,
"children": [
{
"type": "StaticImage",
@@ -20,7 +21,13 @@
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint2.png"
"texture": "resources/w/ui/img/Hint1.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
},
{
"type": "StaticImage",
+26 -37
View File
@@ -5,42 +5,31 @@
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "TextButton",
"name": "inventory_button",
"x": 50.0,
"y": 50.0,
"width": 150.0,
"height": 60.0,
"text": "Inventory",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
},
{
"type": "TextButton",
"name": "quest_journal_button",
"x": 220.0,
"y": 50.0,
"width": 170.0,
"height": 60.0,
"text": "Quests",
"fontSize": 24,
"fontPath": "resources/fonts/DroidSans.ttf",
"textCentered": true,
"color": [1.0, 1.0, 1.0, 1.0],
"textures": {
"normal": "resources/w/red.png",
"hover": "resources/w/red.png",
"pressed": "resources/w/red.png"
}
}
]
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : 300,
"children": [
{
"type": "StaticImage",
"name": "hint2",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint2.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
}]
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : 300,
"children": [
{
"type": "StaticImage",
"name": "hint3",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint3.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
}]
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : 300,
"children": [
{
"type": "StaticImage",
"name": "hint4",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint4.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
}]
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : 300,
"children": [
{
"type": "StaticImage",
"name": "hint5",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint5.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
}]
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : -250,
"x" : 300,
"children": [
{
"type": "StaticImage",
"name": "hint6a",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint6a.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": {
"durationMs": 600
}
}
]
},
{
"type": "Button",
"name": "phoneButton",
"horizontal_gravity": "right",
"vertical_gravity": "top",
"x": -46,
"y" :-46,
"width": 229,
"height": 229,
"clickZoneWidth": 104,
"clickZoneHeight": 104,
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
}
]
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"children": [
{
"type": "Button",
"name": "phoneButton",
"width": 229,
"height": 229,
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
}
]
}]
}
}
+82
View File
@@ -0,0 +1,82 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "horizontal",
"vertical_align": "center",
"horizontal_align": "right",
"spacing": 16,
"width": "match_parent",
"height": 600,
"y" : -200,
"children": [
{
"type": "StaticImage",
"name": "hint6a",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint6a.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
},
{
"type": "StaticImage",
"name": "hint6b",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint6b.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
},{
"type": "Button",
"name": "phoneButton",
"width": 229,
"height": 229,
"x": 74,
"y" :-46,
"clickZoneWidth": 104,
"clickZoneHeight": 104,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
},
{
"type": "Button",
"name": "journalButton",
"width": 163,
"height": 163,
"x": -13,
"y" :-13,
"clickZoneWidth": 89,
"clickZoneHeight": 89,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"textures": {
"normal": "resources/w/ui/img/Journal001_State=Default.png",
"hover": "resources/w/ui/img/Journal001_State=Selected.png",
"pressed": "resources/w/ui/img/Journal001_State=Tap.png"
}
}]
}
}
+54
View File
@@ -0,0 +1,54 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"y" : -250,
"children": [
{
"type": "StaticImage",
"name": "hint6b",
"width": 440,
"height": 134,
"horizontal_gravity": "center",
"texture": "resources/w/ui/img/Hint6b.png",
"pulse": {
"minScale": 0.92,
"maxScale": 1.08,
"periodMs": 1500
},
"fadeIn": { "durationMs": 600 }
}
]
},
{
"type": "Button",
"name": "journalButton",
"horizontal_gravity": "right",
"vertical_gravity": "top",
"x": -13,
"y" :-13,
"width": 163,
"height": 163,
"clickZoneWidth": 89,
"clickZoneHeight": 89,
"horizontal_gravity": "right",
"vertical_gravity": "top",
"textures": {
"normal": "resources/w/ui/img/Journal001_State=Default.png",
"hover": "resources/w/ui/img/Journal001_State=Selected.png",
"pressed": "resources/w/ui/img/Journal001_State=Tap.png"
}
}]
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"children": [
{
"type": "LinearLayout",
"orientation": "vertical",
"vertical_align": "center",
"horizontal_align": "center",
"spacing": 0,
"width": "match_parent",
"height": 600,
"children": [
{
"type": "Button",
"name": "journalButton",
"width": 229,
"height": 229,
"textures": {
"normal": "resources/w/ui/img/Phone001_State=Default.png",
"hover": "resources/w/ui/img/Phone001_State=Selected.png",
"pressed": "resources/w/ui/img/Phone001_State=Tap.png"
}
}
]
}]
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
{
"root": {
"type": "FrameLayout",
"name": "hud_root",
"width": "match_parent",
"height": "match_parent",
"vertical_align": "center",
"horizontal_align": "center",
"children": [
{
"type": "Button",
"name": "journalExitButton",
"horizontal_gravity": "center",
"vertical_gravity": "center",
"width": 1125,
"height": 520,
"textures": {
"normal": "resources/w/ui/img/QuestJournal001.png",
"hover": "resources/w/ui/img/QuestJournal001.png",
"pressed": "resources/w/ui/img/QuestJournal001.png"
}
}
]
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"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",
"width": 1232,
"height": 627,
"textures": {
"normal": "resources/w/ui/img/PhoneTest001.png",
"hover": "resources/w/ui/img/PhoneTest001.png",
"pressed": "resources/w/ui/img/PhoneTest001.png"
}
}
]
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ namespace ZL {
using std::max;
#endif
constexpr float DEFAULT_ZOOM = 24.f;
constexpr float DEFAULT_ZOOM = 6.f;
class Environment {
public:
+52 -2
View File
@@ -369,6 +369,24 @@ namespace ZL
locations["uni_interior"]->onTeleport = teleportCallback;
locations["location_dorm"]->onTeleport = teleportCallback;
// Wire tutorial advance callbacks for all locations.
// advanceTutorialStep() guards against double-advancing, so sharing is safe.
for (auto& [name, loc] : locations) {
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
menuManager.advanceTutorialStep();
});
loc->onPlayerFloorWalk = [this]() {
if (menuManager.tutorialStep == TutorialStep::Step2) {
menuManager.advanceTutorialStep();
}
};
}
// Wire inventory item-pickup callback for tutorial step4/5 item tracking.
inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
};
currentLocation = locations["location_dorm"];
currentLocation->scriptEngine.callLocationEnterCallback();
@@ -552,6 +570,8 @@ namespace ZL
updateDarklandsFlash(delta);
menuManager.uiManager.update(static_cast<float>(delta));
if (currentLocation)
{
currentLocation->update(delta);
@@ -661,6 +681,10 @@ namespace ZL
if (Environment::zoom < zoomstep) {
Environment::zoom = zoomstep;
}
// Tutorial step3 → step4: any mouse-wheel scroll counts as "zoom gesture".
if (menuManager.tutorialStep == TutorialStep::Step3) {
menuManager.advanceTutorialStep();
}
}
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
@@ -722,8 +746,10 @@ namespace ZL
break;
case SDLK_l:
x = x - 1;
std::cout << "current x: " << x << std::endl;
//x = x - 1;
//std::cout << "current x: " << x << std::endl;
std::cout << "Azimuth: " << currentLocation->cameraAzimuth << std::endl;
std::cout << "Inclination: " << currentLocation->cameraInclination << std::endl;
break;
case SDLK_c:
@@ -826,6 +852,10 @@ namespace ZL
// movement threshold was crossed.
currentLocation->lastMouseX = eventX;
currentLocation->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates.
dragStartAzimuth = currentLocation->cameraAzimuth;
dragStartInclination = currentLocation->cameraInclination;
}
}
@@ -881,6 +911,13 @@ namespace ZL
static const float zoomMin = 2.0f;
if (newZoom < zoomMin) newZoom = zoomMin;
Environment::zoom = newZoom;
// Tutorial step3 → step4: detect a significant pinch-zoom (≥ 2 zoom units).
if (menuManager.tutorialStep == TutorialStep::Step3) {
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
menuManager.advanceTutorialStep();
}
}
}
void Game::endPinch()
@@ -1004,6 +1041,19 @@ namespace ZL
if (currentLocation) {
// Forwarded for dialogue hover and (when cameraDragging) camera rotation.
currentLocation->handleMotion(fingerId, eventX, eventY, mx, my);
// Tutorial step1 → step2: detect a significant camera rotation on BOTH axes.
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
// not just nudged a single axis by accident.
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
if (cameraDragging
&& menuManager.tutorialStep == TutorialStep::Step1) {
float deltaAz = std::abs(currentLocation->cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(currentLocation->cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
menuManager.advanceTutorialStep();
}
}
}
}
+5
View File
@@ -87,6 +87,11 @@ namespace ZL {
float pinchStartDistance = 0.0f;
float pinchStartZoom = 0.0f;
// Tutorial: azimuth and inclination captured at the start of each camera drag,
// used to measure how far the user has rotated before advancing the tutorial step.
float dragStartAzimuth = 0.0f;
float dragStartInclination = 0.0f;
std::unique_ptr<AudioPlayerAsync> audioPlayer;
int64_t getSyncTimeMs();
+1
View File
@@ -1519,6 +1519,7 @@ namespace ZL
targetInteractNpc = nullptr;
targetInteractNpcIndex = -1;
targetTeleportZone = nullptr;
if (onPlayerFloorWalk) onPlayerFloorWalk();
}
}
else {
+6 -3
View File
@@ -51,12 +51,11 @@ namespace ZL
std::vector<InteractiveObject> interactiveObjects;
std::unique_ptr<Character> player;
std::vector<std::unique_ptr<Character>> npcs;
float cameraAzimuth = 0.0f;
float cameraInclination = M_PI * 30.f / 180.f;
float cameraAzimuth = -2.35;
float cameraInclination = 1.1036;//M_PI * 30.f / 180.f;
std::vector<PathFinder> navigationMaps;
PathFinder* navigation = nullptr;
@@ -87,6 +86,10 @@ namespace ZL
// Read by draw functions and raycast — do not write from Location code.
bool isDarklands = false;
// Called when the player successfully taps the ground and a floor walk target is set.
// Used by the tutorial system to detect the "tap to walk" gesture.
std::function<void()> onPlayerFloorWalk;
// Set by Game after setup(). Lua and onDeathAnimComplete call this to
// request the transition without coupling Location to Game directly.
std::function<bool()> requestDarklandsTransition;
+123 -1
View File
@@ -34,7 +34,16 @@ namespace ZL {
inventory = &inv;
//hudRoot = loadUiFromFile("resources/config2/hud.json", renderer, zipFile);
hudRoot = loadUiFromFile("resources/w/ui/hud_step0.json", renderer, zipFile);
hudRoot = loadUiFromFile("resources/w/ui/hud_step0.json", renderer, zipFile);
hudStep1Root = loadUiFromFile("resources/w/ui/hud_step1.json", renderer, zipFile);
hudStep2Root = loadUiFromFile("resources/w/ui/hud_step2.json", renderer, zipFile);
hudStep3Root = loadUiFromFile("resources/w/ui/hud_step3.json", renderer, zipFile);
hudStep4Root = loadUiFromFile("resources/w/ui/hud_step4.json", renderer, zipFile);
hudStep5aRoot = loadUiFromFile("resources/w/ui/hud_step5a.json", renderer, zipFile);
hudStep5bRoot = loadUiFromFile("resources/w/ui/hud_step5b.json", renderer, zipFile);
hudStep5abRoot = loadUiFromFile("resources/w/ui/hud_step5ab.json", renderer, zipFile);
phoneScreenRoot = loadUiFromFile("resources/w/ui/screen_phone.json", renderer, zipFile);
journalScreenRoot= loadUiFromFile("resources/w/ui/screen_journal.json", renderer, zipFile);
inventoryRoot = loadUiFromFile("resources/config2/ui_inventory.json", renderer, zipFile);
questJournalRoot = loadUiFromFile("resources/config2/ui_quest_journal.json", renderer, zipFile);
@@ -124,6 +133,119 @@ namespace ZL {
}
}
void MenuManager::openPhoneScreen() {
state = GameState::PhoneScreen;
tutorialPhoneScreenOpened = true;
// Hide the phone hint on the current HUD so it stays hidden when we return.
uiManager.setNodeVisible("hint6a", false);
uiManager.pushMenuFromSavedRoot(phoneScreenRoot);
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
closePhoneScreen();
});
}
void MenuManager::closePhoneScreen() {
state = GameState::Gameplay;
uiManager.popMenu();
}
void MenuManager::openJournalScreen() {
state = GameState::JournalScreen;
tutorialJournalScreenOpened = true;
// Hide the journal hint on the current HUD so it stays hidden when we return.
uiManager.setNodeVisible("hint6b", false);
uiManager.pushMenuFromSavedRoot(journalScreenRoot);
uiManager.setButtonCallback("journalExitButton", [this](const std::string&) {
closeJournalScreen();
});
}
void MenuManager::closeJournalScreen() {
state = GameState::Gameplay;
uiManager.popMenu();
}
// Registers phoneButton / journalButton callbacks on the current HUD root
// and hides any hints that have already been completed.
// Called after every replaceRoot during step 5.
void MenuManager::setupStep5Callbacks() {
if (uiManager.findButton("phoneButton")) {
uiManager.setButtonCallback("phoneButton", [this](const std::string&) {
openPhoneScreen();
});
}
if (uiManager.findButton("journalButton")) {
uiManager.setButtonCallback("journalButton", [this](const std::string&) {
openJournalScreen();
});
}
if (tutorialPhoneScreenOpened) uiManager.setNodeVisible("hint6a", false);
if (tutorialJournalScreenOpened) uiManager.setNodeVisible("hint6b", false);
}
void MenuManager::advanceTutorialStep() {
std::shared_ptr<UiNode> nextRoot;
switch (tutorialStep) {
case TutorialStep::Step0:
tutorialStep = TutorialStep::Step1;
nextRoot = hudStep1Root;
break;
case TutorialStep::Step1:
tutorialStep = TutorialStep::Step2;
nextRoot = hudStep2Root;
break;
case TutorialStep::Step2:
tutorialStep = TutorialStep::Step3;
nextRoot = hudStep3Root;
break;
case TutorialStep::Step3:
tutorialStep = TutorialStep::Step4;
nextRoot = hudStep4Root;
break;
default:
return; // Step4/Step5 transitions are driven by onItemPickedUp
}
if (state == GameState::Gameplay && nextRoot) {
uiManager.replaceRoot(nextRoot);
}
}
void MenuManager::onItemPickedUp(const std::string& itemId) {
if (tutorialStep != TutorialStep::Step4 && tutorialStep != TutorialStep::Step5) {
return;
}
if (itemId == "phone") tutorialPhonePickedUp = true;
if (itemId == "journal") tutorialJournalPickedUp = true;
if (tutorialStep == TutorialStep::Step4) {
tutorialStep = TutorialStep::Step5;
}
refreshItemPickupHud();
}
void MenuManager::refreshItemPickupHud() {
if (state != GameState::Gameplay) return;
std::shared_ptr<UiNode> nextRoot;
if (tutorialPhonePickedUp && tutorialJournalPickedUp) {
nextRoot = hudStep5abRoot;
} else if (tutorialPhonePickedUp) {
nextRoot = hudStep5aRoot;
} else if (tutorialJournalPickedUp) {
nextRoot = hudStep5bRoot;
}
if (nextRoot) {
uiManager.replaceRoot(nextRoot);
// Register item-screen buttons and re-apply any already-completed hint visibility.
setupStep5Callbacks();
}
}
void MenuManager::refreshQuestJournalUi() {
visibleQuestIds.clear();
auto quests = questJournal.getVisibleQuests();
+38 -1
View File
@@ -16,7 +16,18 @@ namespace ZL {
enum class GameState {
Gameplay,
Inventory,
QuestJournal
QuestJournal,
PhoneScreen,
JournalScreen
};
enum class TutorialStep {
Step0, // Dialogue hint: "click to advance"
Step1, // Camera rotation hint
Step2, // Floor tap / walk hint
Step3, // Pinch-zoom hint
Step4, // Pick-up item hint
Step5, // Post-pickup reaction (sub-state: which items were collected)
};
class MenuManager {
@@ -38,6 +49,16 @@ namespace ZL {
bool isInventoryOpen() const { return state == GameState::Inventory; }
bool isQuestJournalOpen() const { return state == GameState::QuestJournal; }
void openPhoneScreen();
void closePhoneScreen();
void openJournalScreen();
void closeJournalScreen();
void advanceTutorialStep();
void onItemPickedUp(const std::string& itemId);
TutorialStep tutorialStep = TutorialStep::Step0;
protected:
Renderer& renderer;
@@ -45,11 +66,27 @@ namespace ZL {
void enterGameplay();
void refreshQuestJournalUi();
void selectQuestByIndex(int index);
void refreshItemPickupHud();
void setupStep5Callbacks();
GameState state = GameState::Gameplay;
Inventory* inventory = nullptr;
bool tutorialPhonePickedUp = false;
bool tutorialJournalPickedUp = false;
bool tutorialPhoneScreenOpened = false;
bool tutorialJournalScreenOpened = false;
std::shared_ptr<UiNode> hudRoot;
std::shared_ptr<UiNode> hudStep1Root;
std::shared_ptr<UiNode> hudStep2Root;
std::shared_ptr<UiNode> hudStep3Root;
std::shared_ptr<UiNode> hudStep4Root;
std::shared_ptr<UiNode> hudStep5aRoot;
std::shared_ptr<UiNode> hudStep5bRoot;
std::shared_ptr<UiNode> hudStep5abRoot;
std::shared_ptr<UiNode> phoneScreenRoot;
std::shared_ptr<UiNode> journalScreenRoot;
std::shared_ptr<UiNode> inventoryRoot;
std::shared_ptr<UiNode> questJournalRoot;
+68 -6
View File
@@ -373,9 +373,17 @@ namespace ZL {
void UiStaticImage::draw(Renderer& renderer) const {
if (!texture) return;
const float alpha = (fadeInEnabled && fadeInDurationMs > 0.0f)
? std::clamp(fadeInElapsedMs / fadeInDurationMs, 0.0f, 1.0f)
: 1.0f;
renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform1f("uAlpha", alpha);
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
renderer.DrawVertexRenderStruct(mesh);
if (alpha < 1.0f) {
renderer.RenderUniform1f("uAlpha", 1.0f); // restore for subsequent draws
}
}
void UiTextField::draw(Renderer& renderer) const {
@@ -504,6 +512,8 @@ namespace ZL {
btn->texDisabled = loadTex("disabled");
btn->border = j.value("border", 0.0f);
if (j.contains("clickZoneWidth")) btn->clickZoneWidth = j["clickZoneWidth"].get<float>();
if (j.contains("clickZoneHeight")) btn->clickZoneHeight = j["clickZoneHeight"].get<float>();
node->button = btn;
}
@@ -616,6 +626,8 @@ namespace ZL {
if (j.contains("color") && j["color"].is_array() && j["color"].size() == 4) {
for (int i = 0; i < 4; ++i) tb->color[i] = j["color"][i].get<float>();
}
if (j.contains("clickZoneWidth")) tb->clickZoneWidth = j["clickZoneWidth"].get<float>();
if (j.contains("clickZoneHeight")) tb->clickZoneHeight = j["clickZoneHeight"].get<float>();
tb->textRenderer = std::make_unique<TextRenderer>();
if (!tb->textRenderer->init(renderer, tb->fontPath, tb->fontSize, zipFile)) {
@@ -679,6 +691,23 @@ namespace ZL {
}
node->staticImage = img;
// Optional fade-in on display
if (j.contains("fadeIn") && j["fadeIn"].is_object()) {
const auto& fi = j["fadeIn"];
img->fadeInEnabled = true;
img->fadeInDurationMs = fi.value("durationMs", 1000.0f);
img->fadeInElapsedMs = 0.0f;
}
// Optional pulse-scale animation
if (j.contains("pulse") && j["pulse"].is_object()) {
const auto& p = j["pulse"];
node->pulseEnabled = true;
node->pulseMinScale = p.value("minScale", 0.9f);
node->pulseMaxScale = p.value("maxScale", 1.1f);
node->pulsePeriodMs = p.value("periodMs", 1000.0f);
}
}
if (typeStr == "TextView") {
@@ -777,6 +806,7 @@ namespace ZL {
textViews.clear();
textFields.clear();
staticImages.clear();
pulsingNodes.clear();
collectButtonsAndSliders(root);
nodeActiveAnims.clear();
@@ -1020,6 +1050,12 @@ namespace ZL {
}
if (node->staticImage) {
staticImages.push_back(node->staticImage);
if (node->staticImage->fadeInEnabled) {
node->staticImage->fadeInElapsedMs = 0.0f; // restart fade every time UI is shown
}
}
if (node->pulseEnabled && node->staticImage) {
pulsingNodes.push_back(node);
}
for (auto& c : node->children) collectButtonsAndSliders(c);
}
@@ -1128,6 +1164,7 @@ namespace ZL {
prev.textViews = textViews;
prev.textFields = textFields;
prev.staticImages = staticImages;
prev.pulsingNodes = pulsingNodes;
prev.pressedButtons = pressedButtons;
prev.pressedTextButtons = pressedTextButtons;
prev.pressedSliders = pressedSliders;
@@ -1190,6 +1227,7 @@ namespace ZL {
textViews = s.textViews;
textFields = s.textFields;
staticImages = s.staticImages;
pulsingNodes = s.pulsingNodes;
pressedButtons = s.pressedButtons;
pressedTextButtons = s.pressedTextButtons;
pressedSliders = s.pressedSliders;
@@ -1233,6 +1271,7 @@ namespace ZL {
renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.RenderUniform1f("uAlpha", 1.0f); // ensure alpha is fully opaque unless a widget overrides it
std::function<void(const std::shared_ptr<UiNode>&)> drawNode =
[&](const std::shared_ptr<UiNode>& node) {
@@ -1301,6 +1340,29 @@ namespace ZL {
void UiManager::update(float deltaMs) {
if (!root) return;
// Fade-in animations (StaticImage elements with fadeInEnabled == true)
for (auto& img : staticImages) {
if (!img || !img->fadeInEnabled) continue;
if (img->fadeInElapsedMs < img->fadeInDurationMs) {
img->fadeInElapsedMs += deltaMs;
if (img->fadeInElapsedMs > img->fadeInDurationMs)
img->fadeInElapsedMs = img->fadeInDurationMs;
}
}
// Pulse-scale animations (StaticImage nodes with pulseEnabled == true)
for (auto& node : pulsingNodes) {
if (!node) continue;
node->pulseElapsedMs += deltaMs;
if (node->pulseElapsedMs >= node->pulsePeriodMs)
node->pulseElapsedMs = std::fmod(node->pulseElapsedMs, node->pulsePeriodMs);
const float phase = node->pulseElapsedMs / node->pulsePeriodMs;
const float t = (std::sin(phase * 2.0f * static_cast<float>(M_PI)) + 1.0f) * 0.5f;
const float s = node->pulseMinScale + (node->pulseMaxScale - node->pulseMinScale) * t;
node->scaleX = s;
node->scaleY = s;
}
std::vector<std::pair<std::shared_ptr<UiNode>, size_t>> animationsToRemove;
std::vector<std::function<void()>> pendingCallbacks;
@@ -1490,7 +1552,7 @@ namespace ZL {
for (auto& b : buttons) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
if (b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
}
else {
@@ -1501,7 +1563,7 @@ namespace ZL {
for (auto& tb : textButtons) {
if (tb->state != ButtonState::Disabled)
{
if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
if (tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
if (tb->state != ButtonState::Pressed) tb->state = ButtonState::Hover;
}
else {
@@ -1534,7 +1596,7 @@ namespace ZL {
for (auto& b : buttons) {
if (b->state != ButtonState::Disabled)
{
if (b->rect.containsConsideringBorder((float)x, (float)y, b->border)) {
if (b->getClickZoneRect().containsConsideringBorder((float)x, (float)y, b->border)) {
b->state = ButtonState::Pressed;
pressedButtons[fingerId] = b;
if (b->onPress) b->onPress(b->name);
@@ -1546,7 +1608,7 @@ namespace ZL {
for (auto& tb : textButtons) {
if (tb->state != ButtonState::Disabled)
{
if (tb->rect.containsConsideringBorder((float)x, (float)y, tb->border)) {
if (tb->getClickZoneRect().containsConsideringBorder((float)x, (float)y, tb->border)) {
tb->state = ButtonState::Pressed;
pressedTextButtons[fingerId] = tb;
if (tb->onPress) tb->onPress(tb->name);
@@ -1593,7 +1655,7 @@ namespace ZL {
if (btnIt != pressedButtons.end()) {
auto b = btnIt->second;
if (b) {
bool contains = b->rect.contains((float)x, (float)y);
bool contains = b->getClickZoneRect().contains((float)x, (float)y);
if (b->state == ButtonState::Pressed) {
if (contains) {
clicked.push_back(b);
@@ -1609,7 +1671,7 @@ namespace ZL {
if (tbIt != pressedTextButtons.end()) {
auto tb = tbIt->second;
if (tb) {
bool contains = tb->rect.contains((float)x, (float)y);
bool contains = tb->getClickZoneRect().contains((float)x, (float)y);
if (tb->state == ButtonState::Pressed) {
if (contains) {
clickedText.push_back(tb);
+43
View File
@@ -93,6 +93,17 @@ namespace ZL {
VertexRenderStruct mesh;
// Optional explicit hit-test zone, centred on rect.
// 0 means "use rect.w / rect.h" (default behaviour).
float clickZoneWidth = 0.0f;
float clickZoneHeight = 0.0f;
UiRect getClickZoneRect() const {
float cw = (clickZoneWidth > 0.0f) ? clickZoneWidth : rect.w;
float ch = (clickZoneHeight > 0.0f) ? clickZoneHeight : rect.h;
return { rect.x + (rect.w - cw) * 0.5f, rect.y + (rect.h - ch) * 0.5f, cw, ch };
}
std::function<void(const std::string&)> onClick;
std::function<void(const std::string&)> onPress; // fires on touch/mouse down
@@ -139,6 +150,17 @@ namespace ZL {
ButtonState state = ButtonState::Normal;
VertexRenderStruct mesh;
// Optional explicit hit-test zone, centred on rect.
// 0 means "use rect.w / rect.h" (default behaviour).
float clickZoneWidth = 0.0f;
float clickZoneHeight = 0.0f;
UiRect getClickZoneRect() const {
float cw = (clickZoneWidth > 0.0f) ? clickZoneWidth : rect.w;
float ch = (clickZoneHeight > 0.0f) ? clickZoneHeight : rect.h;
return { rect.x + (rect.w - cw) * 0.5f, rect.y + (rect.h - ch) * 0.5f, cw, ch };
}
// Text drawn on top of the button
std::string text;
std::string fontPath = "resources/fonts/DroidSans.ttf";
@@ -209,6 +231,11 @@ namespace ZL {
VertexRenderStruct mesh;
// Fade-in on first display (triggers each time the containing UI is shown)
bool fadeInEnabled = false;
float fadeInDurationMs = 1000.0f;
float fadeInElapsedMs = 0.0f; // runtime, reset by collectButtonsAndSliders
void buildMesh();
void draw(Renderer& renderer) const;
};
@@ -236,6 +263,13 @@ namespace ZL {
bool visible = true;
// Pulse-scale animation (StaticImage only; auto-starts when pulseEnabled == true)
bool pulseEnabled = false;
float pulseMinScale = 1.0f;
float pulseMaxScale = 1.1f;
float pulsePeriodMs = 1000.0f;
float pulseElapsedMs = 0.0f; // runtime, not persisted in JSON
// Иерархия
std::vector<std::shared_ptr<UiNode>> children;
@@ -322,6 +356,13 @@ namespace ZL {
tb->animScaleY = 1.0f;
}
}
for (auto& n : pulsingNodes) {
if (n) {
n->scaleX = 1.0f;
n->scaleY = 1.0f;
n->pulseElapsedMs = 0.0f;
}
}
}
std::shared_ptr<UiButton> findButton(const std::string& name);
@@ -404,6 +445,7 @@ namespace ZL {
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
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)
@@ -422,6 +464,7 @@ namespace ZL {
std::vector<std::shared_ptr<UiTextView>> textViews;
std::vector<std::shared_ptr<UiTextField>> textFields;
std::vector<std::shared_ptr<UiStaticImage>> staticImages;
std::vector<std::shared_ptr<UiNode>> pulsingNodes;
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<UiSlider>> pressedSliders;
+5
View File
@@ -104,6 +104,7 @@ bool DialogueSystem::handlePointerReleased(float x, float y) {
if (advanceDialogue) {
runtime.confirmAdvance();
if (onDialogueAdvancedCallback) onDialogueAdvancedCallback();
return true;
}
@@ -143,4 +144,8 @@ void DialogueSystem::stopDialogue() {
runtime.stop();
}
void DialogueSystem::setOnDialogueAdvanced(std::function<void()> cb) {
onDialogueAdvancedCallback = std::move(cb);
}
} // namespace ZL::Dialogue
+2
View File
@@ -27,6 +27,7 @@ public:
void setOnDialogueLineStarted(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 setOnDialogueAdvanced(std::function<void()> cb);
void stopDialogue();
bool isActive() const { return runtime.isActive(); }
@@ -39,6 +40,7 @@ private:
DialogueDatabase database;
DialogueRuntime runtime;
DialogueOverlay overlay;
std::function<void()> onDialogueAdvancedCallback;
};
} // namespace ZL::Dialogue
+1
View File
@@ -7,6 +7,7 @@ namespace ZL {
void Inventory::addItem(const Item& item) {
items.push_back(item);
std::cout << "Item added to inventory: " << item.name << std::endl;
if (onItemAdded) onItemAdded(item.id);
}
void Inventory::removeItem(const std::string& itemId) {
+4
View File
@@ -2,6 +2,7 @@
#include <string>
#include <memory>
#include <vector>
#include <functional>
namespace ZL {
@@ -28,6 +29,9 @@ namespace ZL {
bool hasItem(const std::string& itemId) const;
void clear() { items.clear(); }
size_t getCount() const { return items.size(); }
// Called whenever an item is added; receives the item id.
std::function<void(const std::string&)> onItemAdded;
};
} // namespace ZL