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:
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user