From 62611a36856e809ec4814512a06828f4d5154c7a Mon Sep 17 00:00:00 2001 From: achmad Date: Fri, 29 May 2026 16:36:05 +0700 Subject: [PATCH] feat: add battle pass handler with quests, rewards, XP --- backend/src/lib/handlers/battlepass.ts | 200 +++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 backend/src/lib/handlers/battlepass.ts diff --git a/backend/src/lib/handlers/battlepass.ts b/backend/src/lib/handlers/battlepass.ts new file mode 100644 index 0000000..89e9964 --- /dev/null +++ b/backend/src/lib/handlers/battlepass.ts @@ -0,0 +1,200 @@ +import { route, HandlerContext, HttpError } from '@/lib/router'; +import { getDb } from '@/lib/db'; + +const QUEST_DEFS = [ + { quest_id: 'kill_zombies_1', type: 'kill_zombies', name: 'Zombie Slayer I', description: 'Kill 100 zombies', target: 100, reward_exp: 50, reward_free_currency: 100 }, + { quest_id: 'kill_zombies_2', type: 'kill_zombies', name: 'Zombie Slayer II', description: 'Kill 500 zombies', target: 500, reward_exp: 100, reward_free_currency: 250 }, + { quest_id: 'survive_time_1', type: 'survive_time', name: 'Survivor I', description: 'Survive for 600 seconds', target: 600, reward_exp: 30, reward_free_currency: 50 }, + { quest_id: 'survive_waves_1', type: 'survive_waves', name: 'Wave Breaker I', description: 'Survive 10 waves', target: 10, reward_exp: 40, reward_free_currency: 75 }, + { quest_id: 'buy_black_shop_1', type: 'buy_black_shop', name: 'Black Shopper I', description: 'Buy 5 items from Black Shop', target: 5, reward_exp: 25, reward_free_currency: 50 }, + { quest_id: 'complete_npc_quest_1', type: 'complete_npc_quest', name: 'Helper I', description: 'Complete 3 NPC quests', target: 3, reward_exp: 35, reward_free_currency: 60 }, + { quest_id: 'earn_gold_1', type: 'earn_gold', name: 'Gold Rush I', description: 'Earn 5000 gold', target: 5000, reward_exp: 45, reward_free_currency: 100 }, + { quest_id: 'hero_level_1', type: 'hero_level', name: 'Stronger I', description: 'Reach level 10', target: 10, reward_exp: 30, reward_free_currency: 50 }, + { quest_id: 'cook_grilled_meat_1', type: 'cook_grilled_meat', name: 'Chef I', description: 'Cook grilled meat', target: 1, reward_exp: 20, reward_free_currency: 25 }, + { quest_id: 'use_campfire_1', type: 'use_campfire', name: 'Camper I', description: 'Use campfire 5 times', target: 5, reward_exp: 15, reward_free_currency: 25 }, + { quest_id: 'tip_teammate_1', type: 'tip_teammate', name: 'Friendly I', description: 'Tip teammates 3 times', target: 3, reward_exp: 20, reward_free_currency: 30 }, + { quest_id: 'deal_damage_1', type: 'deal_damage', name: 'Berserker I', description: 'Deal 50000 damage', target: 50000, reward_exp: 60, reward_free_currency: 150 }, + { quest_id: 'collect_item_1', type: 'collect_item', name: 'Collector I', description: 'Collect a rare item', target: 1, reward_exp: 40, reward_free_currency: 80, target_item: 'rare' }, +]; + +// POST /battlepass — Create or ensure BP exists for a player, assign default quests +route('battlepass', ['POST'], (ctx: HandlerContext) => { + const { steam_id } = ctx.body as any; + if (!steam_id) throw new HttpError(400, 'steam_id required'); + const db = getDb(); + db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id, level, experience) VALUES (?, 0, 0)').run(steam_id); + + const existing = db.prepare('SELECT COUNT(*) as c FROM battle_pass_quests WHERE steam_id = ?').get(steam_id) as any; + if (existing.c === 0) { + const insert = db.prepare(` + INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, description, target, reward_exp, reward_free_currency) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `); + for (const q of QUEST_DEFS) { + insert.run(steam_id, q.quest_id, q.type, q.name, q.description, q.target, q.reward_exp, q.reward_free_currency); + } + } + return { success: true }; +}); + +// GET /battlepass/:steamId — Get BP data +route('battlepass/:steamId', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + let bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) { + db.prepare('INSERT OR IGNORE INTO battle_passes (steam_id) VALUES (?)').run(ctx.params.steamId); + bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId); + } + return { + level: bp.level, + experience: bp.experience, + has_premium: bp.has_premium === 1, + claimed_rewards: JSON.parse(bp.claimed_rewards || '[]'), + claimed_premium_rewards: JSON.parse(bp.claimed_premium_rewards || '[]'), + }; +}); + +// GET /battlepass/:steamId/quests — Get quests for a player +route('battlepass/:steamId/quests', ['GET'], (ctx: HandlerContext) => { + const db = getDb(); + const quests = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? ORDER BY id').all(ctx.params.steamId); + return { quests }; +}); + +// POST /battlepass/:steamId/quests/progress — Sync quest progress +route('battlepass/:steamId/quests/progress', ['POST'], (ctx: HandlerContext) => { + const { quest_id, progress } = ctx.body as any; + if (!quest_id) throw new HttpError(400, 'quest_id required'); + const db = getDb(); + const quest = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?').get(ctx.params.steamId, quest_id) as any; + if (!quest) { + db.prepare(`INSERT INTO battle_pass_quests (steam_id, quest_id, type, name, target, progress) VALUES (?, ?, 'custom', ?, 1, ?)`) + .run(ctx.params.steamId, quest_id, quest_id, progress || 0); + return { success: true, completed: false, progress: progress || 0 }; + } + const newProgress = Math.min(progress ?? quest.progress, quest.target); + const completed = newProgress >= quest.target ? 1 : 0; + db.prepare('UPDATE battle_pass_quests SET progress = ?, completed = ?, updated_at = datetime(\'now\') WHERE id = ?') + .run(newProgress, completed, quest.id); + return { success: true, completed: completed === 1, progress: newProgress }; +}); + +// POST /battlepass/:steamId/quests/claim — Claim a quest reward +route('battlepass/:steamId/quests/claim', ['POST'], (ctx: HandlerContext) => { + const { quest_id } = ctx.body as any; + if (!quest_id) throw new HttpError(400, 'quest_id required'); + const db = getDb(); + const quest = db.prepare('SELECT * FROM battle_pass_quests WHERE steam_id = ? AND quest_id = ?').get(ctx.params.steamId, quest_id) as any; + if (!quest) throw new HttpError(404, 'Quest not found'); + if (!quest.completed) throw new HttpError(400, 'Quest not completed'); + if (quest.claimed) throw new HttpError(400, 'Already claimed'); + + db.prepare('UPDATE battle_pass_quests SET claimed = 1, updated_at = datetime(\'now\') WHERE id = ?').run(quest.id); + db.prepare('UPDATE players SET free_currency = free_currency + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(quest.reward_free_currency, ctx.params.steamId); + db.prepare('UPDATE battle_passes SET experience = experience + ?, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(quest.reward_exp, ctx.params.steamId); + + const bp = db.prepare('SELECT level, experience FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + return { + success: true, + reward_exp: quest.reward_exp, + reward_free_currency: quest.reward_free_currency, + new_level: bp.level, + new_experience: bp.experience, + }; +}); + +// POST /battlepass/:steamId/hero-played — Record a hero being played (fire-and-forget) +route('battlepass/:steamId/hero-played', ['POST'], (ctx: HandlerContext) => { + return { success: true }; +}); + +// POST /battlepass/:steamId/claim — Claim a free BP level reward +route('battlepass/:steamId/claim', ['POST'], (ctx: HandlerContext) => { + const { steam_id, level } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + let claimed = JSON.parse(bp.claimed_rewards || '[]'); + if (!claimed.includes(level)) { + claimed.push(level); + db.prepare("UPDATE battle_passes SET claimed_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(claimed), ctx.params.steamId); + } + return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: 0 } }; +}); + +// POST /battlepass/:steamId/claim-premium — Claim a premium BP level reward +route('battlepass/:steamId/claim-premium', ['POST'], (ctx: HandlerContext) => { + const { steam_id, level } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + let claimed = JSON.parse(bp.claimed_premium_rewards || '[]'); + if (!claimed.includes(level)) { + claimed.push(level); + db.prepare("UPDATE battle_passes SET claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify(claimed), ctx.params.steamId); + } + return { success: true, level, currency_granted: { free_currency: level * 250, donate_currency: level * 100 } }; +}); + +// POST /battlepass/:steamId/claim-all — Claim all rewards up to current level +route('battlepass/:steamId/claim-all', ['POST'], (ctx: HandlerContext) => { + const { steam_id } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + + const unclaimedFree: number[] = []; + const unclaimedPremium: number[] = []; + const claimedFree = JSON.parse(bp.claimed_rewards || '[]') as number[]; + const claimedPremium = JSON.parse(bp.claimed_premium_rewards || '[]') as number[]; + + for (let lvl = 1; lvl <= bp.level; lvl++) { + if (!claimedFree.includes(lvl)) unclaimedFree.push(lvl); + if (bp.has_premium && !claimedPremium.includes(lvl)) unclaimedPremium.push(lvl); + } + + db.prepare("UPDATE battle_passes SET claimed_rewards = ?, claimed_premium_rewards = ?, updated_at = datetime('now') WHERE steam_id = ?") + .run(JSON.stringify([...claimedFree, ...unclaimedFree]), + JSON.stringify([...claimedPremium, ...unclaimedPremium]), + ctx.params.steamId); + + return { + success: true, + free_levels: unclaimedFree, + premium_levels: unclaimedPremium, + currency_granted: { + free_currency: unclaimedFree.length * 250 + unclaimedPremium.length * 250, + donate_currency: unclaimedPremium.length * 100, + }, + }; +}); + +// POST /battlepass/:steamId/buy-premium — Activate premium BP +route('battlepass/:steamId/buy-premium', ['POST'], (ctx: HandlerContext) => { + const db = getDb(); + db.prepare("UPDATE battle_passes SET has_premium = 1, updated_at = datetime('now') WHERE steam_id = ?") + .run(ctx.params.steamId); + return { success: true }; +}); + +// POST /battlepass/:steamId/addexp — Add experience to BP +route('battlepass/:steamId/addexp', ['POST'], (ctx: HandlerContext) => { + const { experience } = ctx.body as any; + const db = getDb(); + const bp = db.prepare('SELECT * FROM battle_passes WHERE steam_id = ?').get(ctx.params.steamId) as any; + if (!bp) throw new HttpError(404, 'BP not found'); + + const newExp = bp.experience + (experience || 0); + const levelUp = Math.floor(newExp / 1000); + const newLevel = bp.level + levelUp; + const remainder = newExp % 1000; + + db.prepare('UPDATE battle_passes SET experience = ?, level = ?, updated_at = datetime(\'now\') WHERE steam_id = ?') + .run(remainder, newLevel, ctx.params.steamId); + + return { level: newLevel, experience: remainder, level_up: levelUp > 0 }; +});