made some changes to the UI, removed leaderboards. updated gamesaves

This commit is contained in:
Warren H
2026-06-18 13:00:29 -04:00
parent 3c90998a61
commit a604569a2f
44 changed files with 2301 additions and 435 deletions
+342
View File
@@ -854,6 +854,337 @@ export function getProfile(database, characterId, accountId) {
}
}
function exportCharacterData(database, characterId, classId) {
const character = database.prepare(`
SELECT
level,
experience,
talent_points AS talentPoints
FROM characters
WHERE id = ?
`).get(characterId)
const slots = database.prepare(`
SELECT slot_number AS slotNumber, spell_id AS spellId
FROM character_ability_slots
WHERE character_id = ?
ORDER BY slot_number
`).all(characterId)
const talents = database.prepare(`
SELECT
talents.id,
COALESCE(character_talents.rank, 0) AS rank
FROM talents
LEFT JOIN character_talents
ON character_talents.talent_id = talents.id
AND character_talents.character_id = ?
WHERE talents.class_id = ?
ORDER BY talents.id
`).all(characterId, classId)
const inventory = database.prepare(`
SELECT
items.id,
items.slug,
items.name,
items.slot,
items.rarity,
items.item_level AS itemLevel,
items.healing_power AS healingPower,
items.max_resource_bonus AS maxResourceBonus,
items.glyph,
items.description,
item_sets.id AS setId,
item_sets.slug AS setSlug,
item_sets.name AS setName,
character_inventory.quantity,
character_inventory.equipped
FROM character_inventory
JOIN items ON items.id = character_inventory.item_id
LEFT JOIN item_set_items ON item_set_items.item_id = items.id
LEFT JOIN item_sets ON item_sets.id = item_set_items.set_id
WHERE character_inventory.character_id = ?
ORDER BY items.slot, items.item_level DESC, items.id
`).all(characterId).map((item) => ({ ...item, equipped: Boolean(item.equipped) }))
const talentRanks = {}
for (const talent of talents) {
if (talent.rank > 0) {
talentRanks[String(talent.id)] = talent.rank
}
}
return {
level: character.level,
experience: character.experience,
talentPoints: character.talentPoints,
abilitySlots: Array.from({ length: 6 }, (_, index) => {
const slot = slots.find((candidate) => candidate.slotNumber === index + 1)
return slot?.spellId ?? null
}),
talentRanks,
inventory,
}
}
function buildSyncSave(database, accountId, activeCharacterId) {
const account = database.prepare(`
SELECT
completed_dungeon_parts AS completedDungeonParts,
completed_raid_phases AS completedRaidPhases
FROM accounts
WHERE id = ?
`).get(accountId)
const characters = database.prepare(`
SELECT
id,
class_id AS classId,
name
FROM characters
WHERE account_id = ?
ORDER BY class_id
`).all(accountId)
const activeClassId = characters.find((candidate) => candidate.id === activeCharacterId)?.classId
?? characters[0]?.classId
?? 1
const characterName = characters.find((candidate) => candidate.id === activeCharacterId)?.name
?? characters[0]?.name
?? 'Mira'
return {
version: 3,
characterName,
activeClassId,
completedDungeonParts: account?.completedDungeonParts ?? 0,
completedRaidPhases: account?.completedRaidPhases ?? 0,
characters: Object.fromEntries(
characters.map((character) => [
character.classId,
exportCharacterData(database, character.id, character.classId),
]),
),
lootRolls: {},
}
}
function clampInteger(value, fallback, min, max) {
const numeric = Number(value)
if (!Number.isInteger(numeric)) return fallback
return Math.min(max, Math.max(min, numeric))
}
function importSyncSave(database, accountId, activeCharacterId, payload) {
const save = payload?.save
if (
!save
|| typeof save !== 'object'
|| Number(save.version) !== 3
|| typeof save.characterName !== 'string'
|| !save.characters
|| typeof save.characters !== 'object'
) {
throw new Error('The local save snapshot is invalid.')
}
const maxLevel = Number(
database.prepare("SELECT value FROM game_settings WHERE key = 'max_level'").get()?.value ?? 25,
)
const maxTalentPoints = Number(
database.prepare("SELECT value FROM game_settings WHERE key = 'max_talent_points'").get()?.value ?? 25,
)
const maxExperience = database.prepare(`
SELECT experience_required AS experienceRequired
FROM level_progression
WHERE level = ?
`).get(maxLevel).experienceRequired
const classIds = database.prepare('SELECT id FROM classes ORDER BY id').all().map((row) => row.id)
const existingCharacters = database.prepare(`
SELECT
id,
class_id AS classId,
name
FROM characters
WHERE account_id = ?
ORDER BY class_id
`).all(accountId)
if (existingCharacters.length === 0) {
throw new Error('No character found for this account.')
}
const baseCharacterName = existingCharacters.find((candidate) => candidate.id === activeCharacterId)?.name
?? existingCharacters[0].name
const characterName = normalizeCharacterName(save.characterName, baseCharacterName)
const itemRows = database.prepare(`
SELECT id, slot
FROM items
`).all()
const itemSlots = new Map(itemRows.map((item) => [item.id, item.slot]))
const spellIdsByClass = new Map(
classIds.map((classId) => [
classId,
new Set(
database.prepare(`
SELECT id
FROM spells
WHERE class_id = ?
`).all(classId).map((spell) => spell.id),
),
]),
)
const talentRowsByClass = new Map(
classIds.map((classId) => [
classId,
database.prepare(`
SELECT
id,
max_rank AS maxRank
FROM talents
WHERE class_id = ?
`).all(classId),
]),
)
const charactersByClass = new Map(existingCharacters.map((character) => [character.classId, character]))
database.exec('BEGIN')
try {
for (const classId of classIds) {
if (!charactersByClass.has(classId)) {
const characterId = initializeCharacter(database, accountId, characterName, classId)
charactersByClass.set(classId, { id: characterId, classId, name: characterName })
}
}
database.prepare(`
UPDATE accounts
SET completed_dungeon_parts = ?, completed_raid_phases = ?
WHERE id = ?
`).run(
clampInteger(save.completedDungeonParts, 0, 0, 3),
clampInteger(save.completedRaidPhases, 0, 0, 3),
accountId,
)
const replaceSlot = database.prepare(`
INSERT INTO character_ability_slots (character_id, slot_number, spell_id)
VALUES (?, ?, ?)
`)
const insertTalent = database.prepare(`
INSERT INTO character_talents (character_id, talent_id, rank)
VALUES (?, ?, ?)
`)
const insertInventory = database.prepare(`
INSERT INTO character_inventory (character_id, item_id, quantity, equipped)
VALUES (?, ?, ?, ?)
`)
for (const classId of classIds) {
const local = save.characters[classId]
if (!local || typeof local !== 'object') continue
const characterId = charactersByClass.get(classId).id
database.prepare(`
UPDATE characters
SET name = ?, level = ?, experience = ?, talent_points = ?
WHERE id = ?
`).run(
characterName,
clampInteger(local.level, 1, 1, maxLevel),
clampInteger(local.experience, 0, 0, maxExperience),
clampInteger(local.talentPoints, 1, 0, maxTalentPoints),
characterId,
)
const rawSlots = Array.isArray(local.abilitySlots)
? local.abilitySlots.slice(0, 6)
: []
while (rawSlots.length < 6) rawSlots.push(null)
const validSpellIds = spellIdsByClass.get(classId) ?? new Set()
const seenSpellIds = new Set()
const normalizedSlots = rawSlots.map((value) => {
if (value === null) return null
const spellId = Number(value)
if (
!Number.isInteger(spellId)
|| !validSpellIds.has(spellId)
|| seenSpellIds.has(spellId)
) {
return null
}
seenSpellIds.add(spellId)
return spellId
})
database.prepare(`
DELETE FROM character_ability_slots
WHERE character_id = ?
`).run(characterId)
normalizedSlots.forEach((spellId, index) => {
replaceSlot.run(characterId, index + 1, spellId)
})
database.prepare(`
DELETE FROM character_talents
WHERE character_id = ?
AND talent_id IN (SELECT id FROM talents WHERE class_id = ?)
`).run(characterId, classId)
const localTalentRanks = local.talentRanks && typeof local.talentRanks === 'object'
? local.talentRanks
: {}
for (const talent of talentRowsByClass.get(classId) ?? []) {
const rank = clampInteger(localTalentRanks[String(talent.id)], 0, 0, talent.maxRank)
if (rank > 0) {
insertTalent.run(characterId, talent.id, rank)
}
}
database.prepare(`
DELETE FROM character_inventory
WHERE character_id = ?
`).run(characterId)
const inventoryByItemId = new Map()
const equippedSlots = new Set()
for (const item of Array.isArray(local.inventory) ? local.inventory : []) {
const itemId = Number(item?.id)
const slot = itemSlots.get(itemId)
const quantity = clampInteger(item?.quantity, 0, 0, 9999)
if (!slot || quantity <= 0) continue
const current = inventoryByItemId.get(itemId) ?? { quantity: 0, equipped: false }
current.quantity = Math.min(9999, current.quantity + quantity)
if (
Boolean(item?.equipped)
&& slot !== 'component'
&& !equippedSlots.has(slot)
) {
current.equipped = true
equippedSlots.add(slot)
}
inventoryByItemId.set(itemId, current)
}
for (const [itemId, itemState] of inventoryByItemId) {
insertInventory.run(characterId, itemId, itemState.quantity, itemState.equipped ? 1 : 0)
}
}
let syncedClassId = clampInteger(
save.activeClassId,
existingCharacters[0]?.classId ?? 1,
classIds[0] ?? 1,
classIds[classIds.length - 1] ?? 1,
)
if (!charactersByClass.has(syncedClassId)) {
syncedClassId = existingCharacters[0]?.classId ?? 1
}
const syncedCharacterId = charactersByClass.get(syncedClassId)?.id ?? activeCharacterId
database.prepare(`
UPDATE sessions
SET active_character_id = ?
WHERE account_id = ?
`).run(syncedCharacterId, accountId)
database.exec('COMMIT')
return {
profile: getProfile(database, syncedCharacterId, accountId),
save: buildSyncSave(database, accountId, syncedCharacterId),
}
} catch (error) {
database.exec('ROLLBACK')
throw error
}
}
function itemById(database, itemId) {
return database.prepare(`
SELECT
@@ -1964,6 +2295,17 @@ export async function handleApiRequest(request, response, next) {
const session = requireSession(database, request)
if (request.url === '/api/profile/sync-save' && request.method === 'GET') {
sendJson(response, 200, buildSyncSave(database, session.accountId, session.characterId))
return
}
if (request.url === '/api/profile/sync-save' && request.method === 'PUT') {
const payload = await readJson(request, 512 * 1024)
sendJson(response, 200, importSyncSave(database, session.accountId, session.characterId, payload))
return
}
if (request.url === '/api/profile' && request.method === 'GET') {
sendJson(response, 200, getProfile(database, session.characterId, session.accountId))
return