feat: Add scripts for mob dependency management and server setup
- Implemented `findMobDependencies.ts` to identify foreign key constraints referencing the Mob table and log dependent rows. - Created `fullServerSetup.ts` for idempotent server setup, including economy items, item recipes, game areas, mobs, and optional demo mob attacks. - Developed `removeInvalidMobsWithDeps.ts` to delete invalid mobs and their dependencies, backing up affected scheduled mob attacks. - Added unit tests in `testMobUnit.ts` and `mob.test.ts` for mob functionality, including stats computation and instance retrieval. - Introduced reward modification tests in `testRewardMods.ts` and `rewardMods.unit.ts` to validate drop selection and coin multiplier behavior. - Enhanced command handling for mob deletion in `mobDelete.ts` and setup examples in `setup.ts`, ensuring proper permissions and feedback. - Created utility functions in `testHelpers.ts` for deterministic drop selection from mob definitions.
This commit is contained in:
54
scripts/cleanInvalidMobs.ts
Normal file
54
scripts/cleanInvalidMobs.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { prisma } from "../src/core/database/prisma";
|
||||
import { BaseMobDefinitionSchema } from "../src/game/mobs/mobData";
|
||||
|
||||
async function run() {
|
||||
if (!process.env.XATA_DB) {
|
||||
console.error("XATA_DB not set — aborting");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Scanning mobs table for invalid definitions...");
|
||||
const rows: any[] = await (prisma as any).mob.findMany();
|
||||
const invalid: any[] = [];
|
||||
for (const r of rows) {
|
||||
const cfg =
|
||||
r.metadata ??
|
||||
r.stats ??
|
||||
r.drops ??
|
||||
r.config ??
|
||||
r.definition ??
|
||||
r.data ??
|
||||
null;
|
||||
try {
|
||||
BaseMobDefinitionSchema.parse(cfg as any);
|
||||
} catch (e) {
|
||||
invalid.push({ id: r.id, error: (e as any)?.errors ?? e, row: r });
|
||||
}
|
||||
}
|
||||
if (invalid.length === 0) {
|
||||
console.log("No invalid mob definitions found.");
|
||||
process.exit(0);
|
||||
}
|
||||
console.log(
|
||||
`Found ${invalid.length} invalid rows. Backing up and deleting...`
|
||||
);
|
||||
// backup
|
||||
console.log("Backup file: invalid_mobs_backup.json");
|
||||
require("fs").writeFileSync(
|
||||
"invalid_mobs_backup.json",
|
||||
JSON.stringify(invalid, null, 2)
|
||||
);
|
||||
for (const it of invalid) {
|
||||
try {
|
||||
await (prisma as any).mob.delete({ where: { id: it.id } });
|
||||
console.log("Deleted invalid mob id=", it.id);
|
||||
} catch (e) {
|
||||
console.warn("Failed to delete id=", it.id, e);
|
||||
}
|
||||
}
|
||||
console.log("Cleanup complete. Review invalid_mobs_backup.json");
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
80
scripts/findMobDependencies.ts
Normal file
80
scripts/findMobDependencies.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import fs from "fs";
|
||||
import { prisma } from "../src/core/database/prisma";
|
||||
|
||||
async function run() {
|
||||
if (!process.env.XATA_DB) {
|
||||
console.error("XATA_DB not set — aborting");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync("invalid_mobs_backup.json")) {
|
||||
console.error("invalid_mobs_backup.json not found — run cleanup first");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bak = JSON.parse(fs.readFileSync("invalid_mobs_backup.json", "utf8"));
|
||||
const ids: string[] = bak.map((b: any) => b.id).filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
console.log("No ids found in invalid_mobs_backup.json");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Looking for FK constraints that reference the Mob table...");
|
||||
const fkSql = `
|
||||
SELECT
|
||||
tc.constraint_name, kcu.table_name, kcu.column_name, ccu.table_name AS referenced_table, ccu.column_name AS referenced_column
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.constraint_schema = kcu.constraint_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.constraint_schema = tc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY' AND LOWER(ccu.table_name) = 'mob'
|
||||
`;
|
||||
|
||||
const refs: any[] = await (prisma as any).$queryRawUnsafe(fkSql);
|
||||
if (!refs || refs.length === 0) {
|
||||
console.log("No FK constraints found referencing mob table.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Found referencing constraints:");
|
||||
for (const r of refs) {
|
||||
console.log(
|
||||
` - ${r.table_name}.${r.column_name} -> ${r.referenced_table}.${r.referenced_column} (constraint ${r.constraint_name})`
|
||||
);
|
||||
}
|
||||
|
||||
// For each referencing table/column, search rows that use our ids
|
||||
for (const r of refs) {
|
||||
const table = r.table_name;
|
||||
const column = r.column_name;
|
||||
console.log(
|
||||
`\nChecking table ${table} (column ${column}) for dependent rows...`
|
||||
);
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const cntRes: any[] = await (prisma as any).$queryRawUnsafe(
|
||||
`SELECT COUNT(*) AS cnt FROM "${table}" WHERE "${column}" = '${id}'`
|
||||
);
|
||||
const cnt =
|
||||
cntRes && cntRes[0]
|
||||
? Number(cntRes[0].cnt || cntRes[0].count || 0)
|
||||
: 0;
|
||||
console.log(` mob id=${id} -> ${cnt} dependent row(s)`);
|
||||
if (cnt > 0) {
|
||||
const rows: any[] = await (prisma as any).$queryRawUnsafe(
|
||||
`SELECT * FROM "${table}" WHERE "${column}" = '${id}' LIMIT 5`
|
||||
);
|
||||
console.log(" Sample rows:", rows);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(" Failed to query", table, column, e?.message ?? e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\nDependency scan complete.");
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
393
scripts/fullServerSetup.ts
Normal file
393
scripts/fullServerSetup.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { prisma } from "../src/core/database/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* fullServerSetup.ts
|
||||
*
|
||||
* Script idempotente para poblar UN servidor con todo lo necesario:
|
||||
* - Economy items (herramientas, armas, materiales, cofres, pociones)
|
||||
* - Item recipes (crafteo)
|
||||
* - Item mutations (encantamientos)
|
||||
* - Game areas y niveles
|
||||
* - Mobs con drops
|
||||
* - Opcional: programar ataques de mobs demo
|
||||
*
|
||||
* Uso: provee GUILD_ID como variable de entorno opcional. Si no se provee, usa el id por defecto.
|
||||
* GUILD_ID=1316592320954630144 npx tsx scripts/fullServerSetup.ts
|
||||
*/
|
||||
|
||||
const DEFAULT_GUILD = process.env.GUILD_ID ?? "1316592320954630144";
|
||||
|
||||
async function upsertEconomyItem(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.EconomyItemUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
const existing = await prisma.economyItem.findFirst({
|
||||
where: { key, guildId },
|
||||
});
|
||||
if (existing)
|
||||
return prisma.economyItem.update({
|
||||
where: { id: existing.id },
|
||||
data: { ...data },
|
||||
});
|
||||
return prisma.economyItem.create({ data: { ...data, key, guildId } });
|
||||
}
|
||||
|
||||
async function upsertGameArea(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.GameAreaUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
const existing = await prisma.gameArea.findFirst({ where: { key, guildId } });
|
||||
if (existing)
|
||||
return prisma.gameArea.update({
|
||||
where: { id: existing.id },
|
||||
data: { ...data },
|
||||
});
|
||||
return prisma.gameArea.create({ data: { ...data, key, guildId } });
|
||||
}
|
||||
|
||||
async function upsertMob(
|
||||
guildId: string | null,
|
||||
key: string,
|
||||
data: Omit<Prisma.MobUncheckedCreateInput, "key" | "guildId">
|
||||
) {
|
||||
const existing = await prisma.mob.findFirst({ where: { key, guildId } });
|
||||
if (existing)
|
||||
return prisma.mob.update({ where: { id: existing.id }, data: { ...data } });
|
||||
return prisma.mob.create({ data: { ...data, key, guildId } });
|
||||
}
|
||||
|
||||
async function upsertItemRecipe(
|
||||
guildId: string | null,
|
||||
productKey: string,
|
||||
ingredients: { itemKey: string; qty: number }[],
|
||||
productQty = 1
|
||||
) {
|
||||
// Ensure product exists
|
||||
const product = await prisma.economyItem.findFirst({
|
||||
where: { key: productKey, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (!product) throw new Error(`Product item not found: ${productKey}`);
|
||||
|
||||
// Find existing recipe by productItemId
|
||||
const existing = await prisma.itemRecipe.findUnique({
|
||||
where: { productItemId: product.id },
|
||||
});
|
||||
if (existing) {
|
||||
// Recreate ingredients set
|
||||
await prisma.recipeIngredient.deleteMany({
|
||||
where: { recipeId: existing.id },
|
||||
});
|
||||
for (const ing of ingredients) {
|
||||
const it = await prisma.economyItem.findFirst({
|
||||
where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`);
|
||||
await prisma.recipeIngredient.create({
|
||||
data: { recipeId: existing.id, itemId: it.id, quantity: ing.qty },
|
||||
});
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
const r = await prisma.itemRecipe.create({
|
||||
data: { productItemId: product.id, productQuantity: productQty },
|
||||
});
|
||||
for (const ing of ingredients) {
|
||||
const it = await prisma.economyItem.findFirst({
|
||||
where: { key: ing.itemKey, OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (!it) throw new Error(`Ingredient item not found: ${ing.itemKey}`);
|
||||
await prisma.recipeIngredient.create({
|
||||
data: { recipeId: r.id, itemId: it.id, quantity: ing.qty },
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
export async function runFullServerSetup(
|
||||
guildIdArg?: string | null,
|
||||
options?: { dryRun?: boolean }
|
||||
) {
|
||||
const guildId = guildIdArg ?? DEFAULT_GUILD;
|
||||
console.log("Starting full server setup for guild=", guildId, options ?? {});
|
||||
|
||||
// --- Items: tools, weapons, materials ---
|
||||
await upsertEconomyItem(guildId, "tool.pickaxe.basic", {
|
||||
name: "Pico Básico",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: "pickaxe", tier: 1 },
|
||||
breakable: { enabled: true, maxDurability: 100, durabilityPerUse: 5 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["tool", "mine"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, "tool.pickaxe.iron", {
|
||||
name: "Pico de Hierro",
|
||||
stackable: false,
|
||||
props: {
|
||||
tool: { type: "pickaxe", tier: 2 },
|
||||
breakable: { enabled: true, maxDurability: 180, durabilityPerUse: 4 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["tool", "mine", "tier2"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, "weapon.sword.iron", {
|
||||
name: "Espada de Hierro",
|
||||
stackable: false,
|
||||
props: {
|
||||
damage: 10,
|
||||
tool: { type: "sword", tier: 1 },
|
||||
breakable: { enabled: true, maxDurability: 150, durabilityPerUse: 2 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["weapon"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, "armor.leather.basic", {
|
||||
name: "Armadura de Cuero",
|
||||
stackable: false,
|
||||
props: { defense: 3 } as unknown as Prisma.InputJsonValue,
|
||||
tags: ["armor"],
|
||||
});
|
||||
|
||||
await upsertEconomyItem(guildId, "ore.iron", {
|
||||
name: "Mineral de Hierro",
|
||||
stackable: true,
|
||||
props: { craftingOnly: true } as unknown as Prisma.InputJsonValue,
|
||||
tags: ["ore", "common"],
|
||||
});
|
||||
await upsertEconomyItem(guildId, "ingot.iron", {
|
||||
name: "Lingote de Hierro",
|
||||
stackable: true,
|
||||
props: {} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["ingot", "metal"],
|
||||
});
|
||||
|
||||
// Consumibles y pociones
|
||||
await upsertEconomyItem(guildId, "food.meat.small", {
|
||||
name: "Carne Pequeña",
|
||||
stackable: true,
|
||||
props: {
|
||||
food: { healHp: 8, cooldownSeconds: 20 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["food"],
|
||||
});
|
||||
await upsertEconomyItem(guildId, "potion.energy", {
|
||||
name: "Poción Energética",
|
||||
stackable: true,
|
||||
props: {
|
||||
potion: { removeEffects: ["FATIGUE"], cooldownSeconds: 90 },
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["potion", "utility"],
|
||||
});
|
||||
|
||||
// Cofre con recompensas
|
||||
await upsertEconomyItem(guildId, "chest.daily", {
|
||||
name: "Cofre Diario",
|
||||
stackable: true,
|
||||
props: {
|
||||
chest: {
|
||||
enabled: true,
|
||||
consumeOnOpen: true,
|
||||
randomMode: "single",
|
||||
rewards: [
|
||||
{ type: "coins", amount: 200 },
|
||||
{ type: "item", itemKey: "ingot.iron", qty: 2 },
|
||||
],
|
||||
},
|
||||
} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["chest"],
|
||||
});
|
||||
|
||||
// --- Mutations / enchants catalog ---
|
||||
// Item mutations (catalog)
|
||||
const existingRuby = await prisma.itemMutation.findFirst({
|
||||
where: { key: "ruby_core", guildId },
|
||||
});
|
||||
if (existingRuby) {
|
||||
await prisma.itemMutation.update({
|
||||
where: { id: existingRuby.id },
|
||||
data: { name: "Núcleo de Rubí", effects: { damageBonus: 15 } as any },
|
||||
});
|
||||
} else {
|
||||
await prisma.itemMutation.create({
|
||||
data: {
|
||||
key: "ruby_core",
|
||||
name: "Núcleo de Rubí",
|
||||
guildId,
|
||||
effects: { damageBonus: 15 } as any,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
const existingEmerald = await prisma.itemMutation.findFirst({
|
||||
where: { key: "emerald_core", guildId },
|
||||
});
|
||||
if (existingEmerald) {
|
||||
await prisma.itemMutation.update({
|
||||
where: { id: existingEmerald.id },
|
||||
data: {
|
||||
name: "Núcleo de Esmeralda",
|
||||
effects: { defenseBonus: 10, maxHpBonus: 20 } as any,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.itemMutation.create({
|
||||
data: {
|
||||
key: "emerald_core",
|
||||
name: "Núcleo de Esmeralda",
|
||||
guildId,
|
||||
effects: { defenseBonus: 10, maxHpBonus: 20 } as any,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Recipes (crafteo): iron_ingot <- iron ore x3
|
||||
// Create ingredient items if missing
|
||||
await upsertEconomyItem(guildId, "ingot.iron", {
|
||||
name: "Lingote de Hierro",
|
||||
stackable: true,
|
||||
props: {} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["ingot"],
|
||||
});
|
||||
await upsertEconomyItem(guildId, "ore.iron", {
|
||||
name: "Mineral de Hierro",
|
||||
stackable: true,
|
||||
props: {} as unknown as Prisma.InputJsonValue,
|
||||
tags: ["ore"],
|
||||
});
|
||||
await upsertItemRecipe(
|
||||
guildId,
|
||||
"ingot.iron",
|
||||
[{ itemKey: "ore.iron", qty: 3 }],
|
||||
1
|
||||
);
|
||||
|
||||
// --- Areas & Levels ---
|
||||
const mine = await upsertGameArea(guildId, "mine.cavern", {
|
||||
name: "Mina: Caverna",
|
||||
type: "MINE",
|
||||
config: { cooldownSeconds: 10 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: mine.id, level: 1 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: mine.id,
|
||||
level: 1,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "pickaxe", minTier: 1 },
|
||||
} as any,
|
||||
rewards: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ type: "item", itemKey: "ore.iron", qty: 2, weight: 70 },
|
||||
{ type: "coins", amount: 10, weight: 30 },
|
||||
],
|
||||
} as any,
|
||||
mobs: {
|
||||
draws: 1,
|
||||
table: [
|
||||
{ mobKey: "slime.green", weight: 50 },
|
||||
{ mobKey: "bat", weight: 50 },
|
||||
],
|
||||
} as any,
|
||||
},
|
||||
});
|
||||
|
||||
const lagoon = await upsertGameArea(guildId, "lagoon.shore", {
|
||||
name: "Laguna: Orilla",
|
||||
type: "LAGOON",
|
||||
config: { cooldownSeconds: 12 } as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
await prisma.gameAreaLevel.upsert({
|
||||
where: { areaId_level: { areaId: lagoon.id, level: 1 } },
|
||||
update: {},
|
||||
create: {
|
||||
areaId: lagoon.id,
|
||||
level: 1,
|
||||
requirements: {
|
||||
tool: { required: true, toolType: "rod", minTier: 1 },
|
||||
} as any,
|
||||
rewards: {
|
||||
draws: 2,
|
||||
table: [
|
||||
{ type: "item", itemKey: "food.meat.small", qty: 1, weight: 70 },
|
||||
{ type: "coins", amount: 10, weight: 30 },
|
||||
],
|
||||
} as any,
|
||||
mobs: { draws: 0, table: [] } as any,
|
||||
},
|
||||
});
|
||||
|
||||
// --- Basic mobs ---
|
||||
await upsertMob(guildId, "slime.green", {
|
||||
name: "Slime Verde",
|
||||
stats: { attack: 4, hp: 18 } as any,
|
||||
drops: [{ itemKey: "ingot.iron", qty: 1, weight: 10 }] as any,
|
||||
});
|
||||
await upsertMob(guildId, "bat", {
|
||||
name: "Murciélago",
|
||||
stats: { attack: 3, hp: 10 } as any,
|
||||
drops: Prisma.DbNull,
|
||||
});
|
||||
|
||||
// Advanced mobs
|
||||
await upsertMob(guildId, "goblin", {
|
||||
name: "Duende",
|
||||
stats: { attack: 8, hp: 30 } as any,
|
||||
drops: [{ itemKey: "ore.iron", qty: 1, weight: 50 }] as any,
|
||||
});
|
||||
await upsertMob(guildId, "orc", {
|
||||
name: "Orco",
|
||||
stats: { attack: 12, hp: 50 } as any,
|
||||
drops: Prisma.DbNull,
|
||||
});
|
||||
|
||||
// Programar un par de ataques demo (opcional)
|
||||
const targetUser = process.env.TARGET_USER ?? null;
|
||||
if (targetUser) {
|
||||
const slime = await prisma.mob.findFirst({
|
||||
where: { key: "slime.green", OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (slime) {
|
||||
const now = Date.now();
|
||||
await prisma.scheduledMobAttack.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: slime.id,
|
||||
scheduleAt: new Date(now + 5_000),
|
||||
},
|
||||
{
|
||||
userId: targetUser,
|
||||
guildId: guildId ?? "global",
|
||||
mobId: slime.id,
|
||||
scheduleAt: new Date(now + 15_000),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Full server setup complete.");
|
||||
}
|
||||
|
||||
// Backwards-compatible CLI entry
|
||||
if (require.main === module) {
|
||||
const gid = process.env.GUILD_ID ?? DEFAULT_GUILD;
|
||||
runFullServerSetup(gid)
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
72
scripts/removeInvalidMobsWithDeps.ts
Normal file
72
scripts/removeInvalidMobsWithDeps.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import fs from "fs";
|
||||
import { prisma } from "../src/core/database/prisma";
|
||||
|
||||
async function run() {
|
||||
if (!process.env.XATA_DB) {
|
||||
console.error("XATA_DB not set — aborting");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync("invalid_mobs_backup.json")) {
|
||||
console.error("invalid_mobs_backup.json not found — run cleanup first");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bak = JSON.parse(fs.readFileSync("invalid_mobs_backup.json", "utf8"));
|
||||
const ids: string[] = bak.map((b: any) => b.id).filter(Boolean);
|
||||
if (ids.length === 0) {
|
||||
console.log("No ids found in invalid_mobs_backup.json");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Backing up ScheduledMobAttack rows that reference these mob ids..."
|
||||
);
|
||||
try {
|
||||
const deps = await (prisma as any).scheduledMobAttack.findMany({
|
||||
where: { mobId: { in: ids } },
|
||||
});
|
||||
fs.writeFileSync(
|
||||
"scheduled_mob_attack_backup.json",
|
||||
JSON.stringify(deps, null, 2)
|
||||
);
|
||||
console.log(
|
||||
`Backed up ${deps.length} ScheduledMobAttack rows to scheduled_mob_attack_backup.json`
|
||||
);
|
||||
|
||||
if (deps.length > 0) {
|
||||
console.log(
|
||||
"Deleting ScheduledMobAttack rows referencing invalid mobs..."
|
||||
);
|
||||
const delRes = await (prisma as any).scheduledMobAttack.deleteMany({
|
||||
where: { mobId: { in: ids } },
|
||||
});
|
||||
console.log(`Deleted ${delRes.count || delRes} ScheduledMobAttack rows`);
|
||||
} else {
|
||||
console.log("No dependent ScheduledMobAttack rows to delete.");
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Failed to backup/delete dependent rows:", e?.message ?? e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Deleting invalid mob rows from Mob table...");
|
||||
try {
|
||||
const delMobs = await (prisma as any).mob.deleteMany({
|
||||
where: { id: { in: ids } },
|
||||
});
|
||||
console.log(`Deleted ${delMobs.count || delMobs} mob rows`);
|
||||
} catch (e: any) {
|
||||
console.error("Failed to delete mob rows:", e?.message ?? e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Done. Backups: invalid_mobs_backup.json, scheduled_mob_attack_backup.json"
|
||||
);
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
54
scripts/testMobUnit.ts
Normal file
54
scripts/testMobUnit.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import assert from "assert";
|
||||
import {
|
||||
computeMobStats,
|
||||
getMobInstance,
|
||||
MOB_DEFINITIONS,
|
||||
} from "../src/game/mobs/mobData";
|
||||
import { createOrUpdateMob } from "../src/game/mobs/admin";
|
||||
|
||||
async function run() {
|
||||
console.log("Running mob unit tests...");
|
||||
|
||||
// computeMobStats basic
|
||||
const def = MOB_DEFINITIONS[0];
|
||||
const statsLv1 = computeMobStats(def, 1);
|
||||
assert(typeof statsLv1.hp === "number", "hp should be number");
|
||||
assert(typeof statsLv1.attack === "number", "attack should be number");
|
||||
|
||||
// scaling test
|
||||
const statsLv5 = computeMobStats(def, 5);
|
||||
if ((def.scaling && def.scaling.hpPerLevel) || 0) {
|
||||
assert(statsLv5.hp >= statsLv1.hp, "hp should not decrease with level");
|
||||
}
|
||||
|
||||
console.log("computeMobStats: OK");
|
||||
|
||||
// getMobInstance basic
|
||||
const key = def.key;
|
||||
const inst = getMobInstance(key, 3);
|
||||
assert(inst !== null, "getMobInstance should return instance");
|
||||
assert(inst!.scaled.hp > 0, "instance hp > 0");
|
||||
console.log("getMobInstance: OK");
|
||||
|
||||
// createOrUpdateMob (non-DB mode should return def)
|
||||
try {
|
||||
const res = await createOrUpdateMob({
|
||||
...def,
|
||||
key: "unit.test.mob",
|
||||
} as any);
|
||||
if (!res || !res.def) throw new Error("createOrUpdateMob returned invalid");
|
||||
console.log("createOrUpdateMob: OK (no-DB mode)");
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"createOrUpdateMob: skipped (DB may be required) -",
|
||||
(e as any)?.message ?? e
|
||||
);
|
||||
}
|
||||
|
||||
console.log("All mob unit tests passed.");
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error("Tests failed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
66
scripts/testRewardMods.ts
Normal file
66
scripts/testRewardMods.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
initializeMobRepository,
|
||||
getMobInstance,
|
||||
listMobKeys,
|
||||
} from "../src/game/mobs/mobData";
|
||||
import { runMinigame } from "../src/game/minigames/service";
|
||||
import { prisma } from "../src/core/database/prisma";
|
||||
|
||||
async function run() {
|
||||
console.log("Initializing mob repository...");
|
||||
await initializeMobRepository();
|
||||
console.log("Available mob keys:", listMobKeys());
|
||||
|
||||
// Mock user/guild for smoke (these should exist in your test DB or the functions will create wallet entries etc.)
|
||||
const userId = "test-user";
|
||||
const guildId = "test-guild";
|
||||
|
||||
try {
|
||||
console.log("Ensuring minimal game area 'mine.cavern' exists...");
|
||||
// create minimal area and level if not present
|
||||
let area = await prisma.gameArea.findFirst({
|
||||
where: { key: "mine.cavern", OR: [{ guildId }, { guildId: null }] },
|
||||
orderBy: [{ guildId: "desc" }],
|
||||
});
|
||||
if (!area) {
|
||||
area = await prisma.gameArea.create({
|
||||
data: {
|
||||
key: "mine.cavern",
|
||||
guildId: null,
|
||||
name: "Cavern of Tests",
|
||||
type: "MINE",
|
||||
config: {},
|
||||
metadata: {},
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
let lvl = await prisma.gameAreaLevel.findFirst({
|
||||
where: { areaId: area.id, level: 1 },
|
||||
});
|
||||
if (!lvl) {
|
||||
lvl = await prisma.gameAreaLevel.create({
|
||||
data: {
|
||||
areaId: area.id,
|
||||
level: 1,
|
||||
requirements: {} as any,
|
||||
rewards: {
|
||||
draws: 1,
|
||||
table: [{ type: "coins", amount: 5, weight: 1 }],
|
||||
} as any,
|
||||
mobs: { draws: 0, table: [] } as any,
|
||||
} as any,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Running minigame mine.cavern level 1 as smoke test...");
|
||||
const res = await runMinigame(userId, guildId, "mine.cavern", 1);
|
||||
console.log("Minigame result:", JSON.stringify(res, null, 2));
|
||||
} catch (e) {
|
||||
console.error("runMinigame failed:", e);
|
||||
}
|
||||
}
|
||||
|
||||
run().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user