1
0
Fork 0

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:
Jason Fraley 2026-09-08 15:57:01 -04:00
parent 243bb8ae0d
commit efda519569

View file

@ -5,7 +5,11 @@ import { getPayload } from "payload";
import { emitGameEvent } from "@/utils/event-log/emit"; import { emitGameEvent } from "@/utils/event-log/emit";
import { EventTypes } from "@/utils/event-log/eventTypes"; import { EventTypes } from "@/utils/event-log/eventTypes";
import { hasPermission } from "@/utils/access-control/hasPermission"; import { hasPermission } from "@/utils/access-control/hasPermission";
import { broadcastToUser } from "@/lib/realtime/bus";
import { creditEodXp } from "@/lib/custom-minigames/payouts";
import { import {
applyCreatorBank,
customPlayerXp,
isEodUnlocked, isEodUnlocked,
minefieldFlagsError, minefieldFlagsError,
validateGridSize, validateGridSize,
@ -120,6 +124,7 @@ export async function createMinefield(input: {
rows: number; rows: number;
cols: number; cols: number;
mines: unknown; mines: unknown;
exposeEdgeMines?: boolean;
}): Promise<ActionResult<{ id: number }>> { }): Promise<ActionResult<{ id: number }>> {
try { try {
const { payload, user } = await authenticate(); const { payload, user } = await authenticate();
@ -149,6 +154,7 @@ export async function createMinefield(input: {
mines: mineCheck.positions, mines: mineCheck.positions,
mineCount: mineCheck.positions.length, mineCount: mineCheck.positions.length,
published: true, published: true,
exposeEdgeMines: input.exposeEdgeMines ?? true,
playCount: 0, playCount: 0,
}, },
depth: 0, depth: 0,
@ -177,6 +183,7 @@ export async function updateMinefield(input: {
name?: string; name?: string;
mines?: unknown; mines?: unknown;
published?: boolean; published?: boolean;
exposeEdgeMines?: boolean;
}): Promise<ActionResult<undefined>> { }): Promise<ActionResult<undefined>> {
try { try {
const { payload, user } = await authenticate(); const { payload, user } = await authenticate();
@ -212,6 +219,9 @@ export async function updateMinefield(input: {
if (input.published !== undefined) { if (input.published !== undefined) {
data.published = Boolean(input.published); data.published = Boolean(input.published);
} }
if (input.exposeEdgeMines !== undefined) {
data.exposeEdgeMines = Boolean(input.exposeEdgeMines);
}
if (input.mines !== undefined) { if (input.mines !== undefined) {
const mineCheck = validateMinePositions(input.mines, existing.rows, existing.cols); const mineCheck = validateMinePositions(input.mines, existing.rows, existing.cols);
if (mineCheck.error) { if (mineCheck.error) {
@ -306,7 +316,7 @@ export async function submitCustomMinefieldScore(input: {
seconds: number; seconds: number;
flags: number; flags: number;
won: boolean; won: boolean;
}): Promise<ActionResult<undefined>> { }): Promise<ActionResult<{ xpEarned: number }>> {
try { try {
const { payload, user } = await authenticate(); const { payload, user } = await authenticate();
@ -337,6 +347,45 @@ export async function submitCustomMinefieldScore(input: {
return { success: false, error: flagError }; 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({ await payload.create({
collection: "custom-minefield-scores", collection: "custom-minefield-scores",
data: { data: {
@ -352,7 +401,10 @@ export async function submitCustomMinefieldScore(input: {
await payload.update({ await payload.update({
collection: "custom-minefields", collection: "custom-minefields",
id: minefield.id, id: minefield.id,
data: { playCount: (minefield.playCount ?? 0) + 1 }, data: {
playCount: (minefield.playCount ?? 0) + 1,
...(playerXp > 0 ? { creatorXpBank } : {}),
},
overrideAccess: true, overrideAccess: true,
depth: 0, depth: 0,
}); });
@ -360,7 +412,9 @@ export async function submitCustomMinefieldScore(input: {
await emitGameEvent(payload, { await emitGameEvent(payload, {
type: EventTypes.MinigameCustomEodPlay, type: EventTypes.MinigameCustomEodPlay,
message: input.won 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`, : `Died on custom minefield "${minefield.name}" after ${input.seconds}s`,
actor: user.id as number, actor: user.id as number,
targetCollection: "custom-minefields", targetCollection: "custom-minefields",
@ -369,10 +423,12 @@ export async function submitCustomMinefieldScore(input: {
seconds: input.seconds, seconds: input.seconds,
flags: input.flags, flags: input.flags,
won: Boolean(input.won), won: Boolean(input.won),
xpEarned: playerXp,
creatorXpCredited,
}, },
}); });
return { success: true }; return { success: true, data: { xpEarned: playerXp } };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Unknown error"; const message = error instanceof Error ? error.message : "Unknown error";
if (message === "Unauthorized") { if (message === "Unauthorized") {