Adapt Android mobile controls and native client UI

This commit is contained in:
shen
2026-09-28 19:39:13 -07:00
parent a1ae69aa7a
commit 3c52ced02e
180 changed files with 688 additions and 23782 deletions
+132 -211
View File
@@ -17,9 +17,7 @@
class TouchController {
public:
struct ButtonDef {
int id; // 1 = ATK, 2 = S1, 3 = S2, 4 = S3, 5 = POT, 6 = PICK
// 99 = MENU (toggle drawer)
// 10 = BAG (DIK_I), 11 = CHAR (DIK_C), 12 = SKILL (DIK_V), 13 = QUEST (DIK_N), 14 = COMM (DIK_M), 15 = SET (DIK_ESCAPE)
int id; // 1 = attack, 2..7 = quick slots 1..6
int dik;
float x, y, radius;
const char* label;
@@ -53,6 +51,14 @@ public:
// Called once per frame to maintain continuous analog movement
void update() {
if (!enabled_) return;
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);
@@ -65,23 +71,25 @@ public:
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();
// 1. Check HUD buttons (Action cluster & Drawer menu)
int btn_idx = find_button(px, py);
if (btn_idx >= 0) {
auto& btn = buttons_[btn_idx];
if (btn.id == 99) { // MENU toggle button
drawer_open_ = !drawer_open_;
layout_buttons();
// 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 (btn.id >= 2 && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2))
return true;
#endif
btn.pressed = true;
btn.finger_id = finger_id;
trigger_button(btn.dik, true);
return true;
}
btn.pressed = true;
btn.finger_id = finger_id;
trigger_button(btn.dik, true);
return true;
}
// 2. Intelligent UI Hit-Testing: check if touch lands inside an open in-game UI window
// Everything else uses the unchanged 40250 UI and its normal mouse path.
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
ui_touch_finger_id_ = finger_id;
@@ -91,8 +99,8 @@ public:
}
#endif
// 3. Left side: virtual joystick (lower-left quadrant)
if (norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
// Start movement only in the gameplay phase.
if (controls_visible && norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
joystick_active_ = true;
joystick_finger_id_ = finger_id;
joystick_base_x_ = px;
@@ -103,7 +111,10 @@ public:
return true;
}
// 4. 3D Game World: camera rotation, pinch zoom, or tap to target
if (!controls_visible)
return false;
// The remaining gameplay area controls camera and world selection.
if (camera_finger_id_ < 0) {
camera_finger_id_ = finger_id;
camera_last_x_ = px;
@@ -265,80 +276,12 @@ public:
return false;
}
// Render virtual joystick, buttons, drawer menu, and player status bar
// Overlay only the controls that have no practical desktop-UI touch equivalent.
void append_ui_commands(std::vector<UIRenderCommand>& commands) const {
if (!enabled_) return;
if (!enabled_ || !gameplay_controls_visible()) return;
const float W = float(screen_w_);
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
// --- 1. Top-Left Player Status Bar (Mobile HUD) ---
const auto status = PythonBoot::GetPlayerStatusInfo();
if (status.max_hp > 0) {
const float bar_x = 20.0f;
const float bar_y = 16.0f;
const float bar_w = 205.0f;
const float bar_h = 44.0f;
// Background plate
draw_rect_bar(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x90101824);
draw_rect_lines(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x60405870);
// Avatar circle badge
const float av_cx = bar_x + 22.0f;
const float av_cy = bar_y + 22.0f;
const float av_r = 15.0f;
draw_filled_disc(commands, av_cx, av_cy, av_r, 0xB0203040);
draw_circle(commands, av_cx, av_cy, av_r, 0xE0FFD700, 16);
// "Lv" tick inside avatar
draw_line(commands, av_cx - 4.0f, av_cy - 4.0f, av_cx - 4.0f, av_cy + 3.0f, 0xFFFFFFFF);
draw_line(commands, av_cx - 4.0f, av_cy + 3.0f, av_cx - 1.0f, av_cy + 3.0f, 0xFFFFFFFF);
draw_line(commands, av_cx + 1.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
draw_line(commands, av_cx + 5.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
// Gauges
const float gx1 = bar_x + 44.0f;
const float gx2 = bar_x + bar_w - 10.0f;
const float gw = gx2 - gx1;
// HP Gauge
const float hp_y1 = bar_y + 10.0f;
const float hp_y2 = hp_y1 + 10.0f;
const float hp_ratio = std::clamp(float(status.hp) / float(status.max_hp), 0.0f, 1.0f);
draw_rect_bar(commands, gx1, hp_y1, gx2, hp_y2, 0x70301010);
draw_rect_bar(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y2, 0xE5D82424);
draw_line(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y1, 0x60FFFFFF); // Gloss line
draw_rect_lines(commands, gx1, hp_y1, gx2, hp_y2, 0x80502020);
// MP Gauge
const float mp_y1 = bar_y + 24.0f;
const float mp_y2 = mp_y1 + 8.0f;
const float mp_ratio = status.max_sp > 0 ? std::clamp(float(status.sp) / float(status.max_sp), 0.0f, 1.0f) : 0.0f;
draw_rect_bar(commands, gx1, mp_y1, gx2, mp_y2, 0x70102038);
draw_rect_bar(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y2, 0xE52068E0);
draw_line(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y1, 0x60FFFFFF); // Gloss line
draw_rect_lines(commands, gx1, mp_y1, gx2, mp_y2, 0x80204060);
// EXP bar (bottom trim line)
if (status.max_exp > 0) {
const float exp_ratio = std::clamp(float(status.exp) / float(status.max_exp), 0.0f, 1.0f);
draw_rect_bar(commands, bar_x, bar_y + bar_h - 2.0f, bar_x + bar_w * exp_ratio, bar_y + bar_h, 0xD0E0B020);
}
}
#endif
// --- 2. Top-Right Drawer Menu Bar ---
if (drawer_open_) {
// Background capsule tray behind drawer buttons
draw_rect_bar(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x90101824);
draw_rect_lines(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x60506880);
}
// --- 3. Buttons Rendering (Combat wheel & Drawer items) ---
// Right-side attack and quick-slot buttons.
for (const auto& btn : buttons_) {
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
continue;
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);
@@ -346,6 +289,24 @@ public:
const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF;
const float r = btn.radius;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (btn.id >= 2) {
const std::string skill_icon = PythonBoot::LocalQuickSlotSkillIcon(btn.id - 2);
if (!skill_icon.empty()) {
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;
commands.push_back(std::move(icon));
continue;
}
}
#endif
switch (btn.id) {
case 1: { // ATK: crossed swords
const float s = r * 0.35f;
@@ -376,69 +337,30 @@ public:
draw_line(commands, btn.x + 6.0f, btn.y - h, btn.x + 6.0f, btn.y + h, icon_col);
break;
}
case 5: { // POT: +
const float p = r * 0.4f;
draw_line(commands, btn.x - p, btn.y, btn.x + p, btn.y, icon_col);
draw_line(commands, btn.x, btn.y - p, btn.x, btn.y + p, icon_col);
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: { // PICK: Downward arrow
const float a = r * 0.35f;
draw_line(commands, btn.x - a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
draw_line(commands, btn.x + a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
draw_line(commands, btn.x, btn.y - a * 0.7f, btn.x, btn.y + a * 0.6f, icon_col);
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 99: { // MENU: ☰ hamburger icon
draw_line(commands, btn.x - 7.0f, btn.y - 5.0f, btn.x + 7.0f, btn.y - 5.0f, icon_col);
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
draw_line(commands, btn.x - 7.0f, btn.y + 5.0f, btn.x + 7.0f, btn.y + 5.0f, icon_col);
break;
}
case 10: { // BAG: Backpack
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 4.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y - 4.0f, btn.x, btn.y - 7.0f, icon_col);
draw_line(commands, btn.x, btn.y - 7.0f, btn.x + 3.0f, btn.y - 4.0f, icon_col);
draw_line(commands, btn.x - 6.0f, btn.y, btn.x + 6.0f, btn.y, icon_col);
break;
}
case 11: { // CHAR: Head + Shoulders
draw_circle(commands, btn.x, btn.y - 3.0f, 4.0f, icon_col, 12);
draw_line(commands, btn.x - 6.0f, btn.y + 6.0f, btn.x - 3.0f, btn.y + 2.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 3.0f, btn.y + 2.0f, icon_col);
draw_line(commands, btn.x + 3.0f, btn.y + 2.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
break;
}
case 12: { // SKILL: Lightning
draw_line(commands, btn.x + 2.0f, btn.y - 7.0f, btn.x - 3.0f, btn.y - 1.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y - 1.0f, btn.x + 1.0f, btn.y - 1.0f, icon_col);
draw_line(commands, btn.x + 1.0f, btn.y - 1.0f, btn.x - 2.0f, btn.y + 7.0f, icon_col);
break;
}
case 13: { // QUEST: Scroll
draw_rect_lines(commands, btn.x - 5.0f, btn.y - 6.0f, btn.x + 5.0f, btn.y + 6.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y - 2.0f, btn.x + 3.0f, btn.y - 2.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 1.0f, btn.y + 2.0f, icon_col);
break;
}
case 14: { // COMM: Chat bubble
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 5.0f, btn.x + 6.0f, btn.y + 3.0f, icon_col);
draw_line(commands, btn.x - 3.0f, btn.y + 3.0f, btn.x - 5.0f, btn.y + 6.0f, icon_col);
draw_line(commands, btn.x - 5.0f, btn.y + 6.0f, btn.x, btn.y + 3.0f, icon_col);
break;
}
case 15: { // SET: Gear / Close
draw_circle(commands, btn.x, btn.y, 4.0f, icon_col, 10);
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
draw_line(commands, btn.x, btn.y - 7.0f, btn.x, btn.y + 7.0f, icon_col);
draw_line(commands, btn.x - 5.0f, btn.y - 5.0f, btn.x + 5.0f, btn.y + 5.0f, icon_col);
draw_line(commands, btn.x - 5.0f, btn.y + 5.0f, btn.x + 5.0f, btn.y - 5.0f, icon_col);
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;
}
}
}
// --- 4. Virtual Joystick ---
// 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_,
@@ -494,26 +416,52 @@ public:
}
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;
}
if (camera_dragged_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraEndDrag();
#endif
}
camera_finger_id_ = -1;
camera_dragged_ = false;
pinch_finger1_ = -1;
pinch_finger2_ = -1;
pinch_last_dist_ = 0.0f;
}
void init_buttons() {
buttons_.clear();
// Combat Action Wheel (Lower-Right)
// 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, 22.0f, "POT", 0x90A03030, 0xD0FF5050});
buttons_.push_back({6, 0x2c, 0, 0, 22.0f, "PICK", 0x80208080, 0xD040B0B0});
// Top-Right Drawer Menu Toggle
buttons_.push_back({99, 0, 0, 0, 20.0f, "MENU", 0x90283848, 0xD0FFB040});
// Drawer Menu Buttons
buttons_.push_back({10, 0x17, 0, 0, 18.0f, "BAG", 0x90D09020, 0xD0FFB040}); // DIK_I
buttons_.push_back({11, 0x2e, 0, 0, 18.0f, "CHAR", 0x903060B0, 0xD05080FF}); // DIK_C
buttons_.push_back({12, 0x2f, 0, 0, 18.0f, "SKILL", 0x90803090, 0xD0D050D0}); // DIK_V
buttons_.push_back({13, 0x31, 0, 0, 18.0f, "QUEST", 0x90308050, 0xD050D070}); // DIK_N
buttons_.push_back({14, 0x32, 0, 0, 18.0f, "COMM", 0x90905020, 0xD0E07030}); // DIK_M
buttons_.push_back({15, 0x01, 0, 0, 18.0f, "SET", 0x90506070, 0xD08090A0}); // DIK_ESCAPE
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});
}
void layout_buttons() {
@@ -522,70 +470,40 @@ private:
for (auto& btn : buttons_) {
switch (btn.id) {
case 1:
btn.x = W - 85.0f;
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 3:
btn.x = W - 145.0f;
case 6:
btn.x = W - 125.0f;
btn.y = H - 155.0f;
btn.radius = std::min(28.0f, H * 0.08f);
break;
case 4:
btn.x = W - 85.0f;
btn.y = H - 175.0f;
case 7:
btn.x = W - 75.0f;
btn.y = H - 225.0f;
btn.radius = std::min(28.0f, H * 0.08f);
break;
case 5:
btn.x = W - 225.0f;
btn.y = H - 75.0f;
btn.radius = std::min(24.0f, H * 0.065f);
break;
case 6:
btn.x = W - 85.0f;
btn.y = H - 235.0f;
btn.radius = std::min(24.0f, H * 0.065f);
break;
case 99: // MENU toggle button (to the left of MiniMap)
btn.x = W - 170.0f;
btn.y = 35.0f;
btn.radius = 20.0f;
break;
case 10: // BAG
btn.x = W - 220.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
case 11: // CHAR
btn.x = W - 265.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
case 12: // SKILL
btn.x = W - 310.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
case 13: // QUEST
btn.x = W - 355.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
case 14: // COMM
btn.x = W - 400.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
case 15: // SET
btn.x = W - 445.0f;
btn.y = 35.0f;
btn.radius = 18.0f;
break;
}
}
}
@@ -593,8 +511,6 @@ private:
int find_button(float x, float y) {
for (size_t i = 0; i < buttons_.size(); ++i) {
const auto& btn = buttons_[i];
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
continue;
const float dist = std::hypot(x - btn.x, y - btn.y);
if (dist <= btn.radius * 1.25f) {
return int(i);
@@ -608,6 +524,11 @@ private:
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);
}
@@ -703,7 +624,7 @@ private:
int screen_w_ = 1280;
int screen_h_ = 720;
bool drawer_open_ = false;
bool controls_were_visible_ = false;
int64_t ui_touch_finger_id_ = -1;
bool joystick_active_ = false;