feat(eod): pay reduced XP with creator banking on custom minefield runs
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
243bb8ae0d
commit
efda519569
1 changed files with 60 additions and 4 deletions
|
|
@ -5,7 +5,11 @@ import { getPayload } from "payload";
|
|||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
import { broadcastToUser } from "@/lib/realtime/bus";
|
||||
import { creditEodXp } from "@/lib/custom-minigames/payouts";
|
||||
import {
|
||||
applyCreatorBank,
|
||||
customPlayerXp,
|
||||
isEodUnlocked,
|
||||
minefieldFlagsError,
|
||||
validateGridSize,
|
||||
|
|
@ -120,6 +124,7 @@ export async function createMinefield(input: {
|
|||
rows: number;
|
||||
cols: number;
|
||||
mines: unknown;
|
||||
exposeEdgeMines?: boolean;
|
||||
}): Promise<ActionResult<{ id: number }>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
|
|
@ -149,6 +154,7 @@ export async function createMinefield(input: {
|
|||
mines: mineCheck.positions,
|
||||
mineCount: mineCheck.positions.length,
|
||||
published: true,
|
||||
exposeEdgeMines: input.exposeEdgeMines ?? true,
|
||||
playCount: 0,
|
||||
},
|
||||
depth: 0,
|
||||
|
|
@ -177,6 +183,7 @@ export async function updateMinefield(input: {
|
|||
name?: string;
|
||||
mines?: unknown;
|
||||
published?: boolean;
|
||||
exposeEdgeMines?: boolean;
|
||||
}): Promise<ActionResult<undefined>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
|
|
@ -212,6 +219,9 @@ export async function updateMinefield(input: {
|
|||
if (input.published !== undefined) {
|
||||
data.published = Boolean(input.published);
|
||||
}
|
||||
if (input.exposeEdgeMines !== undefined) {
|
||||
data.exposeEdgeMines = Boolean(input.exposeEdgeMines);
|
||||
}
|
||||
if (input.mines !== undefined) {
|
||||
const mineCheck = validateMinePositions(input.mines, existing.rows, existing.cols);
|
||||
if (mineCheck.error) {
|
||||
|
|
@ -306,7 +316,7 @@ export async function submitCustomMinefieldScore(input: {
|
|||
seconds: number;
|
||||
flags: number;
|
||||
won: boolean;
|
||||
}): Promise<ActionResult<undefined>> {
|
||||
}): Promise<ActionResult<{ xpEarned: number }>> {
|
||||
try {
|
||||
const { payload, user } = await authenticate();
|
||||
|
||||
|
|
@ -337,6 +347,45 @@ export async function submitCustomMinefieldScore(input: {
|
|||
return { success: false, error: flagError };
|
||||
}
|
||||
|
||||
// Payouts: wins pay 10% of the equivalent standard run, the field's author
|
||||
// banks 5% of each payout, and authors earn nothing on their own fields.
|
||||
const isOwnPlay = minefield.author === (user.id as number);
|
||||
let playerXp = 0;
|
||||
let creatorXpCredited = 0;
|
||||
let creatorXpBank = minefield.creatorXpBank ?? 0;
|
||||
if (input.won && !isOwnPlay) {
|
||||
playerXp = customPlayerXp({
|
||||
mineCount: minefield.mineCount,
|
||||
seconds: input.seconds,
|
||||
won: true,
|
||||
});
|
||||
if (playerXp > 0) {
|
||||
const credited = await creditEodXp(payload, user.id as number, playerXp);
|
||||
if (credited) {
|
||||
broadcastToUser("profile-update", Number(user.id), {
|
||||
reason: "xp",
|
||||
xpGained: playerXp,
|
||||
profileId: credited.profileId,
|
||||
});
|
||||
}
|
||||
|
||||
const prevBank = creatorXpBank;
|
||||
const applied = applyCreatorBank(prevBank, playerXp);
|
||||
const authorId = typeof minefield.author === "number" ? minefield.author : null;
|
||||
const authorCredit =
|
||||
applied.wholeXpToCredit > 0 && authorId !== null
|
||||
? await creditEodXp(payload, authorId, applied.wholeXpToCredit)
|
||||
: null;
|
||||
if (authorCredit) {
|
||||
creatorXpCredited = applied.wholeXpToCredit;
|
||||
creatorXpBank = applied.bank;
|
||||
} else {
|
||||
// No profile (or nothing whole due): keep the full banked fraction.
|
||||
creatorXpBank = prevBank + applyCreatorBank(0, playerXp).bank;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await payload.create({
|
||||
collection: "custom-minefield-scores",
|
||||
data: {
|
||||
|
|
@ -352,7 +401,10 @@ export async function submitCustomMinefieldScore(input: {
|
|||
await payload.update({
|
||||
collection: "custom-minefields",
|
||||
id: minefield.id,
|
||||
data: { playCount: (minefield.playCount ?? 0) + 1 },
|
||||
data: {
|
||||
playCount: (minefield.playCount ?? 0) + 1,
|
||||
...(playerXp > 0 ? { creatorXpBank } : {}),
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
|
|
@ -360,7 +412,9 @@ export async function submitCustomMinefieldScore(input: {
|
|||
await emitGameEvent(payload, {
|
||||
type: EventTypes.MinigameCustomEodPlay,
|
||||
message: input.won
|
||||
? `Cleared custom minefield "${minefield.name}" in ${input.seconds}s`
|
||||
? `Cleared custom minefield "${minefield.name}" in ${input.seconds}s${
|
||||
playerXp > 0 ? ` (+${playerXp} XP)` : ""
|
||||
}`
|
||||
: `Died on custom minefield "${minefield.name}" after ${input.seconds}s`,
|
||||
actor: user.id as number,
|
||||
targetCollection: "custom-minefields",
|
||||
|
|
@ -369,10 +423,12 @@ export async function submitCustomMinefieldScore(input: {
|
|||
seconds: input.seconds,
|
||||
flags: input.flags,
|
||||
won: Boolean(input.won),
|
||||
xpEarned: playerXp,
|
||||
creatorXpCredited,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
return { success: true, data: { xpEarned: playerXp } };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
if (message === "Unauthorized") {
|
||||
|
|
|
|||
Loading…
Reference in a new issue