Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 827eb0fc1d The keyboard gets the menu pointer's clicks; its BACK and ENTER hints click
The menus' mouse handling took the pointer's clicks each frame before the
keyboard asked for them, so the keyboard never saw one: with the keyboard
up the pointer is now the keyboard's alone. Clicking the hints under it
(B = BACK, A = ENTER, in its picture) cancels or finishes as Done does.
The pointer counts as moved past 3 pixels, not a hand's tremor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:47:51 +02:00
Stefan MaierandClaude Opus 5.5 a482a5f5bb The on-screen keyboard follows the menu pointer in VR
The key under the controller's pointer is picked and a click types it.
In the menus the triggers no longer reach the game as the gamepad's (they
click the pointer; on the keyboard they also moved its cursor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:42:57 +02:00
Stefan MaierandClaude Opus 5.5 81f321f959 Of overlapping screen keys, the one whose icon is nearest on the left
A key's label is a text box often far wider than its text, reaching over
the next key (Back pressed Delete). Of the keys under the pointer, the one
whose icon begins nearest on its left is taken: each key from its icon to
the next one's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:20 +02:00
Stefan MaierandClaude Opus 5.5 f97ff9219f Menu pointer: narrower keys, the click where the trigger began to move
The screen keys' margin in VR was wide enough to reach the next key (Back
pressed Delete): now 2 pixels to the sides and 4 above and below, with a
label at most 25 pixels off. A click is taken where the controller
pointed as its trigger began to move (past a tenth), not a fixed time
before, which could still be on the key it just left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:27:28 +02:00
Stefan MaierandClaude Opus 5.5 85e79a067a Steadier menu pointer clicks in VR
A click is taken where the controller pointed a few frames before the
trigger went down (pulling it tips the controller), and in VR a screen
key's button takes a label up to 40 pixels off and a margin of 6 about it,
so the keys under the menus (A, B, Back) are hit as readily as items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:23:28 +02:00
Stefan MaierandClaude Opus 5.5 effeda1b7c Point at the menus with a controller in VR
The desktop builds' mouse in the menus (halo_ui_pointer.h) now comes, on
Android in VR, from a controller: where its aim's line meets the menus'
screen fixed in the room is the pointer, the trigger clicks (the hand
pressed last points; a click ticks its rumble), and a ringed dot shows
where it points. The item under it takes the focus and a click presses A,
as with the mouse; the online games and host options panels take it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:56:05 +02:00
Stefan Maier 6299503191 Merge branch 'feature/bots-team' 2026-10-03 11:10:25 +02:00
Stefan MaierandClaude Opus 5.5 c5d86c8d8b Bots spread out about their objective and keep to it
Each bot has a direction of its own (the golden angle on from the last):
far from a shared goal (the enemy's flag, the ball, the hill) it comes by
a way of its own, out to its side of the straight way, straight only for
the last 10 world units; holding a place it takes a spot of its own: on
the hill, about its flag's stand (every other guard ahead of it towards
the enemy's), about a teammate it escorts. With no way to its spot it
takes the point itself. A bot with an objective no longer leaves it in a
fight for a weapon close by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:55:28 +02:00
Stefan MaierandClaude Opus 5.5 1c8e651bb1 Bots play with their team
In a game of teams a bot holds its fire while a teammate stands in the
line of it (and steps aside for a clear shot), takes on first the enemies
near its teammates (people above all), escorts a teammate carrying the
enemy's flag or the ball a few world units off on its own side, and, with
nothing to do, every other bot keeps near the nearest person on its team.
A goal it finds no way to three times running (a person up where only a
teleporter goes, a thing out of reach) is given up for a while, and a
roaming bot then goes somewhere near at random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:08 +02:00
8 changed files with 680 additions and 34 deletions

No files matched your search

+333 -22
View File
@@ -32,6 +32,12 @@ In the objective games a bot plays for the objective, fighting on the way:
- king of the hill: to the hill, and around in it;
- oddball: to the ball (taken as a weapon is), hunting whoever carries it;
with it, away from the enemy, striking those who come near.
In a game of teams a bot plays with its team: it holds its fire while a
teammate is in the line of it, takes on first the enemies near its
teammates (people above all), escorts a teammate who carries the enemy's
flag or the ball, and, with nothing to do, every other bot keeps near the
nearest person on its team.
*/
#include "cseries.h"
@@ -111,6 +117,12 @@ struct bot
real sidestep;
/* whether it walks to a point this tick (not only weaving in a fight) */
boolean walking;
/* the paths to its goal not found in a row, and goals given up for a
while on that account: following a person, a thing to pick up */
short path_failures;
long follow_again_time;
long ignored_item_index;
long ignored_item_time;
real wander_yaw;
short weave_phase;
/* where it is going, and the way there */
@@ -127,6 +139,8 @@ struct bot
/* the objective, as it played it this tick: where it goes, whom it
hunts first, and whether it carries the flag or ball (no firing) */
boolean has_objective;
/* the objective is a place to hold (the hill, its own flag's stand) */
boolean holding;
real_point3d objective;
long objective_unit_index;
boolean carrying;
@@ -232,6 +246,124 @@ static boolean bot_can_see(
from, &vector, unit_index, &collision);
}
/* ---------- the team */
/* whether a player is a person (on a machine of its own, not a bot's) */
static boolean bot_player_is_person(
struct player_datum const *player)
{
return !network_game_server_is_bot_machine(player->network_player_data.machine_index);
}
/* how much nearer an enemy at this point counts for being near the bot's
teammates: up to 10 world units for one by a person, 6 by a bot */
static real bot_threat_to_team(
struct player_datum const *player,
real_point3d const *enemy)
{
real threat = 0.0f;
short index;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d head;
real distance;
real most;
if (!mate || mate == player || mate->team_index != player->team_index)
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
unit_get_camera_position(unit_index, &head);
distance = distance3d(enemy, &head);
most = bot_player_is_person(mate) ? 10.0f : 6.0f;
if (distance < 12.0f)
threat = MAX(threat, most * (1.0f - distance / 12.0f));
}
return threat;
}
/* whether a teammate stands in the line of fire to a point (nearer than
it, within 0.8 world units of the line) */
static boolean bot_teammate_in_line(
struct player_datum const *player,
real_point3d const *eye,
real_point3d const *target)
{
real_vector3d line;
real length;
short index;
if (!game_engine_has_teams())
return FALSE;
vector_from_points3d(eye, target, &line);
length = magnitude3d(&line);
if (length < 0.01f)
return FALSE;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d center;
real_vector3d offset;
real along;
real_point3d nearest;
if (!mate || mate == player || mate->team_index != player->team_index)
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
unit_get_camera_position(unit_index, &center);
center.z -= 0.25f;
vector_from_points3d(eye, &center, &offset);
along = dot_product3d(&offset, &line) / length;
if (along <= 0.0f || along >= length)
continue;
nearest.x = eye->x + line.i * along / length;
nearest.y = eye->y + line.j * along / length;
nearest.z = eye->z + line.k * along / length;
if (distance3d(&nearest, &center) < 0.8f)
return TRUE;
}
return FALSE;
}
/* the nearest person on the bot's team, alive (NONE: none) */
static long bot_nearest_person(
struct player_datum const *player,
real_point3d const *position)
{
long best = NONE;
real best_distance = 40.0f;
short index;
for (index = 0; index < MAXIMUM_TRACKED_PLAYERS; index++)
{
struct player_datum *mate = distributed_player(index);
long unit_index;
real_point3d origin;
real distance;
if (!mate || mate == player || mate->team_index != player->team_index || !bot_player_is_person(mate))
continue;
unit_index = distributed_living_unit(mate);
if (unit_index == NONE)
continue;
object_get_origin(unit_index, &origin);
distance = distance3d(position, &origin);
if (distance < best_distance)
{
best_distance = distance;
best = unit_index;
}
}
return best;
}
/* the nearest enemy unit the bot can see, or NONE */
static long bot_find_target(
struct player_datum const *player,
@@ -256,6 +388,11 @@ static long bot_find_target(
continue;
unit_get_camera_position(unit_index, &head);
distance = distance3d(eye, &head);
if (distance >= bots.sight_range)
continue;
/* (one near a teammate counts as nearer: helped first) */
if (teams)
distance -= bot_threat_to_team(player, &head);
if (distance < best_distance && bot_can_see(player->unit_index, eye, &head))
{
best = unit_index;
@@ -299,6 +436,7 @@ static boolean bot_find_path(
if (goal_surface == NONE)
{
bots.fail_goal++;
bot->path_failures++;
return FALSE;
}
/* (a little wider than a biped: paths that keep off walls and corners) */
@@ -333,9 +471,11 @@ static boolean bot_find_path(
bot->path.valid = FALSE;
bots.paths_failed++;
bots.fail_search++;
bot->path_failures++;
return FALSE;
}
bots.paths_found += bot->path.valid != FALSE;
bot->path_failures = 0;
return bot->path.valid;
}
@@ -776,6 +916,88 @@ static boolean bots_ctf_base(
return FALSE;
}
/* near a teammate who carries something, a few world units off on the bot's
side of it (an escort, not on the carrier's heels) */
static void bot_escort_point(
real_point3d const *position,
real_point3d const *carried,
real_point3d *point)
{
real dx = position->x - carried->x;
real dy = position->y - carried->y;
real length = (real)sqrt(dx * dx + dy * dy);
*point = *carried;
if (length > 0.01f)
{
point->x += dx / length * 2.5f;
point->y += dy / length * 2.5f;
}
}
/* Bots with one place to go spread out: each has a direction of its own
(the golden angle on from the last bot's), a place of its own about a
point to hold, and comes to a point from far off by a way of its own,
wide of the others', going straight to it only for the last stretch. */
static real bot_spread_angle(
long player_index)
{
return (real)DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) * 2.39996f;
}
/* the bot's place about a point (radius: world units) */
static void bot_spot_around(
real_point3d const *center,
long player_index,
real radius,
real_point3d *spot)
{
real angle = bot_spread_angle(player_index);
*spot = *center;
spot->x += (real)cos(angle) * radius;
spot->y += (real)sin(angle) * radius;
}
/* its place about a point, or the point itself when there is no way to the
place (in a wall, past a ledge) */
static void bot_hold_spot(
struct bot const *bot,
real_point3d const *center,
real radius,
real_point3d *spot)
{
if (bot->path_failures >= 3)
*spot = *center;
else
bot_spot_around(center, bot->player_index, radius, spot);
}
/* where it heads for a point from where it is: from far off, out to a side
of the straight way (its own side and width), in from 10 world units */
static void bot_approach(
struct bot const *bot,
real_point3d const *position,
real_point3d const *goal,
real_point3d *point)
{
long index = DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index);
real dx = goal->x - position->x;
real dy = goal->y - position->y;
real distance = (real)sqrt(dx * dx + dy * dy);
real side = (index & 1 ? 1.0f : -1.0f) * (3.0f + (real)((index >> 1) % 3) * 1.5f);
real share;
*point = *goal;
/* (no way to the side: straight) */
if (distance < 10.0f || bot->path_failures >= 2)
return;
share = MIN((distance - 10.0f) / 10.0f, 1.0f);
point->x += -dy / distance * side * share;
point->y += dx / distance * side * share;
}
/* what the objective asks of the bot now (bot->has_objective and the rest) */
static void bot_play_objective_now(
struct bot *bot,
@@ -809,6 +1031,7 @@ static void bot_play_objective_now(
short index;
bot->has_objective = FALSE;
bot->holding = FALSE;
bot->objective_unit_index = NONE;
bot->carrying = FALSE;
if (type == game_engine_king)
@@ -823,13 +1046,12 @@ static void bot_play_objective_now(
dx = center.x - position->x;
dy = center.y - position->y;
bot->has_objective = TRUE;
bot->objective = center;
/* (on the hill: about in it, not on its middle) */
if (dx * dx + dy * dy < 1.5f * 1.5f)
{
bot->objective.x += (real)cos(bot->weave_phase * 0.05f) * 1.0f;
bot->objective.y += (real)sin(bot->weave_phase * 0.05f) * 1.0f;
}
bot->holding = TRUE;
/* (near: a place of its own on the hill; far: a way of its own) */
if (dx * dx + dy * dy < 6.0f * 6.0f)
bot_hold_spot(bot, &center, 1.2f, &bot->objective);
else
bot_approach(bot, position, &center, &bot->objective);
}
return;
}
@@ -876,23 +1098,42 @@ static void bot_play_objective_now(
bot->objective = own->position;
return;
}
/* (every third guards its own) */
/* (every third guards its own: about the stand, or, every other one
of them, ahead of it towards the enemy's) */
if ((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) % 3) == 2 && bots_ctf_base(player->team_index, &base))
{
real_point3d enemy_base;
bot->has_objective = TRUE;
bot->objective = base;
if (distance3d(position, &base) < 4.0f)
bot->holding = TRUE;
if (((DATUM_INDEX_TO_ABSOLUTE_INDEX(player_index) / 3) & 1) &&
bots_ctf_base((player->team_index + 1) % 2, &enemy_base))
{
bot->objective.x += (real)cos(bot->weave_phase * 0.03f) * 3.0f;
bot->objective.y += (real)sin(bot->weave_phase * 0.03f) * 3.0f;
real dx = enemy_base.x - base.x;
real dy = enemy_base.y - base.y;
real length = (real)sqrt(dx * dx + dy * dy);
bot->objective = base;
if (length > 0.01f)
{
bot->objective.x += dx / length * 7.0f;
bot->objective.y += dy / length * 7.0f;
}
}
else
{
bot_hold_spot(bot, &base, 3.0f, &bot->objective);
}
return;
}
/* theirs: to take it, or to cover its carrier */
/* theirs: to take it, or to escort the teammate carrying it */
if (enemy)
{
bot->has_objective = TRUE;
bot->objective = enemy->position;
if (enemy->carrier_player_index != NONE)
bot_hold_spot(bot, &enemy->position, 3.0f, &bot->objective);
else
bot_approach(bot, position, &enemy->position, &bot->objective);
}
return;
}
@@ -911,13 +1152,20 @@ static void bot_play_objective_now(
bot->carrying = TRUE;
return;
}
/* (one a teammate carries: not to be chased) */
/* (one a teammate carries: escorted, not chased) */
if (item->carrier_player_index != NONE && game_engine_has_teams())
{
struct player_datum *carrier = player_try_and_get(item->carrier_player_index);
if (carrier && carrier->team_index == player->team_index)
{
if (!nearest)
{
bot->has_objective = TRUE;
bot_hold_spot(bot, &item->position, 3.0f, &bot->objective);
}
continue;
}
}
distance = distance3d(position, &item->position);
if (distance < nearest_distance)
@@ -929,7 +1177,7 @@ static void bot_play_objective_now(
if (nearest)
{
bot->has_objective = TRUE;
bot->objective = nearest->position;
bot_approach(bot, position, &nearest->position, &bot->objective);
if (nearest->carrier_player_index != NONE)
{
struct player_datum *carrier = player_try_and_get(nearest->carrier_player_index);
@@ -976,6 +1224,7 @@ static void bot_think(
real wanted_yaw;
real wanted_pitch = 0.0f;
boolean firing = FALSE;
boolean following = FALSE;
real moved;
csmemset(action, 0, sizeof(*action));
@@ -1097,13 +1346,13 @@ static void bot_think(
}
bot->weave_phase++;
/* (a good weapon or powerup close by: taken in the fight too) */
if (!bot->carrying && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
if (!bot->carrying && !bot->has_objective && (game_time_get() - bot->item_look_time >= ITEM_LOOK_TICKS / 2 ||
game_time_get() < bot->item_look_time))
{
bot->item_look_time = game_time_get();
bot->item_index = bot_find_item(unit, &position, 10.0f, 4.0f);
}
if (bot->item_index != NONE && !bot->carrying && object_try_and_get(bot->item_index) &&
if (bot->item_index != NONE && !bot->carrying && !bot->has_objective && object_try_and_get(bot->item_index) &&
bot_item_worth(unit, bot->item_index) > 0.0f)
{
real_point3d item_position;
@@ -1153,6 +1402,13 @@ static void bot_think(
firing = !bot->carrying && distance < BOT_FIRE_RANGE &&
fabsf(bot_angle_difference(wanted_yaw, bot->yaw)) < BOT_FIRE_CONE &&
fabsf(wanted_pitch - bot->pitch) < BOT_FIRE_CONE;
/* (a teammate in the line of fire: held, and a step aside for a
clear shot) */
if (firing && bot_teammate_in_line(player, &eye, &head))
{
firing = FALSE;
action->throttle.j = (bot->weave_phase / 30) & 1 ? 1.0f : -1.0f;
}
/* (a melee blow when it is upon it) */
if (distance < 1.0f)
SET_FLAG(action->control_flags, _unit_control_use_equipment_bit, TRUE);
@@ -1201,6 +1457,8 @@ static void bot_think(
long item = bot_find_item(unit, &position, BOT_ITEM_RANGE, 0.0f);
bot->item_look_time = game_time_get();
if (item == bot->ignored_item_index && game_time_get() - bot->ignored_item_time < 600)
item = NONE;
if (item != bot->item_index)
{
bot->item_index = item;
@@ -1215,8 +1473,16 @@ static void bot_think(
}
if (bot->item_index != NONE && !bot->has_objective)
{
/* (taken or gone: on) */
if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
/* (taken or gone, or no way to it: on) */
if (bot->path_failures >= 3)
{
bot->ignored_item_index = bot->item_index;
bot->ignored_item_time = game_time_get();
bot->path_failures = 0;
bot->item_index = NONE;
bot->has_goal = FALSE;
}
else if (!object_try_and_get(bot->item_index) || bot_item_worth(unit, bot->item_index) <= 0.0f)
{
bot->item_index = NONE;
bot->has_goal = FALSE;
@@ -1229,12 +1495,56 @@ static void bot_think(
bot_take_item(bot, player, unit, action);
}
}
/* (in a team, every other bot with nothing to do keeps near the
nearest person on its team) */
following = FALSE;
if (bot->item_index == NONE && !bot->has_objective && game_engine_has_teams() &&
(DATUM_INDEX_TO_ABSOLUTE_INDEX(bot->player_index) & 1) == 0 &&
game_time_get() >= bot->follow_again_time)
{
long person = bot_nearest_person(player, &position);
if (person != NONE)
{
real_point3d origin;
following = TRUE;
object_get_origin(person, &origin);
/* (near: where it is, not on top of them) */
if (distance3d(&position, &origin) > 6.0f)
bot_escort_point(&position, &origin, &bot->goal);
else
bot->goal = position;
bot->has_goal = TRUE;
bot->goal_time = game_time_get();
/* (no way to them, as up where only a teleporter goes: not for
ten seconds) */
if (bot->path_failures >= 3)
{
bot->follow_again_time = game_time_get() + 300;
bot->path_failures = 0;
following = FALSE;
}
}
}
/* hunts, or roams: along its way, looking where it goes */
if (bot->item_index == NONE && !bot->has_objective &&
(!bot->has_goal || game_time_get() - bot->goal_time > 90 ||
if (bot->item_index == NONE && !bot->has_objective && !following &&
(!bot->has_goal || game_time_get() - bot->goal_time > 90 || bot->path_failures >= 3 ||
distance3d(&position, &bot->goal) < BOT_GOAL_RADIUS))
{
boolean unreachable = bot->path_failures >= 3;
bot_choose_goal(bot, player, &position);
/* (no way to the last: somewhere near at random instead) */
if (unreachable)
{
real angle = bot_random() * 2.0f * _pi;
bot->goal = position;
bot->goal.x += (real)cos(angle) * 6.0f;
bot->goal.y += (real)sin(angle) * 6.0f;
}
bot->path_failures = 0;
}
wanted_yaw = bot->wander_yaw;
if (bot_next_point(bot, player->unit_index, &position, &point))
@@ -1363,6 +1673,7 @@ static struct bot *bot_get(
free_bot->player_index = player_index;
free_bot->target_unit_index = NONE;
free_bot->item_index = NONE;
free_bot->ignored_item_index = NONE;
}
return free_bot;
}
+6
View File
@@ -140,6 +140,12 @@ out (halo_vr_scope_pass). */
int halo_vr_scope(short local_player_index, float magnification, float position[3], float forward[3], float up[3],
float *vertical_field_of_view);
int halo_vr_scope_pass(void);
/* The menus' pointer (d3d8_gl.c's halo_ui_pointer_update): where a
controller points on the menus' screen, in their 640x480, whether that
moved since asked, and the trigger's clicks since (nonzero while it points
at the screen); and where it points this frame, to draw it (render.c) */
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks);
int halo_vr_menu_pointer_at(int *x, int *y);
/* Players' names over their heads (hud.c, render.c): whether they are drawn
apart this frame; the slot a name is drawn into (left, top, right, bottom
of the game's 640x480); a name's place (world units, over the head),
+13 -3
View File
@@ -1365,11 +1365,21 @@ HRESULT WINAPI Direct3D_CreateDevice(UINT adapter, D3DDEVTYPE device_type, void
/* ---------- the menus' pointer */
#ifdef HALO_ANDROID
/* the controller pointing at the menus' screen in VR (halo_vr_menu_pointer) */
int halo_ui_pointer_update(int menus_active, struct halo_ui_pointer *pointer)
{
(void)menus_active;
(void)pointer;
return 0;
int x, y, click_x, click_y, moved, clicks;
if (!menus_active || !halo_vr_menu_pointer(&x, &y, &click_x, &click_y, &moved, &clicks))
return 0;
memset(pointer, 0, sizeof(*pointer));
pointer->x = (short)(x - (halo_screen_width() - 640) / 2);
pointer->y = (short)y;
pointer->click_x = (short)(click_x - (halo_screen_width() - 640) / 2);
pointer->click_y = (short)click_y;
pointer->moved = (unsigned char)(moved != 0);
pointer->left_clicks = (unsigned char)(clicks < 255 ? clicks : 255);
return 1;
}
#else
/* a point in the window, as SDL reports it, in the menus' coordinates: the
+133
View File
@@ -1969,6 +1969,133 @@ static int crosshair_pose(struct halo_xr_pose *pose, float *size)
return 1;
}
/* ---------- the menus' pointer
In the menus (the screen fixed in the room), a controller points at the
screen and its trigger clicks, as the desktop builds' mouse does
(halo_ui_pointer.h): where its aim's line meets the screen, in the menus'
640x480. The hand whose trigger was pressed last points; a click gives a
short tick of the hand's rumble. */
static struct
{
int valid;
float x, y;
float reported_x, reported_y;
int hand;
int trigger_down[HALO_XR_HAND_COUNT];
int clicks;
/* a click is taken where it pointed as the trigger began to move
(pulling it through tips the controller) */
int pulling[HALO_XR_HAND_COUNT];
float pull_x[HALO_XR_HAND_COUNT], pull_y[HALO_XR_HAND_COUNT];
float click_x, click_y;
} menu_pointer;
static void menu_pointer_update(void)
{
static const float forward[3] = { 0.0f, 0.0f, -1.0f };
const struct halo_xr_hand *hand;
float direction[3], t, hit_x, hit_y, width, height;
int index;
menu_pointer.valid = 0;
if (!vr.active || vr.last_overlay_mode != HALO_XR_OVERLAY_SCREEN || !vr.frame.focused)
{
menu_pointer.trigger_down[0] = menu_pointer.trigger_down[1] = 0;
menu_pointer.pulling[0] = menu_pointer.pulling[1] = 0;
return;
}
/* (the hand that clicked last points; a click counts as it goes down) */
for (index = 0; index < HALO_XR_HAND_COUNT; index++)
{
int down = vr.frame.hands[index].active && (vr.frame.hands[index].buttons & HALO_XR_BUTTON_TRIGGER);
if (down && !menu_pointer.trigger_down[index])
{
menu_pointer.hand = index;
menu_pointer.clicks++;
}
menu_pointer.trigger_down[index] = down;
}
hand = &vr.frame.hands[menu_pointer.hand];
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
{
hand = &vr.frame.hands[1 - menu_pointer.hand];
if (!hand->active || !(hand->flags & HALO_XR_POSE_VALID))
return;
}
quaternion_rotate(hand->aim.orientation, forward, direction);
if (direction[2] > -1e-3f)
return;
t = (-vr.screen_distance - hand->aim.position[2]) / direction[2];
if (t <= 0.0f)
return;
hit_x = hand->aim.position[0] + direction[0] * t;
hit_y = hand->aim.position[1] + direction[1] * t;
width = vr.screen_width;
height = vr.screen_width * 0.75f;
menu_pointer.x = (hit_x / width + 0.5f) * 640.0f;
menu_pointer.y = (0.5f - hit_y / height) * 480.0f;
menu_pointer.valid = menu_pointer.x >= 0.0f && menu_pointer.x < 640.0f && menu_pointer.y >= 0.0f &&
menu_pointer.y < 480.0f;
/* (the click where the trigger began to move, if it pointed then) */
{
int pulling = vr.frame.hands[menu_pointer.hand].trigger > 0.1f;
if (pulling && !menu_pointer.pulling[menu_pointer.hand] && menu_pointer.valid)
{
menu_pointer.pull_x[menu_pointer.hand] = menu_pointer.x;
menu_pointer.pull_y[menu_pointer.hand] = menu_pointer.y;
}
if (!pulling || !menu_pointer.pulling[menu_pointer.hand])
{
menu_pointer.click_x = menu_pointer.x;
menu_pointer.click_y = menu_pointer.y;
}
else
{
menu_pointer.click_x = menu_pointer.pull_x[menu_pointer.hand];
menu_pointer.click_y = menu_pointer.pull_y[menu_pointer.hand];
}
if (!pulling || menu_pointer.valid)
menu_pointer.pulling[menu_pointer.hand] = pulling;
}
}
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
{
menu_pointer_update();
*click_x = (int)menu_pointer.click_x;
*click_y = (int)menu_pointer.click_y;
*clicks = menu_pointer.clicks;
if (menu_pointer.clicks)
host_xr_haptic(menu_pointer.hand, 0.3f, 0.02f);
menu_pointer.clicks = 0;
if (!menu_pointer.valid)
return 0;
*x = (int)menu_pointer.x;
*y = (int)menu_pointer.y;
/* (moved past a hand's tremor: a few pixels) */
*moved = fabsf(menu_pointer.x - menu_pointer.reported_x) >= 3.0f ||
fabsf(menu_pointer.y - menu_pointer.reported_y) >= 3.0f;
if (*moved)
{
menu_pointer.reported_x = menu_pointer.x;
menu_pointer.reported_y = menu_pointer.y;
}
return 1;
}
int halo_vr_menu_pointer_at(int *x, int *y)
{
if (!menu_pointer.valid)
return 0;
*x = (int)menu_pointer.x;
*y = (int)menu_pointer.y;
return 1;
}
/* ---------- players' names
Names over the heads (hud.c) are drawn into slots of a region of the back
@@ -2273,6 +2400,12 @@ int halo_vr_scope(short local_player_index, float magnification, float position[
return 0;
}
int halo_vr_scope_pass(void) { return 0; }
int halo_vr_menu_pointer(int *x, int *y, int *click_x, int *click_y, int *moved, int *clicks)
{
(void)x; (void)y; (void)click_x; (void)click_y; (void)moved; *clicks = 0;
return 0;
}
int halo_vr_menu_pointer_at(int *x, int *y) { (void)x; (void)y; return 0; }
int halo_vr_names_apart(void) { return 0; }
void halo_vr_name_slot(int slot, short bounds[4]) { (void)slot; (void)bounds; }
int halo_vr_add_name(const float point[3]) { (void)point; return -1; }
+3 -2
View File
@@ -573,9 +573,10 @@ static void vr_gamepad_state(XINPUT_GAMEPAD *pad)
if (left->buttons & HALO_XR_BUTTON_MENU) pad->wButtons |= XINPUT_GAMEPAD_START;
if (left->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_LEFT_THUMB;
if (right->buttons & HALO_XR_BUTTON_THUMBSTICK) pad->wButtons |= XINPUT_GAMEPAD_RIGHT_THUMB;
if ((BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
/* (in the menus the triggers click the pointer: halo_vr_menu_pointer) */
if (!menus && (BYTE)(right->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_RIGHT_TRIGGER] = (BYTE)(right->trigger * 255.0f);
if ((BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
if (!menus && (BYTE)(left->trigger * 255.0f) > pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER])
pad->bAnalogButtons[XINPUT_GAMEPAD_LEFT_TRIGGER] = (BYTE)(left->trigger * 255.0f);
if (menus)
{
+101 -7
View File
@@ -677,6 +677,9 @@ struct widget_instance;
#include "text/text_group.h"
#include "text/unicode.h"
#include "ui_widget.h"
#ifdef HALO_ANDROID
#include "halo_vr.h"
#endif
/* ---------- constants */
@@ -5269,6 +5272,8 @@ struct ui_mouse_target
rectangle2d bounds;
short kind;
short button_index;
/* (a button's: where its icon begins) */
short icon_x0;
};
static struct ui_mouse_target ui_mouse_targets[UI_MOUSE_MAXIMUM_TARGETS];
@@ -5403,6 +5408,7 @@ static void ui_mouse_note_target(
rectangle2d bounds = definition->bounds;
short button_index;
short kind;
short icon_x0;
if (!ui_mouse_noting_targets ||
ui_mouse_target_count >= UI_MOUSE_MAXIMUM_TARGETS ||
@@ -5415,13 +5421,22 @@ static void ui_mouse_note_target(
bounds.y0 += offset.y;
bounds.y1 += offset.y;
button_index = ui_mouse_key_button(widget);
icon_x0 = bounds.x0;
if (button_index != NONE)
{
/* the icon and its label: the text beginning just right of it */
struct widget_instance *sibling;
rectangle2d label_bounds;
short best_distance = 17;
#ifdef HALO_ANDROID
/* (VR: a controller points less steadily than a mouse: the label a
little further off is the button's too) */
short most_distance = halo_vr_active() ? 25 : 17;
#else
short most_distance = 17;
#endif
best_distance = most_distance;
kind = _ui_mouse_target_button;
for (sibling = parent ? parent->child : NULL; sibling; sibling = sibling->next)
{
@@ -5445,12 +5460,23 @@ static void ui_mouse_note_target(
label_bounds = sibling_bounds;
}
}
if (best_distance < 17)
if (best_distance < most_distance)
{
bounds.x1 = MAX(bounds.x1, label_bounds.x1);
bounds.y0 = MIN(bounds.y0, label_bounds.y0);
bounds.y1 = MAX(bounds.y1, label_bounds.y1);
}
#ifdef HALO_ANDROID
/* (and a margin about it: above and below, little to the sides,
where the next key is) */
if (halo_vr_active())
{
bounds.x0 -= 2;
bounds.x1 += 2;
bounds.y0 -= 4;
bounds.y1 += 4;
}
#endif
}
else if (parent && ui_mouse_list_shows_several(parent))
{
@@ -5482,6 +5508,7 @@ static void ui_mouse_note_target(
target->bounds = bounds;
target->kind = kind;
target->button_index = button_index;
target->icon_x0 = icon_x0;
return;
}
@@ -5492,16 +5519,33 @@ static struct ui_mouse_target *ui_mouse_target_at(
short y)
{
long index;
struct ui_mouse_target *button = NULL;
for (index = ui_mouse_target_count - 1; index >= 0; index--)
{
rectangle2d const *bounds = &ui_mouse_targets[index].bounds;
struct ui_mouse_target *target = &ui_mouse_targets[index];
rectangle2d const *bounds = &target->bounds;
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
return &ui_mouse_targets[index];
if (!(x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1))
continue;
/* port: a key's label's text box can be far wider than its text, over
the next key: of keys under the point, the one whose icon is nearest
on its left (each key from its icon to the next one's) */
if (target->kind == _ui_mouse_target_button)
{
if (!button ||
(target->icon_x0 <= x + 2 && (button->icon_x0 > x + 2 || target->icon_x0 > button->icon_x0)))
{
button = target;
}
continue;
}
if (!button)
return target;
break;
}
return NULL;
return button;
}
static boolean ui_mouse_widget_has_focus(
@@ -5692,6 +5736,8 @@ enum
MAXIMUM_SHOWN_PUBLIC_GAMES = 32,
/* the online games panel's rows: "enter a code" and the games */
ONLINE_PANEL_ROWS = 11,
/* (the rows before the games: "enter a code") */
ONLINE_PANEL_FIXED_ROWS = 1,
};
static struct
@@ -5932,6 +5978,48 @@ static boolean ui_join_code_take_event(
return FALSE;
}
/* the pointer over the VR port's panels: the row under it chosen, and a
click on it as A (TRUE: a panel is open and took it) */
static boolean ui_join_code_take_pointer(
struct halo_ui_pointer const *pointer)
{
struct event_record press;
short row;
/* (a click where it was clicked, which may be a moment before) */
short x = pointer->left_clicks ? pointer->click_x : pointer->x;
short y = pointer->left_clicks ? pointer->click_y : pointer->y;
csmemset(&press, 0, sizeof(press));
press.type = _event_type_button;
press.data.button.index = _gamepad_analog_button_a;
press.data.button.value = 1;
if (ui_join_code_globals.panel_open)
{
if (x >= 48 && x < 592 && y >= 128 && y < 128 + ONLINE_PANEL_ROWS * 22)
{
row = (short)(ui_join_code_globals.scroll + (y - 128) / 22);
if (row < ui_join_code_globals.game_count + ONLINE_PANEL_FIXED_ROWS)
{
ui_join_code_globals.selection = row;
if (pointer->left_clicks)
ui_join_code_panel_event(&press);
}
}
return TRUE;
}
if (ui_join_code_globals.host_panel_open)
{
if (x >= 148 && x < 492 && y >= 187 && y < 187 + HOST_PANEL_ROWS * 30)
{
ui_join_code_globals.host_selection = (short)((y - 187) / 30);
if (pointer->left_clicks)
ui_join_code_host_panel_event(&press);
}
return TRUE;
}
return FALSE;
}
/* the keyboard closed: the code typed, looked up */
static void ui_join_code_update(
void)
@@ -6186,8 +6274,9 @@ static void ui_widgets_process_mouse(
struct ui_mouse_target *target;
short controller_index = 0;
if (!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer) ||
virtual_keyboard_active())
/* (the keyboard takes the pointer itself: virtual_keyboard.c) */
if (virtual_keyboard_active() ||
!halo_ui_pointer_update(ui_mouse_menus_active(), &pointer))
{
ui_mouse_press_count = 0;
ui_mouse_hover_pending = FALSE;
@@ -6195,6 +6284,11 @@ static void ui_widgets_process_mouse(
}
else
{
#ifdef HALO_ANDROID
/* (the VR port's panels over the menus take the pointer themselves) */
if (ui_join_code_take_pointer(&pointer))
return;
#endif
if (pointer.moved)
{
ui_mouse_hover_pending = TRUE;
+69
View File
@@ -1166,6 +1166,25 @@ static boolean virtual_keyboard_select(
return TRUE;
}
#ifdef HALO_ANDROID
/* the key drawn at a point of the menus' 640x480, or NONE */
static short virtual_keyboard_key_at(
short x,
short y)
{
short key;
for (key = 0; key < NUMBER_OF_VIRTUAL_KEYS; key++)
{
rectangle2d const *bounds = &keyboard_rect[key];
if (x >= bounds->x0 && x < bounds->x1 && y >= bounds->y0 && y < bounds->y1)
return key;
}
return NONE;
}
#endif
static void virtual_keyboard_process_internal(
void)
{
@@ -1304,6 +1323,56 @@ static void virtual_keyboard_process_internal(
}
}
#ifdef HALO_ANDROID
/* the VR port's: the controller's pointer (halo_ui_pointer.h) picks the
key under it, and its click types it */
{
struct halo_ui_pointer pointer;
if (halo_ui_pointer_update(TRUE, &pointer))
{
short x = pointer.left_clicks ? pointer.click_x : pointer.x;
short y = pointer.left_clicks ? pointer.click_y : pointer.y;
short key = virtual_keyboard_key_at(x, y);
if (key != NONE && (pointer.moved || pointer.left_clicks))
{
short row, column;
boolean found = FALSE;
for (row = 0; row < VIRTUAL_KEYBOARD_ROW_COUNT && !found; row++)
{
for (column = 0; column < VIRTUAL_KEYBOARD_COLUMN_COUNT && !found; column++)
{
if (virtual_keyboard_layout_table[row][column] == key)
{
virtual_keyboard_globals.row = row;
virtual_keyboard_globals.column = column;
found = TRUE;
}
}
}
}
if (pointer.left_clicks && key != NONE)
action = _event_key_select;
/* (the keys' hints under the keyboard, in its picture: B = BACK
cancels, A = ENTER finishes, as Done does) */
if (pointer.left_clicks && key == NONE && y >= 412 && y < 448)
{
if (x >= 380 && x < 480)
{
action = _event_cancel;
}
else if (x >= 488 && x < 600)
{
virtual_keyboard_globals.row = 0;
virtual_keyboard_globals.column = 0;
action = _event_key_select;
}
}
}
}
#endif
if (action != NONE)
{
virtual_keyboard_globals.last_key = virtual_keyboard_layout_table[
+22
View File
@@ -874,6 +874,28 @@ static void render_vr_overlay(
interface_draw_screen();
halo_screen_ui_offset(TRUE);
render_ui_widgets(window->local_player_index, &window->rasterizer_camera.viewport_bounds);
if (menus_active)
{
/* the controller's pointer on the menus' screen: a dot, ringed */
int pointer_x, pointer_y;
if (halo_vr_menu_pointer_at(&pointer_x, &pointer_y))
{
rectangle2d dot;
pointer_x -= (int)(halo_screen_width() - 640) / 2;
dot.x0 = (short)(pointer_x - 5);
dot.x1 = (short)(pointer_x + 5);
dot.y0 = (short)(pointer_y - 5);
dot.y1 = (short)(pointer_y + 5);
draw_quad(&dot, 0xE0102030);
dot.x0 += 2;
dot.x1 -= 2;
dot.y0 += 2;
dot.y1 -= 2;
draw_quad(&dot, 0xFF8FD3FF);
}
}
halo_screen_ui_offset(FALSE);
rasterizer_window_end();