Compare commits

...
2 Commits
Author SHA1 Message Date
Warren H 89bc1b6ab8 Android build v1.1.8 2026-07-02 14:11:18 -04:00
Warren H 5b8c32dc41 Android build v1.1.7 2026-06-30 14:23:45 -04:00
9 changed files with 545 additions and 41 deletions
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal" applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 85 versionCode 87
versionName "1.1.6" versionName "1.1.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -7,19 +7,27 @@ import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import java.io.File; import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public abstract class ControllerBridgeActivity extends BridgeActivity { public abstract class ControllerBridgeActivity extends BridgeActivity {
public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL"; public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL";
private static final String PREFS_NAME = "com.warren.iwanttoheal.performance"; private static final String PREFS_NAME = "com.warren.iwanttoheal.performance";
private static final String CACHE_CLEARED_VERSION_KEY = "webview_cache_cleared_version"; private static final String CACHE_CLEARED_VERSION_KEY = "webview_cache_cleared_version";
private static final float AXIS_DEAD_ZONE = 0.45f;
private static final long DPAD_THROTTLE_MS = 55; private static final long DPAD_THROTTLE_MS = 55;
private final long[] lastDpadDispatchAt = new long[16]; private final long[] lastDpadDispatchAt = new long[16];
private final Set<String> activeMotionTokens = new HashSet<>();
private final Map<String, Long> lastMotionDispatchAt = new HashMap<>();
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -140,6 +148,87 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
boolean repeat = event.getRepeatCount() > 0; boolean repeat = event.getRepeatCount() > 0;
if (isDpadToken(token) && shouldThrottleDpad(token)) return true; if (isDpadToken(token) && shouldThrottleDpad(token)) return true;
dispatchNativeControllerToken(token, repeat);
}
return true;
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if (
bridge == null
|| event.getActionMasked() != MotionEvent.ACTION_MOVE
|| !isControllerMotionEvent(event)
) {
return super.dispatchGenericMotionEvent(event);
}
Set<String> currentTokens = new HashSet<>();
addAxisTokens(
currentTokens,
event.getAxisValue(MotionEvent.AXIS_HAT_X),
"Button14",
"Button15"
);
addAxisTokens(
currentTokens,
event.getAxisValue(MotionEvent.AXIS_HAT_Y),
"Button12",
"Button13"
);
addAxisTokens(
currentTokens,
event.getAxisValue(MotionEvent.AXIS_X),
"Axis0-",
"Axis0+"
);
addAxisTokens(
currentTokens,
event.getAxisValue(MotionEvent.AXIS_Y),
"Axis1-",
"Axis1+"
);
boolean hadMotionTokens = !activeMotionTokens.isEmpty();
long now = SystemClock.uptimeMillis();
for (String token : currentTokens) {
boolean repeat = activeMotionTokens.contains(token);
long lastDispatchAt = lastMotionDispatchAt.containsKey(token)
? lastMotionDispatchAt.get(token)
: 0L;
if (!repeat || now - lastDispatchAt >= DPAD_THROTTLE_MS) {
dispatchNativeControllerToken(token, repeat);
lastMotionDispatchAt.put(token, now);
}
}
activeMotionTokens.clear();
activeMotionTokens.addAll(currentTokens);
if (currentTokens.isEmpty() && !hadMotionTokens) {
return super.dispatchGenericMotionEvent(event);
}
return true;
}
private boolean isControllerMotionEvent(MotionEvent event) {
int source = event.getSource();
return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|| (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD
|| (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
private void addAxisTokens(
Set<String> tokens,
float value,
String negativeToken,
String positiveToken
) {
if (value <= -AXIS_DEAD_ZONE) tokens.add(negativeToken);
if (value >= AXIS_DEAD_ZONE) tokens.add(positiveToken);
}
private void dispatchNativeControllerToken(String token, boolean repeat) {
if (bridge == null || bridge.getWebView() == null) return;
String script = String script =
"window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller',"
+ "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));";
@@ -150,8 +239,6 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
} }
); );
} }
return true;
}
private boolean shouldThrottleDpad(String token) { private boolean shouldThrottleDpad(String token) {
int buttonIndex = Integer.parseInt(token.substring("Button".length())); int buttonIndex = Integer.parseInt(token.substring("Button".length()));
@@ -171,10 +258,13 @@ public abstract class ControllerBridgeActivity extends BridgeActivity {
private String controllerToken(int keyCode) { private String controllerToken(int keyCode) {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER: case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_NUMPAD_ENTER:
return "Button0"; return "Button0";
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_ESCAPE:
return "Button1"; return "Button1";
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
return "Button2"; return "Button2";
+409
View File
@@ -1433,6 +1433,14 @@ textarea:focus-visible,
display: none; display: none;
} }
.game-shell.settings-shell {
padding: 6px 0;
}
.game-shell.settings-shell > .app-header {
display: none;
}
.auth-shell { .auth-shell {
align-items: center; align-items: center;
display: flex; display: flex;
@@ -1988,6 +1996,14 @@ h2 {
outline-color: var(--gold); outline-color: var(--gold);
} }
.settings-nav {
align-items: stretch;
display: grid;
gap: 8px;
grid-template-columns: minmax(92px, auto) minmax(0, 1fr);
margin-top: 0;
}
.settings-tabs, .settings-tabs,
.talent-page-tabs { .talent-page-tabs {
display: grid; display: grid;
@@ -1996,6 +2012,10 @@ h2 {
margin-top: 14px; margin-top: 14px;
} }
.settings-tabs {
margin-top: 0;
}
.settings-tabs button, .settings-tabs button,
.talent-page-tabs button { .talent-page-tabs button {
background: #15161c; background: #15161c;
@@ -2025,9 +2045,15 @@ h2 {
height: calc(100dvh - 92px); height: calc(100dvh - 92px);
} }
.settings-shell .settings-screen {
height: calc(100dvh - 12px);
margin-top: 0;
}
.settings-tab-panel { .settings-tab-panel {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: auto;
} }
.settings-bindings-panel { .settings-bindings-panel {
@@ -2045,6 +2071,7 @@ h2 {
.settings-bindings-panel .binding-list { .settings-bindings-panel .binding-list {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow-y: auto;
} }
.equipment-screen .gear-summary, .equipment-screen .gear-summary,
@@ -6950,6 +6977,95 @@ h2 {
font-size: 15px; font-size: 15px;
} }
.auth-shell {
height: 100dvh;
min-height: 0;
overflow: hidden;
padding: 10px;
}
.auth-panel {
gap: 12px;
max-height: calc(100dvh - 20px);
}
.auth-brand,
.auth-card {
padding: 14px;
}
.auth-brand {
min-height: 0;
}
.auth-brand h1 {
font-size: 28px;
line-height: 1.15;
margin: 6px 0 10px;
}
.auth-brand > p:last-child {
font-size: 17px;
line-height: 1.15;
}
.auth-tabs {
gap: 5px;
}
.auth-tabs button {
min-height: 34px;
}
.auth-card form {
gap: 8px;
margin-top: 10px;
}
.auth-card label {
gap: 5px;
}
.auth-card input {
font-size: 16px;
padding: 7px 9px;
}
.auth-card .primary-button {
min-height: 34px;
padding: 8px 12px;
}
.auth-message {
font-size: 13px;
line-height: 1.1;
margin-top: 8px;
min-height: 24px;
}
.offline-divider {
gap: 8px;
margin: 8px 0;
}
.offline-entry {
gap: 7px;
padding: 10px;
}
.offline-entry p:not(.eyebrow) {
display: none;
}
.offline-entry h2 {
font-size: 9px;
}
.offline-resume-button,
.offline-new-button {
min-height: 32px;
}
.screen-heading { .screen-heading {
padding-bottom: 10px; padding-bottom: 10px;
} }
@@ -6962,6 +7078,178 @@ h2 {
font-size: 16px; font-size: 16px;
} }
.settings-screen {
padding: 10px;
}
.settings-shell .settings-screen {
height: calc(100dvh - 12px);
}
.settings-nav {
gap: 6px;
grid-template-columns: minmax(76px, auto) minmax(0, 1fr);
}
.settings-tabs {
gap: 6px;
}
.settings-tabs button,
.settings-back-button {
font-size: 7px;
min-height: 32px;
padding: 5px 8px;
}
.settings-tab-panel {
scrollbar-width: thin;
}
.dual-screen-settings,
.controller-preferences {
gap: 10px;
margin: 8px 0 0;
padding: 10px;
}
.dual-screen-settings.settings-tab-panel,
.controller-preferences.settings-tab-panel {
flex: 0 0 auto;
}
.dual-screen-settings {
align-items: start;
grid-template-columns: minmax(0, 1fr) minmax(214px, auto);
}
.dual-screen-settings h2,
.settings-heading h2 {
font-size: 12px;
}
.dual-screen-settings p:not(.eyebrow),
.controller-preferences p:not(.eyebrow) {
font-size: 14px;
line-height: 1.15;
margin-top: 4px;
}
.dual-screen-settings > small {
font-size: 13px;
}
.dual-screen-actions {
align-content: start;
align-self: start;
gap: 7px;
min-width: 214px;
}
.dual-screen-actions button,
.controller-preferences button {
min-height: 32px;
padding: 5px 8px;
}
.controller-preferences {
align-items: stretch;
align-content: start;
grid-template-columns: minmax(0, 1fr) repeat(2, minmax(158px, auto));
}
.controller-preferences h3 {
color: var(--ink);
font-family: var(--pixel-font);
font-size: 10px;
line-height: 1.35;
margin: 0;
}
.controller-icon-options {
gap: 6px;
grid-template-columns: minmax(112px, 1fr) repeat(3, minmax(104px, auto));
padding: 6px;
}
.controller-icon-options > span {
font-size: 7px;
}
.controller-icon-options button {
font-size: 6px;
gap: 5px;
}
.controller-style-preview .controller-face-icon {
height: 18px;
min-width: 18px;
}
.controller-style-preview .controller-face-playstation {
font-size: 19px;
}
.settings-bindings-panel .settings-heading {
padding: 4px 0 5px;
}
.settings-heading > p {
font-size: 13px;
}
.settings-heading .eyebrow {
display: none;
}
.binding-tabs {
gap: 7px;
margin: 6px 0;
}
.binding-tabs button {
min-height: 28px;
}
.binding-list {
gap: 4px;
padding-right: 4px;
}
.binding-list button {
min-height: 24px;
padding: 3px 8px;
}
.binding-list button > span {
font-size: 12px;
}
.binding-list kbd {
font-size: 6px;
min-height: 18px;
}
.binding-list .controller-face-icon {
font-size: 10px;
height: 17px;
min-width: 17px;
}
.binding-list .controller-face-playstation {
font-size: 18px;
}
.settings-footer {
font-size: 13px;
margin-top: 6px;
padding-top: 6px;
}
.settings-footer .text-button {
padding: 6px 9px;
}
.dungeon-card { .dungeon-card {
gap: 12px; gap: 12px;
grid-template-columns: 72px 1fr; grid-template-columns: 72px 1fr;
@@ -7141,6 +7429,127 @@ h2 {
.ability-library { .ability-library {
max-height: 190px; max-height: 190px;
} }
.enemy-card {
gap: 10px;
margin-top: 8px;
padding: 8px;
}
.enemy-portrait {
flex-basis: 48px;
font-size: 17px;
height: 48px;
}
.enemy-info p {
font-size: 14px;
line-height: 1;
margin-top: 4px;
max-height: 28px;
overflow: hidden;
}
.bar-label {
font-size: 15px;
}
.combat-layout {
gap: 8px;
grid-template-columns: minmax(0, 1.6fr) minmax(248px, 0.85fr);
margin-top: 8px;
min-height: 0;
}
.party-panel,
.combat-log {
min-height: 0;
padding: 8px;
}
.party-panel-top {
margin-bottom: 6px;
}
.party-grid {
gap: 7px;
margin-top: 8px;
}
.party-member {
min-height: 72px;
padding: 7px;
}
.member-header {
gap: 5px;
}
.member-header small {
font-size: 12px;
}
.member-health {
margin-top: 5px;
}
.member-effects {
gap: 3px;
margin-top: 4px;
min-height: 14px;
}
.member-effects span {
font-size: 11px;
padding: 0 4px;
}
.combat-side-rail .panel-heading .eyebrow {
display: none;
}
.combat-side-rail .panel-heading h2 {
font-size: 10px;
}
.vertical-spell-bar,
.vertical-spell-bar.six-slots {
gap: 6px;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: 8px;
overflow: hidden;
padding-right: 0;
}
.vertical-spell-bar .spell {
min-height: 54px;
padding: 5px 5px 5px 36px;
}
.vertical-spell-bar .spell-icon {
font-size: 10px;
height: 22px;
left: 6px;
width: 22px;
}
.vertical-spell-bar .spell strong {
font-size: 11px;
line-height: 1.1;
}
.vertical-spell-bar .spell small {
font-size: 10px;
line-height: 1;
}
.vertical-spell-bar .spell kbd {
font-size: 8px;
min-height: 16px;
padding: 1px 4px;
right: 3px;
top: 3px;
}
} }
@media (max-width: 800px) { @media (max-width: 800px) {
+1 -1
View File
@@ -456,7 +456,7 @@ function App() {
const canShowCloudSync = account.id !== -1 && cloudSync.available const canShowCloudSync = account.id !== -1 && cloudSync.available
return ( return (
<main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''}`}> <main className={`game-shell ${screen === 'dungeons' || screen === 'raids' ? 'dungeon-shell' : ''} ${screen === 'customize' ? 'workshop-shell' : ''} ${screen === 'settings' ? 'settings-shell' : ''}`}>
<header className="topbar app-header"> <header className="topbar app-header">
<button <button
className="brand-button" className="brand-button"
+3 -8
View File
@@ -91,14 +91,8 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
return ( return (
<section className="content-screen settings-screen"> <section className="content-screen settings-screen">
<div className="screen-heading"> <div className="settings-nav">
<div> <button className="back-button settings-back-button" onClick={onBack} type="button">Back</button>
<p className="eyebrow">Game Options</p>
<h1>Settings</h1>
</div>
<button className="back-button" onClick={onBack} type="button">Back</button>
</div>
<nav className="settings-tabs" role="tablist" aria-label="Settings sections"> <nav className="settings-tabs" role="tablist" aria-label="Settings sections">
{([ {([
{ key: 'display', label: 'Display' }, { key: 'display', label: 'Display' },
@@ -117,6 +111,7 @@ export function SettingsScreen({ onBack }: { onBack: () => void }) {
</button> </button>
))} ))}
</nav> </nav>
</div>
{settingsTab === 'display' && ( {settingsTab === 'display' && (
<section className="dual-screen-settings settings-tab-panel"> <section className="dual-screen-settings settings-tab-panel">
+12 -3
View File
@@ -150,6 +150,16 @@ function formatDualTime(seconds: number) {
return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}` return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, '0')}`
} }
function shouldRelayBottomAction(
action: InputAction,
state: DualScreenCombatState | null,
) {
if (!state) return false
if (action === 'confirm' || action === 'toggleTouchLock') return false
if (state.status === 'playing') return true
return action === 'pause' || action === 'back'
}
export function DualScreenProvider({ children }: { children: ReactNode }) { export function DualScreenProvider({ children }: { children: ReactNode }) {
const [enabled, setEnabledState] = useState( const [enabled, setEnabledState] = useState(
() => localStorage.getItem(STORAGE_KEY) === 'true', () => localStorage.getItem(STORAGE_KEY) === 'true',
@@ -404,8 +414,7 @@ export function DualScreenBottomDisplay() {
} }
useGameAction((action) => { useGameAction((action) => {
if (!state || state.opponentParty) return if (!shouldRelayBottomAction(action, state)) return
if (action === 'confirm' || action === 'back' || action === 'toggleTouchLock') return
sendAction(action) sendAction(action)
}) })
@@ -458,7 +467,7 @@ export function DualScreenBottomDisplay() {
return ( return (
<main <main
className={`dual-bottom-display ${state.opponentParty ? 'pvp-opponent-bottom-display' : ''}`} className={`dual-bottom-display ${state.opponentParty ? 'pvp-opponent-bottom-display' : ''}`}
data-combat-active={!state.opponentParty && state.status === 'playing' && !state.paused ? 'true' : 'false'} data-combat-active={state.status === 'playing' && !state.paused ? 'true' : 'false'}
> >
<header className="dual-controls-header"> <header className="dual-controls-header">
<div> <div>
+2 -1
View File
@@ -131,7 +131,7 @@ const PREFERENCES_STORAGE_KEY = 'ashen-halls-input-preferences-v1'
const GAME_ACTION_EVENT = 'ashen-halls-game-action' const GAME_ACTION_EVENT = 'ashen-halls-game-action'
const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller' const NATIVE_CONTROLLER_EVENT = 'ashen-halls-native-controller'
const FOCUSABLE_SELECTOR = 'button:not(:disabled):not([data-controller-nav="skip"]), input:not(:disabled):not([data-controller-nav="skip"]), select:not(:disabled):not([data-controller-nav="skip"]), textarea:not(:disabled):not([data-controller-nav="skip"]), [tabindex]:not([tabindex="-1"]):not([data-controller-nav="skip"])' const FOCUSABLE_SELECTOR = 'button:not(:disabled):not([data-controller-nav="skip"]), input:not(:disabled):not([data-controller-nav="skip"]), select:not(:disabled):not([data-controller-nav="skip"]), textarea:not(:disabled):not([data-controller-nav="skip"]), [tabindex]:not([tabindex="-1"]):not([data-controller-nav="skip"])'
const MAIN_CONTENT_SELECTOR = '.menu-screen, .content-screen, .dungeon-run-screen' const MAIN_CONTENT_SELECTOR = '.auth-shell, .menu-screen, .content-screen, .dungeon-run-screen, .dual-bottom-display'
const HEADER_CONTENT_SELECTOR = '.app-header' const HEADER_CONTENT_SELECTOR = '.app-header'
const GAMEPAD_COMBAT_POLL_MS = 1000 / 60 const GAMEPAD_COMBAT_POLL_MS = 1000 / 60
const GAMEPAD_MENU_POLL_MS = 1000 / 30 const GAMEPAD_MENU_POLL_MS = 1000 / 30
@@ -309,6 +309,7 @@ function rememberFocusableControl(element: HTMLElement) {
function focusControl(element: HTMLElement) { function focusControl(element: HTMLElement) {
rememberFocusableControl(element) rememberFocusableControl(element)
element.focus({ preventScroll: true }) element.focus({ preventScroll: true })
element.scrollIntoView({ block: 'nearest', inline: 'nearest' })
} }
function currentFocusableControl(candidates = focusableElements()) { function currentFocusableControl(candidates = focusableElements()) {