#pragma once #include #include #include #include #include #include #include #include "UIRenderCommands.h" #ifdef MT_NATIVE_HAS_LIVE_CLIENT #include "platform/ScriptLib/PythonBoot.h" #endif class TouchController { public: struct ButtonDef { int id; // 1 = attack, 2..7 = quick slots 1..6, 8 = auto hunt int dik; float x, y, radius; const char* label; uint32_t color_idle; uint32_t color_pressed; bool pressed = false; int64_t finger_id = -1; }; TouchController() { init_buttons(); } void set_enabled(bool enabled) { enabled_ = enabled; #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetTouchInput(enabled); #endif } bool is_enabled() const { return enabled_; } // Short vibration when a long press fires (the host wires it to the platform). void set_haptic(void (*haptic)()) { haptic_ = haptic; } // The system keyboard is up only after the player taps the focused EditLine, not when a // script focuses one on its own (intrologin.py focuses the ID field as the board opens). // The serial changes on every such tap so the host can re-show a keyboard the user dismissed. bool wants_screen_keyboard() const { return keyboard_requested_; } unsigned screen_keyboard_serial() const { return keyboard_serial_; } // safe_inset: logical pixels kept clear at the left and right edges (rounded corners, cutouts). void update_screen_size(int width, int height, int safe_inset = 0) { if (width <= 0 || height <= 0) return; screen_w_ = width; screen_h_ = height; safe_inset_ = float(std::max(0, safe_inset)); // Position joystick on lower-left joystick_base_x_ = safe_inset_ + std::max(90.0f, float(width) * 0.12f); joystick_base_y_ = float(height) - std::max(90.0f, float(height) * 0.22f); if (!joystick_active_) { joystick_knob_x_ = joystick_base_x_; joystick_knob_y_ = joystick_base_y_; } layout_buttons(); } // Called once per frame to maintain continuous analog movement void update() { if (!enabled_) return; #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (keyboard_requested_ && !PythonBoot::TextInputFocused()) keyboard_requested_ = false; // A world tap presses and releases on later frames than its mouse move so // 40250's per-frame actor picking has already hit the target under the finger. if (tap_stage_ == 1) { PythonBoot::UIMouseMove(int(tap_x_), int(tap_y_)); PythonBoot::UIMouseButton(1, true, int(tap_x_), int(tap_y_)); tap_stage_ = 2; } else if (tap_stage_ == 2) { PythonBoot::UIMouseButton(1, false, int(tap_x_), int(tap_y_)); tap_stage_ = 0; } // A UI finger held still turns into a right click (use item, equip, skill up...). if (ui_touch_finger_id_ >= 0 && ui_gesture_ == UiGesture::Pending && get_time_sec() - ui_down_time_ >= kLongPressSec) { ui_gesture_ = UiGesture::LongPressed; last_tap_time_ = -1.0; PythonBoot::UIMouseButton(2, true, int(ui_start_x_), int(ui_start_y_)); PythonBoot::UIMouseButton(2, false, int(ui_start_x_), int(ui_start_y_)); if (haptic_) haptic_(); } for (auto& btn : buttons_) { if (btn.id == kAutoHuntButton && btn.pressed && !auto_hunt_long_pressed_ && get_time_sec() - auto_hunt_down_time_ >= kLongPressSec) { auto_hunt_long_pressed_ = true; PythonBoot::AutoHuntOpenSettings(); if (haptic_) haptic_(); } } #endif const bool controls_visible = gameplay_controls_visible(); if (!controls_visible) { if (controls_were_visible_) release_gameplay_controls(); controls_were_visible_ = false; return; } controls_were_visible_ = true; if (joystick_active_) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetMoveDirection(move_angle_, true); #endif } } // Handles finger touch down (norm_x, norm_y in 0.0 .. 1.0) bool on_finger_down(int64_t finger_id, float norm_x, float norm_y) { if (!enabled_) return false; const float px = norm_x * float(screen_w_); const float py = norm_y * float(screen_h_); const bool controls_visible = gameplay_controls_visible(); // The mobile overlay owns only the movement and combat control regions. if (controls_visible) { const int btn_idx = find_button(px, py); if (btn_idx >= 0) { auto& btn = buttons_[btn_idx]; #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (is_quick_slot_button(btn.id) && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2)) return true; #endif btn.pressed = true; btn.finger_id = finger_id; if (btn.id == kAutoHuntButton) { // Tap toggles; holding opens the settings window (update()). auto_hunt_down_time_ = get_time_sec(); auto_hunt_long_pressed_ = false; return true; } trigger_button(btn.dik, true); return true; } } // Everything else uses the unchanged 40250 UI and its normal mouse path. #ifdef MT_NATIVE_HAS_LIVE_CLIENT // Only hover here (the tooltip shows while the finger rests); what the finger does next picks // the gesture: lift = click, slide = press + drag, hold = right click, second tap = double click. if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) { ui_touch_finger_id_ = finger_id; ui_gesture_ = UiGesture::Pending; ui_start_x_ = px; ui_start_y_ = py; ui_down_time_ = get_time_sec(); PythonBoot::UIMouseMove(int(px), int(py)); return true; } #endif // Mobile-game layout: the left half moves (floating stick), the right half looks. if (controls_visible && norm_x < kMoveZoneRight && norm_y > kMoveZoneTop && !joystick_active_) { joystick_active_ = true; joystick_finger_id_ = finger_id; joystick_base_x_ = px; joystick_base_y_ = py; joystick_knob_x_ = px; joystick_knob_y_ = py; update_joystick_motion(px, py); return true; } if (!controls_visible) return false; const bool in_look_zone = norm_x >= kMoveZoneRight; if (look_finger_id_ < 0) { // Outside the look zone the finger can only tap-select. look_finger_id_ = finger_id; look_can_rotate_ = in_look_zone; look_rotating_ = false; look_moved_ = false; look_start_x_ = look_last_x_ = px; look_start_y_ = look_last_y_ = py; look_down_time_ = get_time_sec(); #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::UIMouseMove(int(px), int(py)); // start picking under the finger #endif return true; } if (pinch_finger_id_ < 0 && look_can_rotate_ && in_look_zone) { pinch_finger_id_ = finger_id; pinch_x_ = px; pinch_y_ = py; pinch_last_dist_ = std::hypot(px - look_last_x_, py - look_last_y_); look_rotating_ = false; look_moved_ = true; // a pinch never ends as a tap return true; } return false; } // Handles finger motion bool on_finger_motion(int64_t finger_id, float norm_x, float norm_y) { if (!enabled_) return false; const float px = norm_x * float(screen_w_); const float py = norm_y * float(screen_h_); // 1. UI Touch dragging (e.g. dragging item in inventory or scrollbar) if (ui_touch_finger_id_ == finger_id) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (ui_gesture_ == UiGesture::Pending && std::hypot(px - ui_start_x_, py - ui_start_y_) > kUiDragSlop) { // 40250 drag: press where the finger landed (a slot attaches its icon, a title bar // or scroll bar captures), then follow the finger. ui_gesture_ = UiGesture::Dragging; last_tap_time_ = -1.0; PythonBoot::UIMouseButton(1, true, int(ui_start_x_), int(ui_start_y_)); } PythonBoot::UIMouseMove(int(px), int(py)); #endif return true; } // 2. Virtual Joystick finger if (joystick_active_ && finger_id == joystick_finger_id_) { update_joystick_motion(px, py); return true; } // 3. Button drag tracking (check if finger slid off) for (auto& btn : buttons_) { if (btn.finger_id == finger_id) { const float dist = std::hypot(px - btn.x, py - btn.y); if (dist > btn.radius * 1.5f && btn.pressed) { btn.pressed = false; trigger_button(btn.dik, false); } else if (dist <= btn.radius * 1.5f && !btn.pressed) { btn.pressed = true; trigger_button(btn.dik, true); } return true; } } // 4. Two-finger pinch zoom (look finger + second right-half finger) if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) { if (finger_id == pinch_finger_id_) { pinch_x_ = px; pinch_y_ = py; } else { look_last_x_ = px; look_last_y_ = py; } const float cur_dist = std::hypot(pinch_x_ - look_last_x_, pinch_y_ - look_last_y_); const float delta_d = cur_dist - pinch_last_dist_; if (std::abs(delta_d) > 2.0f) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::UIMouseWheel(int(delta_d * 8.0f)); #endif pinch_last_dist_ = cur_dist; } return true; } // 5. Single-finger look drag: yaw by dx, pitch by dy, stops the moment the finger stops. if (finger_id == look_finger_id_) { if (!look_moved_ && std::hypot(px - look_start_x_, py - look_start_y_) > kLookDeadZone) { look_moved_ = true; look_rotating_ = look_can_rotate_; look_last_x_ = px; // start from here so the dead zone does not jump the camera look_last_y_ = py; } if (look_rotating_) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::CameraRotateBy((px - look_last_x_) * kLookYawDegPerPx, (py - look_last_y_) * kLookPitchDegPerPx); #endif } look_last_x_ = px; look_last_y_ = py; return true; } return false; } // Handles finger touch up bool on_finger_up(int64_t finger_id, float norm_x, float norm_y) { if (!enabled_) return false; const float px = norm_x * float(screen_w_); const float py = norm_y * float(screen_h_); // 1. UI Touch release (e.g. dropped item in inventory or clicked button) if (ui_touch_finger_id_ == finger_id) { ui_touch_finger_id_ = -1; #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (ui_gesture_ == UiGesture::Dragging) { // Dropped on a skill button: that quick slot takes the dragged icon. const int btn_idx = gameplay_controls_visible() ? find_button(px, py) : -1; if (btn_idx >= 0 && is_quick_slot_button(buttons_[btn_idx].id) && PythonBoot::UIIsAttaching()) { PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(buttons_[btn_idx].id - 2); PythonBoot::UIMouseButton(1, false, int(ui_start_x_), int(ui_start_y_)); } else { PythonBoot::UIMouseButton(1, false, int(px), int(py)); } } else if (ui_gesture_ == UiGesture::Pending) { const double now = get_time_sec(); const int x = int(ui_start_x_), y = int(ui_start_y_); if (last_tap_time_ >= 0.0 && now - last_tap_time_ < kDoubleTapSec && std::hypot(ui_start_x_ - last_tap_x_, ui_start_y_ - last_tap_y_) < kDoubleTapSlop) { // Win32 sends DOWN, UP, DBLCLK, UP; the first click picked the icon up, which a // mouse user would have seen and a finger hides, so put it back first. if (last_tap_attached_ && PythonBoot::UIIsAttaching()) PythonBoot::UIDeattachObject(); PythonBoot::UIMouseDoubleClick(x, y); PythonBoot::UIMouseButton(1, false, x, y); last_tap_time_ = -1.0; } else { const bool was_attaching = PythonBoot::UIIsAttaching(); PythonBoot::UIMouseButton(1, true, x, y); PythonBoot::UIMouseButton(1, false, x, y); last_tap_time_ = now; last_tap_x_ = ui_start_x_; last_tap_y_ = ui_start_y_; last_tap_attached_ = !was_attaching && PythonBoot::UIIsAttaching(); } keyboard_requested_ = PythonBoot::TextInputFocused() && PythonBoot::IsPointInsideFocusedWindow(x, y); if (keyboard_requested_) ++keyboard_serial_; } ui_gesture_ = UiGesture::None; #endif return true; } // 2. Joystick release if (joystick_active_ && finger_id == joystick_finger_id_) { joystick_active_ = false; joystick_finger_id_ = -1; joystick_knob_x_ = joystick_base_x_; joystick_knob_y_ = joystick_base_y_; #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetMoveDirection(0.0f, false); #endif return true; } // 3. Button release for (auto& btn : buttons_) { if (btn.finger_id == finger_id) { if (btn.pressed) { btn.pressed = false; trigger_button(btn.dik, false); #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (btn.id == kAutoHuntButton && !auto_hunt_long_pressed_) PythonBoot::AutoHuntToggle(); #endif } btn.finger_id = -1; return true; } } // 4. Pinch end: the finger left down keeps looking (never a tap). if (pinch_finger_id_ >= 0 && (finger_id == pinch_finger_id_ || finger_id == look_finger_id_)) { if (finger_id == look_finger_id_) { look_finger_id_ = pinch_finger_id_; look_last_x_ = pinch_x_; look_last_y_ = pinch_y_; } pinch_finger_id_ = -1; pinch_last_dist_ = 0.0f; look_rotating_ = look_can_rotate_; look_moved_ = true; return true; } // 5. Look finger release; a short still touch selects a target (mob, NPC, ground). if (finger_id == look_finger_id_) { look_finger_id_ = -1; look_rotating_ = false; keyboard_requested_ = false; if (!look_moved_ && (get_time_sec() - look_down_time_) < kTapMaxSec && tap_stage_ == 0) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::UIMouseMove(int(px), int(py)); tap_x_ = px; tap_y_ = py; tap_stage_ = 1; #endif } return true; } return false; } // Overlay only the controls that have no practical desktop-UI touch equivalent. void append_ui_commands(std::vector& commands) const { if (!enabled_ || !gameplay_controls_visible()) return; // Right-side attack and quick-slot buttons. for (const auto& btn : buttons_) { const uint32_t col = btn.pressed ? btn.color_pressed : btn.color_idle; draw_filled_disc(commands, btn.x, btn.y, btn.radius, col); draw_circle(commands, btn.x, btn.y, btn.radius * 0.85f, btn.pressed ? 0xFFFFFFFF : 0x70FFFFFF, 16); const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF; const float r = btn.radius; #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (btn.id == kAutoHuntButton) { draw_auto_hunt_button(commands, btn); continue; } if (is_quick_slot_button(btn.id)) { std::string skill_icon; float uv[4]; int count = 0; if (PythonBoot::LocalQuickSlotIcon(btn.id - 2, &skill_icon, uv, &count)) { UIRenderCommand icon{}; icon.kind = UIRenderCommand::Image; icon.x1 = btn.x - r * 0.68f; icon.y1 = btn.y - r * 0.68f; icon.x2 = btn.x + r * 0.68f; icon.y2 = btn.y + r * 0.68f; icon.argb = icon_col; icon.text = skill_icon; icon.su = uv[0]; icon.sv = uv[1]; icon.eu = uv[2]; icon.ev = uv[3]; commands.push_back(std::move(icon)); if (count > 0) { const std::string digits = std::to_string(std::min(count, 9999)); const float h = r * 0.26f; draw_segment_text(commands, digits, btn.x + r * 0.62f - segment_text_width(digits, h), btn.y + r * 0.62f - h, h); } continue; } } #endif switch (btn.id) { case 1: { // ATK: crossed swords const float s = r * 0.35f; draw_line(commands, btn.x - s, btn.y - s, btn.x + s, btn.y + s, icon_col); draw_line(commands, btn.x + s, btn.y - s, btn.x - s, btn.y + s, icon_col); const float g = s * 0.35f; draw_line(commands, btn.x - s*0.4f - g, btn.y - s*0.4f + g, btn.x - s*0.4f + g, btn.y - s*0.4f - g, icon_col); draw_line(commands, btn.x + s*0.4f - g, btn.y - s*0.4f - g, btn.x + s*0.4f + g, btn.y - s*0.4f + g, icon_col); break; } case 2: { // S1: I const float h = r * 0.35f; draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col); draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x + 4.0f, btn.y - h, icon_col); draw_line(commands, btn.x - 4.0f, btn.y + h, btn.x + 4.0f, btn.y + h, icon_col); break; } case 3: { // S2: II const float h = r * 0.35f; draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x - 4.0f, btn.y + h, icon_col); draw_line(commands, btn.x + 4.0f, btn.y - h, btn.x + 4.0f, btn.y + h, icon_col); break; } case 4: { // S3: III const float h = r * 0.35f; draw_line(commands, btn.x - 6.0f, btn.y - h, btn.x - 6.0f, btn.y + h, icon_col); draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col); draw_line(commands, btn.x + 6.0f, btn.y - h, btn.x + 6.0f, btn.y + h, icon_col); break; } case 5: { // S4: IV const float h = r * 0.35f; draw_line(commands, btn.x - 7.0f, btn.y - h, btn.x - 7.0f, btn.y + h, icon_col); draw_line(commands, btn.x, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col); draw_line(commands, btn.x + 14.0f, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col); break; } case 6: { // S5: V const float h = r * 0.35f; draw_line(commands, btn.x - 8.0f, btn.y - h, btn.x, btn.y + h, icon_col); draw_line(commands, btn.x + 8.0f, btn.y - h, btn.x, btn.y + h, icon_col); break; } case 7: { // S6: VI const float h = r * 0.35f; draw_line(commands, btn.x - 11.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col); draw_line(commands, btn.x + 5.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col); draw_line(commands, btn.x + 11.0f, btn.y - h, btn.x + 11.0f, btn.y + h, icon_col); break; } } } // Left-side movement joystick. draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_, 0x8080C0FF, 24); draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_ * 0.45f, 0x4080C0FF, 16); draw_line(commands, joystick_base_x_ - joystick_radius_, joystick_base_y_, joystick_base_x_ - joystick_radius_ + 8.0f, joystick_base_y_, 0x90FFFFFF); draw_line(commands, joystick_base_x_ + joystick_radius_ - 8.0f, joystick_base_y_, joystick_base_x_ + joystick_radius_, joystick_base_y_, 0x90FFFFFF); draw_line(commands, joystick_base_x_, joystick_base_y_ - joystick_radius_, joystick_base_x_, joystick_base_y_ - joystick_radius_ + 8.0f, 0x90FFFFFF); draw_line(commands, joystick_base_x_, joystick_base_y_ + joystick_radius_ - 8.0f, joystick_base_x_, joystick_base_y_ + joystick_radius_, 0x90FFFFFF); if (joystick_active_) { draw_line(commands, joystick_base_x_, joystick_base_y_, joystick_knob_x_, joystick_knob_y_, 0xB000FFFF); } const uint32_t knob_color = joystick_active_ ? 0xB040A0FF : 0x6040A0FF; draw_filled_disc(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_, knob_color); draw_circle(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_ * 0.5f, 0x80FFFFFF, 12); } // Desktop mouse testing simulation bool on_mouse_button(int button, bool pressed, int x, int y) { if (!enabled_) return false; const float norm_x = float(x) / float(screen_w_); const float norm_y = float(y) / float(screen_h_); if (pressed) { if (button == 1) { return on_finger_down(101, norm_x, norm_y); } return false; } else { if (button == 1) { return on_finger_up(101, norm_x, norm_y); } return false; } } bool on_mouse_motion(int x, int y) { if (!enabled_) return false; const float norm_x = float(x) / float(screen_w_); const float norm_y = float(y) / float(screen_h_); bool handled = false; if (ui_touch_finger_id_ == 101) { handled |= on_finger_motion(101, norm_x, norm_y); } if (joystick_active_ && joystick_finger_id_ == 101) { handled |= on_finger_motion(101, norm_x, norm_y); } if (look_finger_id_ == 101) { handled |= on_finger_motion(101, norm_x, norm_y); } return handled; } // Seven-segment text (digits, 'F', 'P', 'S', ' ') with its top-left at (left, top), each glyph h // tall, on a dark backing. Used for quick-slot counts and the test-build FPS readout. static void draw_segment_text(std::vector& commands, const std::string& text, float left, float top, float h, std::uint32_t color = 0xFFFFFFFF) { static constexpr std::uint8_t kDigits[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F}; const float w = h * 0.55f, gap = h * 0.3f; UIRenderCommand back{}; back.kind = UIRenderCommand::Bar; back.x1 = left - 3.0f; back.y1 = top - 3.0f; back.x2 = left + segment_text_width(text, h) + 3.0f; back.y2 = top + h + 3.0f; back.argb = 0xA0000000; commands.push_back(back); float x = left; for (char c : text) { std::uint8_t seg = 0; if (c >= '0' && c <= '9') seg = kDigits[c - '0']; else if (c == 'F') seg = 0x71; else if (c == 'P') seg = 0x73; else if (c == 'S') seg = 0x6D; const float t = top, m = top + h * 0.5f, b = top + h; if (seg & 0x01) draw_line(commands, x, t, x + w, t, color); if (seg & 0x02) draw_line(commands, x + w, t, x + w, m, color); if (seg & 0x04) draw_line(commands, x + w, m, x + w, b, color); if (seg & 0x08) draw_line(commands, x, b, x + w, b, color); if (seg & 0x10) draw_line(commands, x, m, x, b, color); if (seg & 0x20) draw_line(commands, x, t, x, m, color); if (seg & 0x40) draw_line(commands, x, m, x + w, m, color); x += w + gap; } } static float segment_text_width(const std::string& text, float h) { return text.empty() ? 0.0f : float(text.size()) * h * 0.85f - h * 0.3f; } private: bool gameplay_controls_visible() const { #ifdef MT_NATIVE_HAS_LIVE_CLIENT return !PythonBoot::CurrentMapName().empty(); #else return false; #endif } void release_gameplay_controls() { if (joystick_active_) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetMoveDirection(0.0f, false); #endif } joystick_active_ = false; joystick_finger_id_ = -1; joystick_knob_x_ = joystick_base_x_; joystick_knob_y_ = joystick_base_y_; for (auto& btn : buttons_) { if (btn.pressed) trigger_button(btn.dik, false); btn.pressed = false; btn.finger_id = -1; } look_finger_id_ = -1; look_rotating_ = false; pinch_finger_id_ = -1; pinch_last_dist_ = 0.0f; } static constexpr int kAutoHuntButton = 8; static bool is_quick_slot_button(int id) { return id >= 2 && id <= 7; } // A circular arrow; green while the hunt runs. static void draw_auto_hunt_button(std::vector& commands, const ButtonDef& btn) { bool on = false; #ifdef MT_NATIVE_HAS_LIVE_CLIENT on = PythonBoot::AutoHuntIsEnabled(); #endif const uint32_t disc = btn.pressed ? btn.color_pressed : (on ? 0xC040C040 : btn.color_idle); draw_filled_disc(commands, btn.x, btn.y, btn.radius, disc); const uint32_t col = on ? 0xFFB0FFB0 : 0xDDFFFFFF; const float r = btn.radius * 0.45f; const float kPi = 3.14159265f; const int segments = 12; const float a0 = -kPi * 0.35f, a1 = a0 + kPi * 1.6f; for (int i = 0; i < segments; ++i) { const float t0 = a0 + (a1 - a0) * float(i) / float(segments); const float t1 = a0 + (a1 - a0) * float(i + 1) / float(segments); draw_line(commands, btn.x + std::cos(t0) * r, btn.y + std::sin(t0) * r, btn.x + std::cos(t1) * r, btn.y + std::sin(t1) * r, col); } // Arrowhead at the arc's end, pointing along the direction of travel. const float ex = btn.x + std::cos(a1) * r, ey = btn.y + std::sin(a1) * r; const float tx = -std::sin(a1), ty = std::cos(a1); const float nx = std::cos(a1), ny = std::sin(a1); const float h = r * 0.55f; draw_line(commands, ex, ey, ex - tx * h + nx * h * 0.6f, ey - ty * h + ny * h * 0.6f, col); draw_line(commands, ex, ey, ex - tx * h - nx * h * 0.6f, ey - ty * h - ny * h * 0.6f, col); if (on) { // A play dot in the middle while running. draw_rect_bar(commands, btn.x - 2.0f, btn.y - 2.0f, btn.x + 2.0f, btn.y + 2.0f, col); } } void init_buttons() { buttons_.clear(); // Lower-right combat controls. All menus and status UI remain the original 40250 UI. buttons_.push_back({1, 0x39, 0, 0, 42.0f, "ATK", 0x80D48820, 0xD0FFB040}); buttons_.push_back({2, 0x02, 0, 0, 26.0f, "S1", 0x803060C0, 0xD05080FF}); buttons_.push_back({3, 0x03, 0, 0, 26.0f, "S2", 0x80903090, 0xD0D050D0}); buttons_.push_back({4, 0x04, 0, 0, 26.0f, "S3", 0x80309060, 0xD050D080}); buttons_.push_back({5, 0x05, 0, 0, 26.0f, "S4", 0x80906030, 0xD0D08050}); buttons_.push_back({6, 0x06, 0, 0, 26.0f, "S5", 0x80603090, 0xD08050D0}); buttons_.push_back({7, 0x07, 0, 0, 26.0f, "S6", 0x80308090, 0xD050C0D0}); buttons_.push_back({kAutoHuntButton, 0, 0, 0, 24.0f, "AUTO", 0x80707070, 0xD0FFFFFF}); } void layout_buttons() { const float W = float(screen_w_) - safe_inset_; const float H = float(screen_h_); for (auto& btn : buttons_) { switch (btn.id) { case 1: btn.x = W - 75.0f; btn.y = H - 85.0f; btn.radius = std::min(46.0f, H * 0.12f); break; case 2: btn.x = W - 360.0f; btn.y = H - 75.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case 3: btn.x = W - 295.0f; btn.y = H - 75.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case 4: btn.x = W - 230.0f; btn.y = H - 75.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case 5: btn.x = W - 165.0f; btn.y = H - 85.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case 6: btn.x = W - 125.0f; btn.y = H - 155.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case 7: btn.x = W - 75.0f; btn.y = H - 225.0f; btn.radius = std::min(28.0f, H * 0.08f); break; case kAutoHuntButton: btn.x = W - 75.0f; btn.y = H - 295.0f; btn.radius = std::min(24.0f, H * 0.07f); break; } } } int find_button(float x, float y) { for (size_t i = 0; i < buttons_.size(); ++i) { const auto& btn = buttons_[i]; const float dist = std::hypot(x - btn.x, y - btn.y); if (dist <= btn.radius * 1.25f) { return int(i); } } return -1; } void trigger_button(int dik, bool pressed) { #ifdef MT_NATIVE_HAS_LIVE_CLIENT if (dik == 0x39) { PythonBoot::SetAttackKey(pressed); } if (dik >= 0x02 && dik <= 0x07) { if (pressed) PythonBoot::UseLocalQuickSlot(dik - 0x02); return; } if (dik != 0) { PythonBoot::UIKey(dik, pressed); } #endif } void update_joystick_motion(float px, float py) { const float dx = px - joystick_base_x_; const float dy = py - joystick_base_y_; const float dist = std::hypot(dx, dy); if (dist <= joystick_radius_) { joystick_knob_x_ = px; joystick_knob_y_ = py; } else if (dist > 0.0f) { joystick_knob_x_ = joystick_base_x_ + (dx / dist) * joystick_radius_; joystick_knob_y_ = joystick_base_y_ + (dy / dist) * joystick_radius_; } if (dist > 8.0f) { const float rad = std::atan2(-dx, -dy); move_angle_ = rad * 180.0f / 3.14159265358979323846f; if (move_angle_ < 0.0f) move_angle_ += 360.0f; #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetMoveDirection(move_angle_, true); #endif } else { #ifdef MT_NATIVE_HAS_LIVE_CLIENT PythonBoot::SetMoveDirection(0.0f, false); #endif } } static void draw_line(std::vector& commands, float x1, float y1, float x2, float y2, uint32_t argb) { UIRenderCommand cmd{}; cmd.kind = UIRenderCommand::Line; cmd.x1 = x1; cmd.y1 = y1; cmd.x2 = x2; cmd.y2 = y2; cmd.argb = argb; commands.push_back(cmd); } static void draw_rect_bar(std::vector& commands, float x1, float y1, float x2, float y2, uint32_t argb) { UIRenderCommand bar{}; bar.kind = UIRenderCommand::Bar; bar.x1 = x1; bar.y1 = y1; bar.x2 = x2; bar.y2 = y2; bar.argb = argb; commands.push_back(bar); } static void draw_rect_lines(std::vector& commands, float x1, float y1, float x2, float y2, uint32_t argb) { draw_line(commands, x1, y1, x2, y1, argb); draw_line(commands, x2, y1, x2, y2, argb); draw_line(commands, x2, y2, x1, y2, argb); draw_line(commands, x1, y2, x1, y1, argb); } static void draw_circle(std::vector& commands, float cx, float cy, float radius, uint32_t argb, int segments = 16) { const float step = 2.0f * 3.14159265f / float(segments); for (int i = 0; i < segments; ++i) { const float a1 = float(i) * step; const float a2 = float(i + 1) * step; draw_line(commands, cx + std::cos(a1) * radius, cy + std::sin(a1) * radius, cx + std::cos(a2) * radius, cy + std::sin(a2) * radius, argb); } } static void draw_filled_disc(std::vector& commands, float cx, float cy, float radius, uint32_t argb) { // Base rectangular fill draw_rect_bar(commands, cx - radius * 0.65f, cy - radius * 0.65f, cx + radius * 0.65f, cy + radius * 0.65f, (argb & 0x00FFFFFF) | 0x55000000); // Cross fills for roundness draw_rect_bar(commands, cx - radius * 0.85f, cy - radius * 0.35f, cx + radius * 0.85f, cy + radius * 0.35f, (argb & 0x00FFFFFF) | 0x55000000); draw_rect_bar(commands, cx - radius * 0.35f, cy - radius * 0.85f, cx + radius * 0.35f, cy + radius * 0.85f, (argb & 0x00FFFFFF) | 0x55000000); // Border rings draw_circle(commands, cx, cy, radius, argb, 20); draw_circle(commands, cx, cy, radius - 1.0f, (argb & 0x00FFFFFF) | 0x40000000, 20); } static double get_time_sec() { using namespace std::chrono; return duration_cast>(steady_clock::now().time_since_epoch()).count(); } bool enabled_ = false; int screen_w_ = 1280; int screen_h_ = 720; float safe_inset_ = 0.0f; bool controls_were_visible_ = false; int64_t ui_touch_finger_id_ = -1; enum class UiGesture { None, Pending, Dragging, LongPressed }; UiGesture ui_gesture_ = UiGesture::None; float ui_start_x_ = 0.0f; float ui_start_y_ = 0.0f; double ui_down_time_ = 0.0; double last_tap_time_ = -1.0; float last_tap_x_ = 0.0f; float last_tap_y_ = 0.0f; bool last_tap_attached_ = false; void (*haptic_)() = nullptr; static constexpr float kUiDragSlop = 10.0f; static constexpr double kLongPressSec = 0.5; static constexpr double kDoubleTapSec = 0.35; static constexpr float kDoubleTapSlop = 24.0f; bool keyboard_requested_ = false; unsigned keyboard_serial_ = 0; bool joystick_active_ = false; int64_t joystick_finger_id_ = -1; float joystick_base_x_ = 140.0f; float joystick_base_y_ = 580.0f; float joystick_knob_x_ = 140.0f; float joystick_knob_y_ = 580.0f; float joystick_radius_ = 65.0f; float joystick_knob_radius_ = 28.0f; float move_angle_ = 0.0f; // Zones are fractions of the screen; distances are logical UI pixels. static constexpr float kMoveZoneRight = 0.5f; static constexpr float kMoveZoneTop = 0.2f; static constexpr float kLookDeadZone = 8.0f; static constexpr float kLookYawDegPerPx = 0.25f; static constexpr float kLookPitchDegPerPx = 0.15f; static constexpr double kTapMaxSec = 0.3; int64_t look_finger_id_ = -1; bool look_can_rotate_ = false; bool look_rotating_ = false; bool look_moved_ = false; float look_start_x_ = 0.0f; float look_start_y_ = 0.0f; float look_last_x_ = 0.0f; float look_last_y_ = 0.0f; double look_down_time_ = 0.0; int64_t pinch_finger_id_ = -1; float pinch_x_ = 0.0f; float pinch_y_ = 0.0f; float pinch_last_dist_ = 0.0f; int tap_stage_ = 0; // 1: press next frame, 2: release next frame float tap_x_ = 0.0f; float tap_y_ = 0.0f; std::vector buttons_; double auto_hunt_down_time_ = 0.0; bool auto_hunt_long_pressed_ = false; };