feat: rewrite seed-dev-data for MariaDB with test users and campaign members

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aaron Wood 2026-04-11 00:20:15 -04:00
parent ec75add5b7
commit 39f8220eb7

View file

@ -1,210 +1,175 @@
import bcrypt from "bcrypt";
import type { ResultSetHeader, RowDataPacket } from "mysql2";
import db from "./db.js";
import { SEED_ITEMS } from "./seed-items.js";
import { SEED_TALENTS } from "./seed-talents.js";
export function seedDevData() {
const hasCampaigns = (
db.prepare("SELECT COUNT(*) as c FROM campaigns").get() as { c: number }
).c;
if (hasCampaigns > 0) return;
export async function seedDevData(): Promise<void> {
const [userRows] = await db.execute<RowDataPacket[]>(
"SELECT COUNT(*) as c FROM users"
);
if ((userRows[0] as { c: number }).c > 0) return;
const passwordHash = await bcrypt.hash("password", 12);
// Create DM user
const [dmResult] = await db.execute<ResultSetHeader>(
"INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)",
["dm@darkwatch.test", "DungeonMaster", passwordHash]
);
const dmId = dmResult.insertId;
// Create Player user
const [playerResult] = await db.execute<ResultSetHeader>(
"INSERT INTO users (email, username, password_hash) VALUES (?, ?, ?)",
["player@darkwatch.test", "Adventurer", passwordHash]
);
const playerId = playerResult.insertId;
// Create campaign
db.prepare(
"INSERT INTO campaigns (name) VALUES ('Tomb of the Serpent King')",
).run();
const [campaignResult] = await db.execute<ResultSetHeader>(
"INSERT INTO campaigns (name) VALUES (?)",
["Tomb of the Serpent King"]
);
const campaignId = campaignResult.insertId;
const insertChar = db.prepare(`
INSERT INTO characters (campaign_id, name, class, ancestry, level, xp, hp_current, hp_max, ac, alignment, title, background, deity, languages, gp, sp, cp, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
// Add DM as dm, player as player
await db.execute(
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'dm')",
[campaignId, dmId]
);
await db.execute(
"INSERT INTO campaign_members (campaign_id, user_id, role) VALUES (?, ?, 'player')",
[campaignId, playerId]
);
const insertStat = db.prepare(
// Create invite token for testing
await db.execute(
"INSERT INTO campaign_invites (campaign_id, token, created_by) VALUES (?, ?, ?)",
[campaignId, "dev-invite-token-abc123", dmId]
);
// Seed game_items from seed-items.ts
for (const item of SEED_ITEMS) {
await db.execute(
"INSERT INTO game_items (name, type, slot_count, effects, properties) VALUES (?, ?, ?, ?, ?)",
[
item.name,
item.type,
item.slot_count,
JSON.stringify(item.effects),
JSON.stringify(item.properties),
]
);
}
// Seed game_talents from seed-talents.ts
for (const talent of SEED_TALENTS) {
await db.execute(
"INSERT INTO game_talents (name, source, description, effect) VALUES (?, ?, ?, ?)",
[
talent.name,
talent.source,
talent.description,
JSON.stringify(talent.effect),
]
);
}
async function createCharacter(
charCampaignId: number,
userId: number,
data: {
name: string; class: string; ancestry: string; level: number; xp: number;
hp_current: number; hp_max: number; ac: number; alignment: string;
title: string; background: string; deity: string; languages: string;
gp: number; sp: number; cp: number; color: string;
}
): Promise<number> {
const [r] = await db.execute<ResultSetHeader>(
`INSERT INTO characters
(campaign_id, user_id, name, class, ancestry, level, xp, hp_current, hp_max,
ac, alignment, title, background, deity, languages, gp, sp, cp, color)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[charCampaignId, userId, data.name, data.class, data.ancestry, data.level, data.xp,
data.hp_current, data.hp_max, data.ac, data.alignment, data.title,
data.background, data.deity, data.languages, data.gp, data.sp, data.cp, data.color]
);
return r.insertId;
}
async function addStats(charId: number, stats: [string, number][]) {
for (const [name, value] of stats) {
await db.execute(
"INSERT INTO character_stats (character_id, stat_name, value) VALUES (?, ?, ?)",
[charId, name, value]
);
const insertGear = db.prepare(
"INSERT INTO character_gear (character_id, name, type, slot_count, properties, effects, game_item_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
);
const insertTalent = db.prepare(
"INSERT INTO character_talents (character_id, name, description, effect, game_talent_id) VALUES (?, ?, ?, ?, ?)",
);
// --- Limpie ---
const limpie = insertChar.run(
1,
"Limpie",
"Thief",
"Goblin",
1,
6,
1,
1,
10,
"Lawful",
"Footpad",
"Chirurgeo",
"Madeera the Covenant",
"Common, Goblin",
8,
0,
0,
"hsl(45, 60%, 65%)",
);
const limpieId = limpie.lastInsertRowid;
// Limpie stats
const limpieStats: [string, number][] = [
["STR", 11],
["DEX", 11],
["CON", 8],
["INT", 10],
["WIS", 14],
["CHA", 10],
];
for (const [name, value] of limpieStats) {
insertStat.run(limpieId, name, value);
}
}
// Limpie gear
insertGear.run(
limpieId,
"Shortsword",
"weapon",
1,
"{}",
'{"damage":"1d6","melee":true,"stat":"STR"}',
null,
async function addGear(charId: number, items: { name: string; type: string; slot_count: number; properties: string; effects: string }[]) {
for (const item of items) {
await db.execute(
"INSERT INTO character_gear (character_id, name, type, slot_count, properties, effects) VALUES (?, ?, ?, ?, ?, ?)",
[charId, item.name, item.type, item.slot_count, item.properties, item.effects]
);
insertGear.run(
limpieId,
"Shortbow",
"weapon",
1,
"{}",
'{"damage":"1d4","ranged":true,"stat":"DEX","two_handed":true}',
null,
);
insertGear.run(
limpieId,
"Leather Armor",
"armor",
1,
"{}",
'{"ac_base":11,"ac_dex":true}',
null,
);
insertGear.run(limpieId, "Shield", "armor", 1, "{}", '{"ac_bonus":2}', null);
insertGear.run(limpieId, "Arrows (20)", "gear", 1, "{}", "{}", null);
insertGear.run(limpieId, "Torch", "gear", 1, "{}", "{}", null);
insertGear.run(limpieId, "Rations", "gear", 1, "{}", "{}", null);
insertGear.run(limpieId, "Rope (60ft)", "gear", 1, "{}", "{}", null);
insertGear.run(limpieId, "Thieves' Tools", "gear", 1, "{}", "{}", null);
// Limpie talents
insertTalent.run(
limpieId,
"Backstab",
"Extra 1 + half level (round down) weapon dice of damage with surprise attacks",
'{"damage_bonus_surprise":true}',
null,
);
insertTalent.run(
limpieId,
"Thievery",
"Trained in climbing, sneaking, hiding, disguise, finding & disabling traps, delicate tasks",
"{}",
null,
);
insertTalent.run(
limpieId,
"Keen Senses",
"Can't be surprised",
'{"immune_surprise":true}',
null,
);
// --- Brynn ---
const brynn = insertChar.run(
1,
"Brynn",
"Fighter",
"Human",
2,
15,
8,
10,
10,
"Neutral",
"the Bold",
"Soldier",
"Gede",
"Common",
25,
5,
10,
"hsl(200, 60%, 65%)",
);
const brynnId = brynn.lastInsertRowid;
// Brynn stats
const brynnStats: [string, number][] = [
["STR", 16],
["DEX", 12],
["CON", 14],
["INT", 8],
["WIS", 10],
["CHA", 11],
];
for (const [name, value] of brynnStats) {
insertStat.run(brynnId, name, value);
}
}
// Brynn gear
insertGear.run(
brynnId,
"Longsword",
"weapon",
1,
"{}",
'{"damage":"1d8","melee":true,"stat":"STR"}',
null,
async function addTalents(charId: number, talents: { name: string; description: string; effect: string }[]) {
for (const t of talents) {
await db.execute(
"INSERT INTO character_talents (character_id, name, description, effect) VALUES (?, ?, ?, ?)",
[charId, t.name, t.description, t.effect]
);
insertGear.run(
brynnId,
"Chainmail",
"armor",
1,
"{}",
'{"ac_base":13,"ac_dex":true}',
null,
);
insertGear.run(brynnId, "Shield", "armor", 1, "{}", '{"ac_bonus":2}', null);
insertGear.run(
brynnId,
"Javelin",
"weapon",
1,
"{}",
'{"damage":"1d4","melee":true,"stat":"STR","thrown":true,"range":"far"}',
null,
);
insertGear.run(brynnId, "Torch", "gear", 1, "{}", "{}", null);
insertGear.run(brynnId, "Rations", "gear", 1, "{}", "{}", null);
// Brynn talents
insertTalent.run(
brynnId,
"Weapon Mastery",
"+1 to attack and damage with longswords",
'{"attack_bonus":1,"damage_bonus":1}',
null,
);
insertTalent.run(
brynnId,
"Grit",
"+2 HP and +1 HP each level",
'{"hp_bonus":2,"hp_per_level":1}',
null,
);
console.log("Dev data seeded: campaign + 2 characters (Limpie & Brynn)");
}
}
// --- Limpie (player's character 1) ---
const limpieId = await createCharacter(campaignId, playerId, {
name: "Limpie", class: "Thief", ancestry: "Goblin", level: 1, xp: 6,
hp_current: 1, hp_max: 1, ac: 10, alignment: "Lawful", title: "Footpad",
background: "Chirurgeo", deity: "Madeera the Covenant",
languages: "Common, Goblin", gp: 8, sp: 0, cp: 0, color: "hsl(45, 60%, 65%)",
});
await addStats(limpieId, [["STR",11],["DEX",11],["CON",8],["INT",10],["WIS",14],["CHA",10]]);
await addGear(limpieId, [
{ name: "Shortsword", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d6","melee":true,"stat":"STR"}' },
{ name: "Shortbow", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d4","ranged":true,"stat":"DEX","two_handed":true}' },
{ name: "Leather Armor", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_base":11,"ac_dex":true}' },
{ name: "Shield", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_bonus":2}' },
{ name: "Arrows (20)", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
{ name: "Torch", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
{ name: "Rations", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
{ name: "Rope (60ft)", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
{ name: "Thieves' Tools", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
]);
await addTalents(limpieId, [
{ name: "Backstab", description: "Extra 1 + half level (round down) weapon dice of damage with surprise attacks", effect: '{"damage_bonus_surprise":true}' },
{ name: "Thievery", description: "Trained in climbing, sneaking, hiding, disguise, finding & disabling traps, delicate tasks", effect: "{}" },
{ name: "Keen Senses", description: "Can't be surprised", effect: '{"immune_surprise":true}' },
]);
// --- Brynn (player's character 2) ---
const brynnId = await createCharacter(campaignId, playerId, {
name: "Brynn", class: "Fighter", ancestry: "Human", level: 2, xp: 15,
hp_current: 8, hp_max: 10, ac: 10, alignment: "Neutral", title: "the Bold",
background: "Soldier", deity: "Gede", languages: "Common",
gp: 25, sp: 5, cp: 10, color: "hsl(200, 60%, 65%)",
});
await addStats(brynnId, [["STR",16],["DEX",12],["CON",14],["INT",8],["WIS",10],["CHA",11]]);
await addGear(brynnId, [
{ name: "Longsword", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d8","melee":true,"stat":"STR"}' },
{ name: "Chainmail", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_base":13,"ac_dex":true}' },
{ name: "Shield", type: "armor", slot_count: 1, properties: "{}", effects: '{"ac_bonus":2}' },
{ name: "Javelin", type: "weapon", slot_count: 1, properties: "{}", effects: '{"damage":"1d4","melee":true,"stat":"STR","thrown":true,"range":"far"}' },
{ name: "Torch", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
{ name: "Rations", type: "gear", slot_count: 1, properties: "{}", effects: "{}" },
]);
await addTalents(brynnId, [
{ name: "Weapon Mastery", description: "+1 to attack and damage with longswords", effect: '{"attack_bonus":1,"damage_bonus":1}' },
{ name: "Grit", description: "+2 HP and +1 HP each level", effect: '{"hp_bonus":2,"hp_per_level":1}' },
]);
console.log("Dev data seeded: 2 users, 1 campaign, 2 characters (Limpie & Brynn)");
}