Files
mtgodot-poc/native_render/touch_controller.h
T
shenleiandClaude Opus 5.5 3e3708ef06 android-native: auto hunt, in-client account registration, mobile UI polish
- Auto hunt (client-only): PythonPlayerAutoHunt.cpp + AutoHuntScript.inc as the
  embedded mt_autohunt module, AUTO touch button, F9/F8.
- Account registration: tools/40250/register_server.py (rc.d mt_register, :11080)
  inserts into account.account; net.RegisterAccount/GetRegisterAccountResult
  (PythonAccountRegister.cpp) and the mt_register login-window dialog
  (RegisterScript.inc).
- --py-exec also runs in --login-screen mode for scripted login-screen tests.
- Touch controller, window manager and render command updates for the
  Android native client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:15:04 +09:00

893 lines
36 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <string>
#include <vector>
#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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<UIRenderCommand>& 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<duration<double>>(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<ButtonDef> buttons_;
double auto_hunt_down_time_ = 0.0;
bool auto_hunt_long_pressed_ = false;
};