Compare commits

...

3 Commits

Author SHA1 Message Date
Warren H 1e24aecad8 Android build v1.0.52 2026-06-21 12:35:10 -04:00
Warren H c9fb28ab6d Android build v1.0.50 2026-06-21 12:34:35 -04:00
Warren H c1e2c6d8b5 Android build v1.0.50 2026-06-21 00:27:07 -04:00
13 changed files with 175 additions and 7 deletions
Binary file not shown.
Binary file not shown.
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 68
versionName "1.0.49"
versionCode 71
versionName "1.0.52"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+7 -2
View File
@@ -1,5 +1,6 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite'
import { catalogPayload } from '../server/catalog.mjs'
import { getProfile } from '../server/game-api.mjs'
const database = new DatabaseSync(':memory:')
@@ -7,10 +8,14 @@ const database = new DatabaseSync(':memory:')
try {
database.exec(readFileSync('db/schema.sql', 'utf8'))
database.exec(readFileSync('db/seed.sql', 'utf8'))
const profile = getProfile(database, 1)
const catalog = catalogPayload(getProfile(database, 1))
writeFileSync(
'src/offline-starter-profile.json',
`${JSON.stringify(profile, null, 2)}\n`,
`${JSON.stringify(catalog.profile, null, 2)}\n`,
)
writeFileSync(
'src/offline-catalog-meta.ts',
`export const bundledCatalogHash = '${catalog.hash}'\n`,
)
console.log('Offline starter profile exported from SQLite.')
} finally {
+74
View File
@@ -0,0 +1,74 @@
import { createHash } from 'node:crypto'
function normalizeRecipe(recipe) {
return {
...recipe,
components: recipe.components.map((component) => ({
...component,
owned: 0,
})),
canCraft: false,
}
}
function normalizeDungeon(dungeon) {
return {
...dungeon,
completionCount: 0,
leaderboard: [],
leaderboards: {
part_1: [],
part_2: [],
part_3: [],
full_run: [],
},
}
}
export function normalizeCatalogProfile(profile) {
return {
...profile,
character: {
...profile.character,
id: 1,
name: 'Mira',
level: 1,
experience: 0,
talentPoints: 1,
currentLevelExperience: 0,
nextLevelExperience: 100,
},
abilitySlots: profile.abilitySlots,
allocatedTalentPoints: 0,
inventory: [],
completedDungeonParts: 0,
completedRaidPhases: 0,
gearStats: {
averageItemLevel: 0,
healingPower: 0,
maxResourceBonus: 0,
},
setBonuses: profile.setBonuses.map((bonus) => ({
...bonus,
equippedPieces: 0,
active: false,
})),
craftingRecipes: profile.craftingRecipes.map(normalizeRecipe),
dungeons: profile.dungeons.map(normalizeDungeon),
}
}
export function catalogHash(profile) {
return createHash('sha256')
.update(JSON.stringify(profile))
.digest('hex')
}
export function catalogPayload(profile) {
const normalized = normalizeCatalogProfile(profile)
return {
version: 1,
hash: catalogHash(normalized),
profile: normalized,
}
}
+6
View File
@@ -9,6 +9,7 @@ import {
import { isIP } from 'node:net'
import { extname, resolve, sep } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { catalogPayload } from './catalog.mjs'
const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url))
const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', import.meta.url))
@@ -2696,6 +2697,11 @@ export async function handleApiRequest(request, response, next) {
return
}
if (request.url === '/api/catalog' && request.method === 'GET') {
sendJson(response, 200, catalogPayload(getProfile(database, 1)))
return
}
const session = requireSession(database, request)
if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
+3
View File
@@ -621,6 +621,7 @@ textarea:focus-visible,
min-height: 120px;
outline: 2px solid #3a3944;
padding: 14px;
position: relative;
}
.dual-top-member.selected {
@@ -5254,6 +5255,7 @@ h2 {
.member-health .health-text {
color: var(--ink);
display: none;
font-family: 'Press Start 2P', monospace;
font-size: 7px;
font-style: normal;
@@ -5302,6 +5304,7 @@ h2 {
inset: 0;
pointer-events: none;
position: absolute;
z-index: 3;
}
.floating-heal {
+2
View File
@@ -1375,6 +1375,7 @@ export function CombatScreen({
encounterIndex,
encounterCount: encounters.length,
party,
floatingTexts,
partySize: dungeon.partySize,
selectedId,
log,
@@ -1430,6 +1431,7 @@ export function CombatScreen({
selectedId,
spells,
freeCastReady,
floatingTexts,
roguelikeUpgrades,
speedMultiplier,
status,
+4
View File
@@ -1212,6 +1212,9 @@ export function PvPRoguelikeScreen({
encounterIndex,
encounterCount: encounters.length,
party: playerSide.party,
floatingTexts: floatingTexts
.filter((entry) => entry.side === 'player')
.map(({ id, memberId, value }) => ({ id, memberId, value })),
partySize: playerSide.party.length,
selectedId,
log,
@@ -1243,6 +1246,7 @@ export function PvPRoguelikeScreen({
encounter.maxHealth,
encounterIndex,
encounters.length,
floatingTexts,
gameClass.resourceName,
lastDevice,
log,
+10
View File
@@ -39,6 +39,11 @@ export type DualScreenCombatState = {
encounterIndex: number
encounterCount: number
party: PartyMember[]
floatingTexts: Array<{
id: number
memberId: string
value: number
}>
partySize: number
selectedId: string
log: CombatLogEntry[]
@@ -599,6 +604,11 @@ export function DualScreenTopCombat({
)}
<em className="health-text">{Math.ceil(member.health)} / {member.maxHealth}</em>
</div>
<div className="floating-combat-texts" aria-hidden="true">
{state.floatingTexts
.filter((entry) => entry.memberId === member.id)
.map((entry) => <span className="floating-heal" key={entry.id}>+{entry.value}</span>)}
</div>
{state.directPartyTargeting && targetBinding && (
<div className="member-target-key">
<ControllerBindingLabel
+66 -3
View File
@@ -1,4 +1,5 @@
import starterProfile from './offline-starter-profile.json'
import { bundledCatalogHash } from './offline-catalog-meta'
import type {
Account,
AuthSession,
@@ -82,6 +83,12 @@ type OnlineCache = {
dirty: boolean
}
type CatalogCache = {
version: 1
hash: string
profile: CharacterProfile
}
export type CloudSyncStatus = {
available: boolean
dirty: boolean
@@ -102,6 +109,8 @@ type LocalSaveStore = {
const modeKey = 'chronicle.repositoryMode'
const offlineSaveKey = 'chronicle.offlineSave.v1'
const onlineCacheKey = 'chronicle.onlineCache.v1'
const catalogCacheKey = 'chronicle.catalog.v1'
const catalogBundleKey = 'chronicle.catalog.bundleHash.v1'
const authTokenKey = 'chronicle.authToken.v1'
const offlineAccount = { id: -1, username: 'Offline' }
const ABILITY_SLOT_COUNT = 6
@@ -281,8 +290,42 @@ function clearOnlineCache() {
localStorage.removeItem(onlineCacheKey)
}
function bundledCatalog(): CatalogCache {
return {
version: 1,
hash: bundledCatalogHash,
profile: starterProfile as CharacterProfile,
}
}
function readCatalogCache(): CatalogCache | null {
if (localStorage.getItem(catalogBundleKey) !== bundledCatalogHash) {
localStorage.removeItem(catalogCacheKey)
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
return null
}
const serialized = localStorage.getItem(catalogCacheKey)
if (!serialized) return null
try {
const raw = JSON.parse(serialized) as CatalogCache
if (raw.version !== 1 || typeof raw.hash !== 'string' || !raw.profile) return null
return raw
} catch {
return null
}
}
function writeCatalogCache(cache: CatalogCache) {
localStorage.setItem(catalogBundleKey, bundledCatalogHash)
localStorage.setItem(catalogCacheKey, JSON.stringify(cache))
}
function activeCatalog(): CatalogCache {
return readCatalogCache() ?? bundledCatalog()
}
function buildProfile(save: OfflineSave): CharacterProfile {
const static_ = clone(starterProfile) as CharacterProfile
const static_ = clone(activeCatalog().profile)
const cd = save.characters[save.activeClassId]
const gameClass = static_.classes.find((c) => c.id === save.activeClassId)!
@@ -657,6 +700,23 @@ function isNetworkError(reason: unknown): reason is NetworkError {
return reason instanceof Error && Boolean((reason as NetworkError).network)
}
async function loadServerCatalog(): Promise<CatalogCache> {
return requestJson('/api/catalog')
}
async function refreshCatalogFromServer(): Promise<CatalogCache | null> {
try {
const catalog = await loadServerCatalog()
if (catalog.version !== 1 || !catalog.hash || !catalog.profile) return null
if (catalog.hash !== activeCatalog().hash || !readCatalogCache()) {
writeCatalogCache(catalog)
}
return catalog
} catch {
return null
}
}
function cachedOnlineSession(): AuthSession | null {
const cache = readOnlineCache()
if (!cache) return null
@@ -698,6 +758,7 @@ async function pushServerSyncSave(save: OfflineSave): Promise<{ profile: Charact
async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
const cache = readOnlineCache()
if (session.token) writeAuthToken(session.token)
await refreshCatalogFromServer()
if (!session.account || !session.profile) {
if (session.account && cache?.account.id === session.account.id) {
return {
@@ -848,7 +909,7 @@ const serverRepository: GameRepository = {
}
function emptyCharacterData(classId: number): CharacterData {
const static_ = clone(starterProfile) as CharacterProfile
const static_ = clone(activeCatalog().profile)
const gc = static_.classes.find((c) => c.id === classId)!
const talentRanks: Record<string, number> = {}
for (const t of gc.talents) talentRanks[String(t.id)] = 0
@@ -1544,7 +1605,9 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
if (!cache) {
throw new Error('No signed-in save is available for cloud sync.')
}
await refreshCatalogFromServer()
const synced = await pushServerSyncSave(cache.save)
await refreshCatalogFromServer()
writeOnlineCache({
version: 1,
account: cache.account,
@@ -1552,7 +1615,7 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
dirty: false,
})
writeMode('online')
return synced.profile
return buildProfile(synced.save)
}
export function selectOnlineMode() {
+1
View File
@@ -0,0 +1 @@
export const bundledCatalogHash = '07506c52bab428c439f09a9b82e39e6eff2b243fb972d664da48852059bcd937'