Android build v1.1.39

This commit is contained in:
Warren H
2026-07-09 22:07:35 -04:00
parent 4766a34cd3
commit 90930b71aa
9 changed files with 445 additions and 167 deletions
+230 -125
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import {
loadAuthSession,
loginAccount,
@@ -8,13 +8,18 @@ import {
import { selectOfflineMode, selectOnlineMode } from '../gameRepository'
import {
createDefaultIwt2Save,
loadExistingIwt2Save,
IWT2_LOCAL_SAVE_SLOTS,
loadIwt2OnlineSave,
loadIwt2SaveSlots,
replaceIwt2Save,
selectActiveIwt2SaveSlot,
type Iwt2LocalSaveSlot,
type Iwt2LocalSaveSlots,
type Iwt2Save,
} from '../modes/iwt2/save/iwt2Repository'
type Iwt2Launch = {
localSlot: Iwt2LocalSaveSlot
onlineBackupsAvailable: boolean
save: Iwt2Save
}
@@ -23,6 +28,8 @@ type Props = {
onContinue: (launch: Iwt2Launch) => void
}
const IWT2_CHARACTER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9 -]{1,17}$/
function formatSaveTimestamp(updatedAt: number) {
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
@@ -49,18 +56,26 @@ function SaveMetadata({ save }: { save: Iwt2Save }) {
)
}
function firstEmptySlot(saves: Iwt2LocalSaveSlots): Iwt2LocalSaveSlot {
return IWT2_LOCAL_SAVE_SLOTS.find((slot) => !saves[slot]) ?? 1
}
export function AuthScreen({ onContinue }: Props) {
const [localSave] = useState(loadExistingIwt2Save)
const newSave = useMemo(() => createDefaultIwt2Save(), [])
const [localSaves] = useState(loadIwt2SaveSlots)
const [editingSlot, setEditingSlot] = useState<Iwt2LocalSaveSlot | null>(null)
const [characterName, setCharacterName] = useState('')
const [account, setAccount] = useState<Account | null>(null)
const [onlineSave, setOnlineSave] = useState<Iwt2Save | null>(null)
const [onlineDestinationSlot, setOnlineDestinationSlot] = useState<Iwt2LocalSaveSlot>(
() => firstEmptySlot(localSaves),
)
const [confirmOnlineDestination, setConfirmOnlineDestination] = useState<Iwt2LocalSaveSlot | null>(null)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [checkingSession, setCheckingSession] = useState(true)
const [checkingOnlineSave, setCheckingOnlineSave] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [confirmNewSave, setConfirmNewSave] = useState(false)
const [message, setMessage] = useState('Local play is ready while online saves are checked.')
const [message, setMessage] = useState('Choose a slot or sign in to inspect the online save.')
const [messageIsError, setMessageIsError] = useState(false)
const inspectOnlineSave = useCallback(async (cancelled: () => boolean = () => false) => {
@@ -69,7 +84,7 @@ export function AuthScreen({ onContinue }: Props) {
const result = await loadIwt2OnlineSave()
if (cancelled()) return
setOnlineSave(result.save)
setMessage(result.save ? 'Online save ready to load.' : 'Account has no online IWT2 save yet.')
setMessage(result.save ? 'Online save ready. Choose its local destination slot.' : 'Account has no online IWT2 save yet.')
setMessageIsError(false)
} catch (reason) {
if (cancelled()) return
@@ -94,8 +109,8 @@ export function AuthScreen({ onContinue }: Props) {
if (cancelled) return
setMessage(
reason instanceof Error
? `${reason.message} Local saves remain available.`
: 'Online service unavailable. Local saves remain available.',
? `${reason.message} Local slots remain available.`
: 'Online service unavailable. Local slots remain available.',
)
setMessageIsError(true)
})
@@ -107,22 +122,36 @@ export function AuthScreen({ onContinue }: Props) {
}
}, [inspectOnlineSave])
function continueLocal() {
if (!localSave) return
function continueLocal(slot: Iwt2LocalSaveSlot) {
const save = localSaves[slot]
if (!save) return
selectActiveIwt2SaveSlot(slot)
selectOfflineMode()
onContinue({ save: localSave, onlineBackupsAvailable: false })
onContinue({ localSlot: slot, save, onlineBackupsAvailable: false })
}
function continueNew() {
if (localSave && !confirmNewSave) {
setConfirmNewSave(true)
setMessage('New save replaces current local file. Select again to confirm.')
setMessageIsError(false)
function openCharacterEditor(slot: Iwt2LocalSaveSlot) {
setEditingSlot(slot)
setCharacterName('')
setMessage(localSaves[slot]
? `Creating a new character will replace Slot ${slot}.`
: `Enter a name for the new character in Slot ${slot}.`)
setMessageIsError(false)
}
function createCharacter(event: React.FormEvent, slot: Iwt2LocalSaveSlot) {
event.preventDefault()
const name = characterName.trim()
if (!IWT2_CHARACTER_NAME_PATTERN.test(name)) {
setMessage('Character name must be 218 characters, start with a letter, and use letters, numbers, spaces, or hyphens.')
setMessageIsError(true)
return
}
replaceIwt2Save(newSave)
const save = createDefaultIwt2Save(name)
replaceIwt2Save(save, slot)
selectActiveIwt2SaveSlot(slot)
selectOfflineMode()
onContinue({ save: newSave, onlineBackupsAvailable: false })
onContinue({ localSlot: slot, save, onlineBackupsAvailable: false })
}
async function signIn(event: React.FormEvent) {
@@ -148,8 +177,21 @@ export function AuthScreen({ onContinue }: Props) {
function continueOnline() {
if (!account || !onlineSave) return
const destinationSave = localSaves[onlineDestinationSlot]
if (destinationSave && confirmOnlineDestination !== onlineDestinationSlot) {
setConfirmOnlineDestination(onlineDestinationSlot)
setMessage(`Online save will replace ${destinationSave.character.name} in Slot ${onlineDestinationSlot}. Select again to confirm.`)
setMessageIsError(false)
return
}
replaceIwt2Save(onlineSave, onlineDestinationSlot)
selectActiveIwt2SaveSlot(onlineDestinationSlot)
selectOnlineMode()
onContinue({ save: onlineSave, onlineBackupsAvailable: true })
onContinue({
localSlot: onlineDestinationSlot,
onlineBackupsAvailable: true,
save: onlineSave,
})
}
async function handleDifferentAccount() {
@@ -177,68 +219,112 @@ export function AuthScreen({ onContinue }: Props) {
<header className="save-gateway-heading">
<div>
<p className="eyebrow">I Want To Heal 2</p>
<h1>Choose Save</h1>
<h1>Choose Character</h1>
</div>
<p>Compare last update and character XP before entering the arena.</p>
<p>Three local slots. Each character keeps separate progress, timestamp, and XP.</p>
</header>
<div className="save-option-grid">
<article className="save-option-card local-save-card">
<div className="save-option-heading">
<span aria-hidden="true">L</span>
<div>
<p className="eyebrow">Offline</p>
<h2>Local Save</h2>
</div>
</div>
{localSave ? (
<>
<strong className="save-character-name">{localSave.character.name}</strong>
<SaveMetadata save={localSave} />
</>
) : (
<p className="save-empty-state">No local IWT2 save found.</p>
)}
<button
className="save-continue-button"
disabled={!localSave}
onClick={continueLocal}
type="button"
>
Continue Offline Local
</button>
</article>
{IWT2_LOCAL_SAVE_SLOTS.map((slot) => {
const save = localSaves[slot]
const editing = editingSlot === slot
return (
<article className={`save-option-card local-save-card ${save ? '' : 'empty'}`} key={slot}>
<div className="save-option-heading">
<span aria-hidden="true">{slot}</span>
<div>
<p className="eyebrow">Local Save</p>
<h2>Slot {slot}</h2>
</div>
</div>
<article className="save-option-card new-save-card">
<div className="save-option-heading">
<span aria-hidden="true">N</span>
<div>
<p className="eyebrow">Offline</p>
<h2>New Save</h2>
</div>
</div>
<strong className="save-character-name">{newSave.character.name}</strong>
<SaveMetadata save={newSave} />
<button
className={`save-continue-button ${confirmNewSave ? 'danger' : ''}`}
onClick={continueNew}
type="button"
>
{confirmNewSave ? 'Confirm Replace Local Save' : 'Continue Offline — New'}
</button>
</article>
{editing ? (
<form className="character-create-form" onSubmit={(event) => createCharacter(event, slot)}>
<label>
Character Name
<input
autoFocus
maxLength={18}
minLength={2}
onChange={(event) => setCharacterName(event.target.value)}
pattern="[A-Za-z][A-Za-z0-9 \-]{1,17}"
placeholder="Mira"
required
value={characterName}
/>
</label>
<small>
{save ? `Replaces ${save.character.name} in this slot.` : 'Creates a fresh Level 1 character.'}
</small>
<div className="slot-save-actions">
<button
className={`save-continue-button ${save ? 'danger' : ''}`}
type="submit"
>
{save ? `Replace Slot ${slot}` : `Create in Slot ${slot}`}
</button>
<button
className="save-secondary-button"
onClick={() => setEditingSlot(null)}
type="button"
>
Cancel
</button>
</div>
</form>
) : save ? (
<>
<strong className="save-character-name">{save.character.name}</strong>
<SaveMetadata save={save} />
<div className="slot-save-actions">
<button
aria-label={`Continue ${save.character.name} in Slot ${slot}`}
className="save-continue-button"
onClick={() => continueLocal(slot)}
type="button"
>
Continue Offline
</button>
<button
aria-label={`Create new character in Slot ${slot}`}
className="save-secondary-button"
onClick={() => openCharacterEditor(slot)}
type="button"
>
New Character
</button>
</div>
</>
) : (
<>
<p className="save-empty-state">Empty slot</p>
<button
aria-label={`Create character in Slot ${slot}`}
className="save-continue-button"
onClick={() => openCharacterEditor(slot)}
type="button"
>
Create Character
</button>
</>
)}
</article>
)
})}
</div>
<article className="save-option-card online-save-card">
<div className="save-option-heading">
<span aria-hidden="true">O</span>
<div>
<p className="eyebrow">Account</p>
<h2>Online Save</h2>
</div>
<section className="online-save-panel">
<div className="save-option-heading online-save-heading">
<span aria-hidden="true">O</span>
<div>
<p className="eyebrow">Account</p>
<h2>Online Save</h2>
</div>
</div>
{account ? (
<>
{account ? (
<>
<div className="online-save-summary">
<strong className="save-character-name">
{onlineSave?.character.name ?? account.username}
</strong>
@@ -249,58 +335,77 @@ export function AuthScreen({ onContinue }: Props) {
{onlineBusy ? 'Checking online save...' : 'No online IWT2 save found.'}
</p>
)}
<div className="online-save-actions">
<button
className="save-continue-button"
disabled={onlineBusy || !onlineSave}
onClick={continueOnline}
type="button"
>
Load Online Save
</button>
<button
className="save-secondary-button"
disabled={onlineBusy}
onClick={() => { void handleDifferentAccount() }}
type="button"
>
Use Different Account
</button>
</div>
</>
) : (
<form className="online-save-login" onSubmit={signIn}>
<label>
Username
<input
autoComplete="username"
maxLength={20}
minLength={3}
onChange={(event) => setUsername(event.target.value)}
pattern="[A-Za-z0-9_]+"
required
value={username}
/>
</label>
<label>
Password
<input
autoComplete="current-password"
maxLength={128}
minLength={10}
onChange={(event) => setPassword(event.target.value)}
required
type="password"
value={password}
/>
</label>
<button className="save-continue-button" disabled={onlineBusy} type="submit">
{onlineBusy ? 'Checking Account...' : 'Sign In & Check Save'}
</div>
<label className="online-save-destination">
Copy Into
<select
disabled={onlineBusy || !onlineSave}
onChange={(event) => {
setOnlineDestinationSlot(Number(event.target.value) as Iwt2LocalSaveSlot)
setConfirmOnlineDestination(null)
}}
value={onlineDestinationSlot}
>
{IWT2_LOCAL_SAVE_SLOTS.map((slot) => (
<option key={slot} value={slot}>
Slot {slot} {localSaves[slot]?.character.name ?? 'Empty'}
</option>
))}
</select>
</label>
<div className="online-save-actions">
<button
className={`save-continue-button ${confirmOnlineDestination === onlineDestinationSlot ? 'danger' : ''}`}
disabled={onlineBusy || !onlineSave}
onClick={continueOnline}
type="button"
>
{confirmOnlineDestination === onlineDestinationSlot
? `Confirm Replace Slot ${onlineDestinationSlot}`
: `Load Into Slot ${onlineDestinationSlot}`}
</button>
</form>
)}
</article>
</div>
<button
className="save-secondary-button"
disabled={onlineBusy}
onClick={() => { void handleDifferentAccount() }}
type="button"
>
Switch Account
</button>
</div>
</>
) : (
<form className="online-save-login" onSubmit={signIn}>
<label>
Username
<input
autoComplete="username"
maxLength={20}
minLength={3}
onChange={(event) => setUsername(event.target.value)}
pattern="[A-Za-z0-9_]+"
required
value={username}
/>
</label>
<label>
Password
<input
autoComplete="current-password"
maxLength={128}
minLength={10}
onChange={(event) => setPassword(event.target.value)}
required
type="password"
value={password}
/>
</label>
<button className="save-continue-button" disabled={onlineBusy} type="submit">
{onlineBusy ? 'Checking Account...' : 'Sign In & Check Save'}
</button>
</form>
)}
</section>
<p
aria-live="polite"