made some changes to the UI, removed leaderboards. updated gamesaves
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user