Compare commits

..
1 Commits
Author SHA1 Message Date
Warren H 90930b71aa Android build v1.1.39 2026-07-09 22:07:35 -04:00
9 changed files with 445 additions and 167 deletions
Binary file not shown.
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "com.warren.iwanttoheal"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 119
versionName "1.1.38"
versionCode 120
versionName "1.1.39"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 50 KiB

+151 -8
View File
@@ -1888,7 +1888,7 @@
box-shadow: 8px 8px 0 #050609;
display: grid;
gap: 12px;
grid-template-rows: auto minmax(0, 1fr) auto;
grid-template-rows: auto minmax(0, 1fr) auto auto;
margin: auto;
max-width: 1080px;
min-height: min(512px, calc(100dvh - 28px));
@@ -1935,10 +1935,10 @@
border: 2px solid #090a0d;
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
min-width: 0;
outline: 2px solid #3a4250;
padding: 15px;
padding: 12px;
}
.local-save-card {
@@ -1972,7 +1972,7 @@
outline: 1px solid #4c5667;
}
.online-save-card .save-option-heading > span {
.online-save-panel .save-option-heading > span {
color: #80bdf2;
}
@@ -2031,7 +2031,7 @@
font-size: 14px;
line-height: 1.25;
margin: 0;
min-height: 72px;
min-height: 58px;
padding: 12px;
}
@@ -2047,6 +2047,99 @@
width: 100%;
}
.slot-save-actions {
display: grid;
gap: 7px;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: auto;
}
.slot-save-actions .save-continue-button {
margin-top: 0;
}
.character-create-form {
display: flex;
flex: 1;
flex-direction: column;
gap: 9px;
min-height: 0;
}
.character-create-form label,
.online-save-destination {
color: var(--muted);
display: grid;
font-family: var(--pixel-font);
font-size: 7px;
gap: 5px;
text-transform: uppercase;
}
.character-create-form input,
.online-save-destination select {
background: #0e1016;
border: 2px solid #090a0d;
color: var(--ink);
font: 16px var(--body-font);
min-height: 38px;
outline: 2px solid #3e3d47;
padding: 7px 9px;
width: 100%;
}
.character-create-form > small {
color: var(--muted);
font-size: 12px;
line-height: 1.2;
}
.online-save-panel {
align-items: center;
background: #131821;
border: 2px solid #090a0d;
border-top-color: #5799db;
display: grid;
gap: 12px;
grid-template-columns: 130px minmax(260px, 1fr) 160px minmax(170px, 210px);
min-width: 0;
outline: 2px solid #3a4250;
padding: 10px 12px;
}
.online-save-heading {
min-width: 0;
}
.online-save-summary {
align-items: center;
display: grid;
gap: 8px;
grid-template-columns: minmax(75px, 0.7fr) minmax(0, 1.8fr);
min-width: 0;
}
.online-save-summary .save-character-name {
font-size: 16px;
}
.online-save-summary .save-metadata {
gap: 5px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.online-save-summary .save-metadata > div {
padding: 5px 7px;
}
.online-save-summary .save-metadata dd {
font-size: 11px;
}
.online-save-summary .save-empty-state {
min-height: 48px;
}
.save-continue-button {
background: var(--gold);
color: #19150e;
@@ -2076,7 +2169,9 @@
.online-save-login {
display: grid;
gap: 9px;
margin-top: auto;
grid-column: 2 / -1;
grid-template-columns: repeat(2, minmax(0, 1fr)) minmax(180px, 0.75fr);
margin: 0;
}
.online-save-login label {
@@ -2123,6 +2218,11 @@
}
@media (max-width: 800px) {
.auth-shell.save-gateway-shell {
align-items: flex-start;
overflow: auto;
}
.save-gateway-panel {
min-height: auto;
}
@@ -2140,6 +2240,20 @@
.save-option-grid {
grid-template-columns: 1fr;
}
.online-save-panel {
align-items: stretch;
grid-template-columns: 1fr;
}
.online-save-login {
grid-column: auto;
grid-template-columns: 1fr;
}
.online-save-summary {
grid-template-columns: 1fr;
}
}
@media (max-width: 1000px) and (max-height: 620px) {
@@ -2171,8 +2285,8 @@
}
.save-option-card {
gap: 8px;
padding: 10px;
gap: 6px;
padding: 8px;
}
.save-option-heading > span {
@@ -2206,6 +2320,35 @@
padding: 6px 8px;
}
.slot-save-actions {
gap: 5px;
}
.character-create-form {
gap: 6px;
}
.character-create-form input,
.online-save-destination select {
font-size: 14px;
min-height: 32px;
padding: 5px 7px;
}
.character-create-form > small {
font-size: 10px;
}
.online-save-panel {
gap: 8px;
grid-template-columns: 115px minmax(230px, 1fr) 145px minmax(160px, 190px);
padding: 7px 9px;
}
.online-save-summary {
gap: 5px;
}
.online-save-login {
gap: 6px;
}
+3 -1
View File
@@ -1,9 +1,10 @@
import { useState } from 'react'
import { AuthScreen } from './components/AuthScreen'
import { IWantToHeal2App } from './modes/iwt2/IWantToHeal2App'
import type { Iwt2Save } from './modes/iwt2/save/iwt2Repository'
import type { Iwt2LocalSaveSlot, Iwt2Save } from './modes/iwt2/save/iwt2Repository'
type Iwt2Launch = {
localSlot: Iwt2LocalSaveSlot
onlineBackupsAvailable: boolean
save: Iwt2Save
}
@@ -15,6 +16,7 @@ function App() {
return (
<IWantToHeal2App
initialSave={launch.save}
localSlot={launch.localSlot}
onlineBackupsAvailable={launch.onlineBackupsAvailable}
onExitToSaveSelect={() => setLaunch(null)}
/>
+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"
+7 -5
View File
@@ -22,7 +22,7 @@ import {
Iwt2RoguelikeUpgradeScreen,
Iwt2SettingsScreen,
} from './screens/Iwt2ShellScreens'
import { writeIwt2Save, type Iwt2Save } from './save/iwt2Repository'
import { writeIwt2Save, type Iwt2LocalSaveSlot, type Iwt2Save } from './save/iwt2Repository'
import { IWT2_BOSS_METADATA, type Iwt2BossId } from './content/bosses'
import { iwt2BossCoinRewardFor } from './content/bossRewards'
import {
@@ -136,8 +136,8 @@ const MENU_ITEMS: Array<{
},
{
screen: 'cloud-save',
title: 'Backup Slot',
description: 'Choose local, online, or fresh IWT2 progress.',
title: 'Cloud Backup',
description: 'Compare the active local slot with your online backup.',
glyph: 'C',
},
{
@@ -150,10 +150,12 @@ const MENU_ITEMS: Array<{
export function IWantToHeal2App({
initialSave,
localSlot,
onlineBackupsAvailable,
onExitToSaveSelect,
}: {
initialSave: Iwt2Save
localSlot: Iwt2LocalSaveSlot
onlineBackupsAvailable: boolean
onExitToSaveSelect: () => void
}) {
@@ -174,8 +176,8 @@ export function IWantToHeal2App({
const cancelPvpQueueRef = useRef<(() => void) | null>(null)
useEffect(() => {
writeIwt2Save(save)
}, [save])
writeIwt2Save(save, localSlot)
}, [localSlot, save])
useEffect(() => () => {
cancelPvpQueueRef.current?.()
+51 -11
View File
@@ -101,14 +101,20 @@ export type Iwt2OnlineSaveResult = {
save: Iwt2Save | null
}
const IWT2_SAVE_KEY = 'i-want-to-heal-2:save:v1'
export const IWT2_LOCAL_SAVE_SLOTS = [1, 2, 3] as const
export type Iwt2LocalSaveSlot = (typeof IWT2_LOCAL_SAVE_SLOTS)[number]
export type Iwt2LocalSaveSlots = Record<Iwt2LocalSaveSlot, Iwt2Save | null>
export function createDefaultIwt2Save(): Iwt2Save {
const IWT2_LEGACY_SAVE_KEY = 'i-want-to-heal-2:save:v1'
const IWT2_SAVE_MIGRATION_KEY = 'i-want-to-heal-2:save-slots:migrated:v1'
const IWT2_ACTIVE_SAVE_SLOT_KEY = 'i-want-to-heal-2:active-save-slot:v1'
export function createDefaultIwt2Save(characterName = 'Healer'): Iwt2Save {
return {
version: 2,
updatedAt: Date.now(),
character: {
name: 'Healer',
name: normalizeCharacterName(characterName),
level: 1,
experience: 0,
healerStyle: 'dawnweaver',
@@ -153,12 +159,38 @@ function normalizeSave(value: unknown): Iwt2Save {
}
}
export function loadIwt2Save(): Iwt2Save {
return loadExistingIwt2Save() ?? createDefaultIwt2Save()
function iwt2SaveSlotKey(slot: Iwt2LocalSaveSlot) {
return `i-want-to-heal-2:save:slot-${slot}:v1`
}
export function loadExistingIwt2Save(): Iwt2Save | null {
const serialized = window.localStorage.getItem(IWT2_SAVE_KEY)
function migrateLegacyIwt2Save() {
if (window.localStorage.getItem(IWT2_SAVE_MIGRATION_KEY)) return
const legacySave = window.localStorage.getItem(IWT2_LEGACY_SAVE_KEY)
const firstSlotKey = iwt2SaveSlotKey(1)
if (legacySave && !window.localStorage.getItem(firstSlotKey)) {
window.localStorage.setItem(firstSlotKey, legacySave)
}
window.localStorage.setItem(IWT2_SAVE_MIGRATION_KEY, 'true')
}
export function activeIwt2SaveSlot(): Iwt2LocalSaveSlot {
const stored = Number(window.localStorage.getItem(IWT2_ACTIVE_SAVE_SLOT_KEY))
return IWT2_LOCAL_SAVE_SLOTS.includes(stored as Iwt2LocalSaveSlot)
? stored as Iwt2LocalSaveSlot
: 1
}
export function selectActiveIwt2SaveSlot(slot: Iwt2LocalSaveSlot) {
window.localStorage.setItem(IWT2_ACTIVE_SAVE_SLOT_KEY, String(slot))
}
export function loadIwt2Save(slot = activeIwt2SaveSlot()): Iwt2Save {
return loadExistingIwt2Save(slot) ?? createDefaultIwt2Save()
}
export function loadExistingIwt2Save(slot = activeIwt2SaveSlot()): Iwt2Save | null {
migrateLegacyIwt2Save()
const serialized = window.localStorage.getItem(iwt2SaveSlotKey(slot))
if (!serialized) return null
try {
return normalizeSave(JSON.parse(serialized))
@@ -167,6 +199,14 @@ export function loadExistingIwt2Save(): Iwt2Save | null {
}
}
export function loadIwt2SaveSlots(): Iwt2LocalSaveSlots {
return {
1: loadExistingIwt2Save(1),
2: loadExistingIwt2Save(2),
3: loadExistingIwt2Save(3),
}
}
export async function loadIwt2OnlineSave(): Promise<Iwt2OnlineSaveResult> {
const result = await requestGameApiJson<{ save: unknown | null }>('/api/iwt2/sync-save')
return {
@@ -185,12 +225,12 @@ export async function writeIwt2OnlineSave(save: Iwt2Save): Promise<Iwt2OnlineSav
}
}
export function writeIwt2Save(save: Iwt2Save) {
replaceIwt2Save(save)
export function writeIwt2Save(save: Iwt2Save, slot = activeIwt2SaveSlot()) {
replaceIwt2Save(save, slot)
}
export function replaceIwt2Save(save: Iwt2Save) {
window.localStorage.setItem(IWT2_SAVE_KEY, JSON.stringify(normalizeSave(save)))
export function replaceIwt2Save(save: Iwt2Save, slot = activeIwt2SaveSlot()) {
window.localStorage.setItem(iwt2SaveSlotKey(slot), JSON.stringify(normalizeSave(save)))
}
export function recordIwt2BossKill(
+1 -15
View File
@@ -9,7 +9,6 @@ import {
import { useDualScreen, useDualScreenWorkshopPublisher, type DualScreenWorkshopState } from '../../../dualScreen'
import { getGameMode } from '../../../gameRepository'
import {
createDefaultIwt2Save,
loadIwt2OnlineSave,
setIwt2InfusionAbility,
setIwt2PassiveInfusion,
@@ -1946,11 +1945,6 @@ export function Iwt2CloudSaveScreen({
setMessage('Local save now uses online progress.')
}, [onlineBackupsAvailable, onlineSave, onSaveUpdated])
const handleUseNewSave = useCallback(() => {
onSaveUpdated(createDefaultIwt2Save())
setMessage('Started a new local IWT2 save.')
}, [onSaveUpdated])
const actions = useMemo<Iwt2NavAction[]>(() => withBackAction([
{
key: 'use-local',
@@ -1970,16 +1964,8 @@ export function Iwt2CloudSaveScreen({
disabled: syncingOnlineSave || !onlineBackupsAvailable || !onlineSave,
onConfirm: handleUseOnlineSave,
},
{
key: 'new-save',
label: 'Use New Save File',
detail: 'Start over with a fresh IWT2 character. Online save is not overwritten until you choose local save.',
value: 'New',
onConfirm: handleUseNewSave,
},
], onBack), [
handleUseLocalSave,
handleUseNewSave,
handleUseOnlineSave,
onlineBackupsAvailable,
onlineSave,
@@ -1989,7 +1975,7 @@ export function Iwt2CloudSaveScreen({
])
return (
<Iwt2ScreenShell title="Backup Slot" onBack={onBack}>
<Iwt2ScreenShell title="Cloud Backup" onBack={onBack}>
{statusMessage && <p className="iwt2-screen-note">{statusMessage}</p>}
<Iwt2ActionList actions={actions} />
</Iwt2ScreenShell>