Working on UI improvement
This commit is contained in:
1 parent
0da1ae124c
commit
80af0e61ae
43 files changed
+889
-64
No files matched your search
+1
-1
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1519,6 +1519,7 @@ namespace ZL
|
||||
targetInteractNpc = nullptr;
|
||||
targetInteractNpcIndex = -1;
|
||||
targetTeleportZone = nullptr;
|
||||
if (onPlayerFloorWalk) onPlayerFloorWalk();
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
+6
-3
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user