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 { 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") {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue