Compare commits

...

2 Commits

Author SHA1 Message Date
Warren H c1e2c6d8b5 Android build v1.0.50 2026-06-21 00:27:07 -04:00
Warren H f7b041f86f Android build v1.0.49 2026-06-21 00:13:06 -04:00
10 changed files with 222 additions and 10 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 67 versionCode 69
versionName "1.0.47" versionName "1.0.50"
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.
+15
View File
@@ -1908,6 +1908,21 @@ JOIN coin_sources
ON coin_sources.encounter_id = crafting_recipes.source_encounter_id ON coin_sources.encounter_id = crafting_recipes.source_encounter_id
AND coin_sources.difficulty_id = crafting_recipes.difficulty_id; AND coin_sources.difficulty_id = crafting_recipes.difficulty_id;
DELETE FROM crafting_recipe_components
WHERE recipe_id IN (1101, 1102, 1103);
INSERT INTO crafting_recipe_components (recipe_id, item_id, quantity)
SELECT recipe_id, items.id, quantity
FROM (
SELECT 1101 AS recipe_id, 'tigrex-boss-coin-diff-2-ilvl-10' AS item_slug, 5 AS quantity
UNION ALL SELECT 1101, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
UNION ALL SELECT 1102, 'tigrex-boss-coin-diff-2-ilvl-10', 5
UNION ALL SELECT 1102, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
UNION ALL SELECT 1103, 'tigrex-boss-coin-diff-2-ilvl-10', 5
UNION ALL SELECT 1103, 'bulldrome-boss-coin-diff-2-ilvl-10', 5
) AS requirements
JOIN items ON items.slug = requirements.item_slug;
DELETE FROM gear_upgrade_paths; DELETE FROM gear_upgrade_paths;
INSERT INTO gear_upgrade_paths (from_item_id, to_item_id) INSERT INTO gear_upgrade_paths (from_item_id, to_item_id)
+7 -2
View File
@@ -1,5 +1,6 @@
import { readFileSync, writeFileSync } from 'node:fs' import { readFileSync, writeFileSync } from 'node:fs'
import { DatabaseSync } from 'node:sqlite' import { DatabaseSync } from 'node:sqlite'
import { catalogPayload } from '../server/catalog.mjs'
import { getProfile } from '../server/game-api.mjs' import { getProfile } from '../server/game-api.mjs'
const database = new DatabaseSync(':memory:') const database = new DatabaseSync(':memory:')
@@ -7,10 +8,14 @@ const database = new DatabaseSync(':memory:')
try { try {
database.exec(readFileSync('db/schema.sql', 'utf8')) database.exec(readFileSync('db/schema.sql', 'utf8'))
database.exec(readFileSync('db/seed.sql', 'utf8')) database.exec(readFileSync('db/seed.sql', 'utf8'))
const profile = getProfile(database, 1) const catalog = catalogPayload(getProfile(database, 1))
writeFileSync( writeFileSync(
'src/offline-starter-profile.json', '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.') console.log('Offline starter profile exported from SQLite.')
} finally { } 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 { isIP } from 'node:net'
import { extname, resolve, sep } from 'node:path' import { extname, resolve, sep } from 'node:path'
import { DatabaseSync } from 'node:sqlite' import { DatabaseSync } from 'node:sqlite'
import { catalogPayload } from './catalog.mjs'
const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url)) const databasePath = fileURLToPath(new URL('../data/game.db', import.meta.url))
const bossImageDirectory = fileURLToPath(new URL('../data/uploads/bosses/', 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 return
} }
if (request.url === '/api/catalog' && request.method === 'GET') {
sendJson(response, 200, catalogPayload(getProfile(database, 1)))
return
}
const session = requireSession(database, request) const session = requireSession(database, request)
if (request.url === '/api/profile/sync-save' && request.method === 'GET') { if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
+66 -3
View File
@@ -1,4 +1,5 @@
import starterProfile from './offline-starter-profile.json' import starterProfile from './offline-starter-profile.json'
import { bundledCatalogHash } from './offline-catalog-meta'
import type { import type {
Account, Account,
AuthSession, AuthSession,
@@ -82,6 +83,12 @@ type OnlineCache = {
dirty: boolean dirty: boolean
} }
type CatalogCache = {
version: 1
hash: string
profile: CharacterProfile
}
export type CloudSyncStatus = { export type CloudSyncStatus = {
available: boolean available: boolean
dirty: boolean dirty: boolean
@@ -102,6 +109,8 @@ type LocalSaveStore = {
const modeKey = 'chronicle.repositoryMode' const modeKey = 'chronicle.repositoryMode'
const offlineSaveKey = 'chronicle.offlineSave.v1' const offlineSaveKey = 'chronicle.offlineSave.v1'
const onlineCacheKey = 'chronicle.onlineCache.v1' const onlineCacheKey = 'chronicle.onlineCache.v1'
const catalogCacheKey = 'chronicle.catalog.v1'
const catalogBundleKey = 'chronicle.catalog.bundleHash.v1'
const authTokenKey = 'chronicle.authToken.v1' const authTokenKey = 'chronicle.authToken.v1'
const offlineAccount = { id: -1, username: 'Offline' } const offlineAccount = { id: -1, username: 'Offline' }
const ABILITY_SLOT_COUNT = 6 const ABILITY_SLOT_COUNT = 6
@@ -281,8 +290,42 @@ function clearOnlineCache() {
localStorage.removeItem(onlineCacheKey) 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 { function buildProfile(save: OfflineSave): CharacterProfile {
const static_ = clone(starterProfile) as CharacterProfile const static_ = clone(activeCatalog().profile)
const cd = save.characters[save.activeClassId] const cd = save.characters[save.activeClassId]
const gameClass = static_.classes.find((c) => c.id === 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) 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 { function cachedOnlineSession(): AuthSession | null {
const cache = readOnlineCache() const cache = readOnlineCache()
if (!cache) return null if (!cache) return null
@@ -698,6 +758,7 @@ async function pushServerSyncSave(save: OfflineSave): Promise<{ profile: Charact
async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> { async function finalizeOnlineSession(session: AuthSession): Promise<AuthSession> {
const cache = readOnlineCache() const cache = readOnlineCache()
if (session.token) writeAuthToken(session.token) if (session.token) writeAuthToken(session.token)
await refreshCatalogFromServer()
if (!session.account || !session.profile) { if (!session.account || !session.profile) {
if (session.account && cache?.account.id === session.account.id) { if (session.account && cache?.account.id === session.account.id) {
return { return {
@@ -848,7 +909,7 @@ const serverRepository: GameRepository = {
} }
function emptyCharacterData(classId: number): CharacterData { 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 gc = static_.classes.find((c) => c.id === classId)!
const talentRanks: Record<string, number> = {} const talentRanks: Record<string, number> = {}
for (const t of gc.talents) talentRanks[String(t.id)] = 0 for (const t of gc.talents) talentRanks[String(t.id)] = 0
@@ -1544,7 +1605,9 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
if (!cache) { if (!cache) {
throw new Error('No signed-in save is available for cloud sync.') throw new Error('No signed-in save is available for cloud sync.')
} }
await refreshCatalogFromServer()
const synced = await pushServerSyncSave(cache.save) const synced = await pushServerSyncSave(cache.save)
await refreshCatalogFromServer()
writeOnlineCache({ writeOnlineCache({
version: 1, version: 1,
account: cache.account, account: cache.account,
@@ -1552,7 +1615,7 @@ export async function syncCloudSave(): Promise<CharacterProfile> {
dirty: false, dirty: false,
}) })
writeMode('online') writeMode('online')
return synced.profile return buildProfile(synced.save)
} }
export function selectOnlineMode() { export function selectOnlineMode() {
+1
View File
@@ -0,0 +1 @@
export const bundledCatalogHash = '07506c52bab428c439f09a9b82e39e6eff2b243fb972d664da48852059bcd937'
+51 -3
View File
@@ -1437,6 +1437,22 @@
"setName": null "setName": null
}, },
"components": [ "components": [
{
"item": {
"id": 383002,
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 5,
"owned": 0
},
{ {
"item": { "item": {
"id": 683002, "id": 683002,
@@ -1450,7 +1466,7 @@
"glyph": "$", "glyph": "$",
"description": "A boss coin from Tigrex used for item level 10 crafting." "description": "A boss coin from Tigrex used for item level 10 crafting."
}, },
"quantity": 10, "quantity": 5,
"owned": 0 "owned": 0
} }
], ],
@@ -1477,6 +1493,22 @@
"setName": null "setName": null
}, },
"components": [ "components": [
{
"item": {
"id": 383002,
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 5,
"owned": 0
},
{ {
"item": { "item": {
"id": 683002, "id": 683002,
@@ -1490,7 +1522,7 @@
"glyph": "$", "glyph": "$",
"description": "A boss coin from Tigrex used for item level 10 crafting." "description": "A boss coin from Tigrex used for item level 10 crafting."
}, },
"quantity": 10, "quantity": 5,
"owned": 0 "owned": 0
} }
], ],
@@ -1517,6 +1549,22 @@
"setName": null "setName": null
}, },
"components": [ "components": [
{
"item": {
"id": 383002,
"slug": "bulldrome-boss-coin-diff-2-ilvl-10",
"name": "Green Bulldrome Coin",
"slot": "component",
"rarity": "uncommon",
"itemLevel": 10,
"healingPower": 0,
"maxResourceBonus": 0,
"glyph": "$",
"description": "A boss coin from Bulldrome used for item level 10 crafting."
},
"quantity": 5,
"owned": 0
},
{ {
"item": { "item": {
"id": 683002, "id": 683002,
@@ -1530,7 +1578,7 @@
"glyph": "$", "glyph": "$",
"description": "A boss coin from Tigrex used for item level 10 crafting." "description": "A boss coin from Tigrex used for item level 10 crafting."
}, },
"quantity": 10, "quantity": 5,
"owned": 0 "owned": 0
} }
], ],