113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
|
|
// gacha.ts — authoritative pull execution
|
||
|
|
//
|
||
|
|
// All pull outcomes are resolved server-side. The client sends a banner ID and
|
||
|
|
// pull count; this module resolves rarity, determines the result from the
|
||
|
|
// banner pool, updates pity counters, deducts currency, and records pull
|
||
|
|
// history. Nothing here is computed client-side.
|
||
|
|
//
|
||
|
|
// Pity model (PoC defaults — all values should be content-driven):
|
||
|
|
// - Soft pity begins at 70 pulls (increasing rate toward hard pity)
|
||
|
|
// - Hard pity at 90 pulls guarantees SSR
|
||
|
|
// - Featured SSR rate-up: 50% chance when SSR hits
|
||
|
|
// - Pity carries over between sessions but NOT between different banners
|
||
|
|
// unless the banner explicitly declares carry-over.
|
||
|
|
|
||
|
|
export interface BannerDefinition {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
type: "standard" | "featured" | "event";
|
||
|
|
pull_cost_currency: string;
|
||
|
|
pull_cost_amount: number;
|
||
|
|
multi_cost_amount: number; // cost for 10-pull; usually 10x single minus one
|
||
|
|
pool: BannerPoolEntry[];
|
||
|
|
soft_pity_start: number;
|
||
|
|
hard_pity: number;
|
||
|
|
featured_character_ids: string[];
|
||
|
|
active_from: number; // unix ms
|
||
|
|
active_until: number | null; // null = permanent
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface BannerPoolEntry {
|
||
|
|
character_id: string;
|
||
|
|
rarity: "R" | "SR" | "SSR" | "UR";
|
||
|
|
base_weight: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface PullRecord {
|
||
|
|
banner_id: string;
|
||
|
|
character_id: string;
|
||
|
|
rarity: string;
|
||
|
|
pulled_at: number;
|
||
|
|
pity_count_at_pull: number;
|
||
|
|
was_soft_pity: boolean;
|
||
|
|
was_hard_pity: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── RPCs ──────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export function rpcGachaPull(
|
||
|
|
ctx: nkruntime.Context,
|
||
|
|
logger: nkruntime.Logger,
|
||
|
|
nk: nkruntime.Nakama,
|
||
|
|
payload: string
|
||
|
|
): string {
|
||
|
|
// TODO PR 5: implement full pull resolution
|
||
|
|
// Stub returns a placeholder response so the endpoint is registered and
|
||
|
|
// callable from the Godot client in PoC step 1.
|
||
|
|
logger.info(`gacha/pull stub called by ${ctx.userId}`);
|
||
|
|
|
||
|
|
const req = JSON.parse(payload || "{}");
|
||
|
|
const bannerId: string = req.banner_id ?? "standard";
|
||
|
|
const count: number = req.count ?? 1;
|
||
|
|
|
||
|
|
if (count !== 1 && count !== 10) {
|
||
|
|
throw new Error("Pull count must be 1 or 10");
|
||
|
|
}
|
||
|
|
|
||
|
|
return JSON.stringify({
|
||
|
|
ok: false,
|
||
|
|
error: "not_implemented",
|
||
|
|
message: "Gacha pull system is stubbed. Implement in PR 5.",
|
||
|
|
banner_id: bannerId,
|
||
|
|
count,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function rpcListBanners(
|
||
|
|
ctx: nkruntime.Context,
|
||
|
|
logger: nkruntime.Logger,
|
||
|
|
nk: nkruntime.Nakama,
|
||
|
|
_payload: string
|
||
|
|
): string {
|
||
|
|
// TODO PR 5: load active banners from content storage
|
||
|
|
logger.info(`gacha/banners stub called by ${ctx.userId}`);
|
||
|
|
return JSON.stringify({ banners: [], message: "Banner list is stubbed. Implement in PR 5." });
|
||
|
|
}
|
||
|
|
|
||
|
|
export function rpcGetPity(
|
||
|
|
ctx: nkruntime.Context,
|
||
|
|
logger: nkruntime.Logger,
|
||
|
|
nk: nkruntime.Nakama,
|
||
|
|
payload: string
|
||
|
|
): string {
|
||
|
|
// TODO PR 5: read pity counters from player storage
|
||
|
|
const req = JSON.parse(payload || "{}");
|
||
|
|
return JSON.stringify({
|
||
|
|
banner_id: req.banner_id ?? "standard",
|
||
|
|
current_pity: 0,
|
||
|
|
soft_pity_start: 70,
|
||
|
|
hard_pity: 90,
|
||
|
|
message: "Pity counter is stubbed. Implement in PR 5.",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export function rpcGetPullHistory(
|
||
|
|
ctx: nkruntime.Context,
|
||
|
|
logger: nkruntime.Logger,
|
||
|
|
nk: nkruntime.Nakama,
|
||
|
|
_payload: string
|
||
|
|
): string {
|
||
|
|
// TODO PR 5: load pull history from player storage
|
||
|
|
return JSON.stringify({ history: [], message: "Pull history is stubbed. Implement in PR 5." });
|
||
|
|
}
|