models/Track.ts
8.0 KB · sha256:20def99a59df4db706260a24520f5574733884a29af4985d08b9a9a96321d911
import "reflect-metadata";
import { Schema } from "mongoose";
import { field, model, getModel, unique, type Doc } from "../db/odm";
import {
findPlayerByUuid,
addGroup,
removeGroup,
getPlayerGroups,
} from "./Player";
export interface TransactionConfig {
handler: string;
amount?: number;
[arg: string]: unknown;
}
export interface TrackRung {
group: string;
requirements: string[];
costs: TransactionConfig[];
autoPromote: boolean;
}
const TransactionConfigSchema = new Schema<TransactionConfig>(
{
handler: { type: String, required: true, lowercase: true, trim: true },
amount: { type: Number, default: null },
},
{ _id: false, strict: false },
);
const TrackRungSchema = new Schema<TrackRung>(
{
group: { type: String, required: true, lowercase: true, trim: true },
requirements: { type: [String], default: () => [] },
costs: { type: [TransactionConfigSchema], default: () => [] },
autoPromote: { type: Boolean, default: false },
},
{ _id: false },
);
@model("Track")
export class Track {
@unique()
@field({ type: String, required: true, lowercase: true, trim: true })
name!: string;
@field({ type: [TrackRungSchema], default: () => [] })
rungs!: TrackRung[];
@field({ type: Number, default: null })
pollSeconds!: number | null;
@field({ type: [String], default: () => [] })
groups!: string[];
}
export type TrackDoc = Doc<Track>;
const TrackModel = getModel(Track);
function emptyRung(group: string): TrackRung {
return {
group: group.toLowerCase(),
requirements: [],
costs: [],
autoPromote: false,
};
}
async function migrateLegacy(doc: TrackDoc): Promise<TrackDoc> {
if ((!doc.rungs || doc.rungs.length === 0) && doc.groups && doc.groups.length > 0) {
doc.rungs = doc.groups.map(emptyRung);
doc.groups = [];
await doc.save();
}
return doc;
}
export async function findTrack(name: string): Promise<TrackDoc | null> {
const doc = await TrackModel.findOne({ name: name.toLowerCase() }).exec();
return doc ? migrateLegacy(doc) : null;
}
export async function listTracks(): Promise<TrackDoc[]> {
const docs = await TrackModel.find().sort({ name: 1 }).exec();
return Promise.all(docs.map(migrateLegacy));
}
export function listTrackNames(): Promise<string[]> {
return TrackModel.find()
.select("name")
.lean()
.exec()
.then((rows) => rows.map((r) => r.name));
}
export async function createTrack(
name: string,
groups: string[] = [],
): Promise<TrackDoc> {
const existing = await findTrack(name);
if (existing) throw new Error(`Track '${name}' already exists`);
return TrackModel.create({
name: name.toLowerCase(),
rungs: groups.map(emptyRung),
});
}
export async function deleteTrack(name: string): Promise<boolean> {
const res = await TrackModel.deleteOne({ name: name.toLowerCase() }).exec();
return res.deletedCount > 0;
}
export async function setTrackGroups(
name: string,
groups: string[],
): Promise<TrackDoc | null> {
return TrackModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{ rungs: groups.map(emptyRung), groups: [] },
{ new: true },
).exec();
}
export async function setTrackPollSeconds(
name: string,
seconds: number | null,
): Promise<TrackDoc | null> {
return TrackModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{ pollSeconds: seconds },
{ new: true },
).exec();
}
export async function appendToTrack(
name: string,
group: string,
): Promise<TrackDoc | null> {
return TrackModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{ $push: { rungs: emptyRung(group) } },
{ new: true },
).exec();
}
export async function insertIntoTrack(
name: string,
group: string,
position: number,
): Promise<TrackDoc | null> {
return TrackModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{
$push: { rungs: { $each: [emptyRung(group)], $position: position } },
},
{ new: true },
).exec();
}
export async function removeFromTrack(
name: string,
group: string,
): Promise<TrackDoc | null> {
return TrackModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{ $pull: { rungs: { group: group.toLowerCase() } } },
{ new: true },
).exec();
}
export async function updateRung(
name: string,
index: number,
patch: Partial<Omit<TrackRung, "group">>,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc) return null;
const rung = doc.rungs[index];
if (!rung) return null;
if (patch.requirements !== undefined) rung.requirements = patch.requirements;
if (patch.costs !== undefined) rung.costs = patch.costs;
if (patch.autoPromote !== undefined) rung.autoPromote = patch.autoPromote;
doc.markModified("rungs");
await doc.save();
return doc;
}
export async function addRungRequirement(
name: string,
index: number,
expression: string,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc?.rungs[index]) return null;
doc.rungs[index].requirements.push(expression);
doc.markModified("rungs");
await doc.save();
return doc;
}
export async function removeRungRequirement(
name: string,
index: number,
reqIndex: number,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc?.rungs[index]) return null;
if (reqIndex < 0 || reqIndex >= doc.rungs[index].requirements.length) return null;
doc.rungs[index].requirements.splice(reqIndex, 1);
doc.markModified("rungs");
await doc.save();
return doc;
}
export async function addRungCost(
name: string,
index: number,
cost: TransactionConfig,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc?.rungs[index]) return null;
doc.rungs[index].costs.push(cost);
doc.markModified("rungs");
await doc.save();
return doc;
}
export async function removeRungCost(
name: string,
index: number,
costIndex: number,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc?.rungs[index]) return null;
if (costIndex < 0 || costIndex >= doc.rungs[index].costs.length) return null;
doc.rungs[index].costs.splice(costIndex, 1);
doc.markModified("rungs");
await doc.save();
return doc;
}
export async function setRungAutoPromote(
name: string,
index: number,
on: boolean,
): Promise<TrackDoc | null> {
const doc = await findTrack(name);
if (!doc?.rungs[index]) return null;
doc.rungs[index].autoPromote = on;
doc.markModified("rungs");
await doc.save();
return doc;
}
export type LadderResult =
| { ok: true; from: string | null; to: string }
| { ok: false; reason: string };
export async function promote(
uuid: string,
trackName: string,
): Promise<LadderResult> {
const track = await findTrack(trackName);
if (!track || track.rungs.length === 0)
return { ok: false, reason: "track-empty-or-missing" };
const player = await findPlayerByUuid(uuid);
if (!player) return { ok: false, reason: "player-not-found" };
const current = await getPlayerGroups(uuid);
const idx = track.rungs.findIndex((r) => current.includes(r.group));
if (idx === -1) {
await addGroup(uuid, track.rungs[0].group);
return { ok: true, from: null, to: track.rungs[0].group };
}
if (idx >= track.rungs.length - 1)
return { ok: false, reason: "already-at-top" };
const from = track.rungs[idx].group;
const to = track.rungs[idx + 1].group;
await removeGroup(uuid, from);
await addGroup(uuid, to);
return { ok: true, from, to };
}
export async function demote(
uuid: string,
trackName: string,
): Promise<LadderResult> {
const track = await findTrack(trackName);
if (!track || track.rungs.length === 0)
return { ok: false, reason: "track-empty-or-missing" };
const current = await getPlayerGroups(uuid);
const idx = track.rungs.findIndex((r) => current.includes(r.group));
if (idx === -1) return { ok: false, reason: "not-on-track" };
const from = track.rungs[idx].group;
if (idx === 0) {
await removeGroup(uuid, from);
return { ok: true, from, to: "" };
}
const to = track.rungs[idx - 1].group;
await removeGroup(uuid, from);
await addGroup(uuid, to);
return { ok: true, from, to };
}
export { TrackModel };