/* SPDX-FileCopyrightText: © 2025 Hornwitser SPDX-License-Identifier: AGPL-3.0-or-later */ import type { ApiAccount, ApiSchedule, ApiScheduleEventSlot, ApiScheduleShiftSlot } from "~/shared/types/api"; import { toId } from "~/shared/utils/functions"; const locations = [ { id: 1, name: "Stage", description: "Inside the main building.", updatedAt: "d-1 18:21", }, { id: 2, name: "Clubhouse", description: "That big red building in the middle of the park.", updatedAt: "d-1 18:25", }, { id: 4, name: "Summerhouse", description: "Next to the campfire by the lake", updatedAt: "d-1 18:22", }, { id: 6, name: "Campfire", description: "Next to the big tree by the lake.", updatedAt: "d-1 18:41", }, { id: 7, name: "Outside", description: "Takes place somewhere outside.", updatedAt: "d-1 18:37", } ] let slotId = 1; let eventId = 1; const events = [ { name: "Arcade", description: "Play retro games!", slots: [ "d1 12:00 4h clubhouse", "d2 12:00 4h clubhouse", "d3 12:00 4h clubhouse", ], }, { name: "Arcade Setup", crew: true, slots: ["d1 11:00 1h clubhouse"], }, { name: "Bonfire Stories", description: "Share your stories as we sit cosily around the campfire.", slots: ["d2 18:00 2h campfire"], }, { name: "Stage Rigging", crew: true, slots: ["d1 11:00 7h30m stage"]}, { name: "Reconfigure for DJ", crew: true, slots: [ "d2 19:00 1h stage", "d3 18:45 1h15m stage", ], }, { name: "DJ Alpha", slots: ["d2 20:00 2h stage"] }, { name: "DJ Bravo", slots: ["d3 20:00 2h stage"] }, { name: "DJ Charlie", slots: ["d3 22:00 2h stage"] }, { name: "Prepare Fursuit Games", crew: true, slots: ["d4 17:00 1h clubhouse"] }, { name: "Fursuit Games", description: "Playful time for the suiters.", slots: ["d4 18:00 2h clubhouse"], }, { name: "Fishing Trip", slots: ["d3 12:00 3h30m outside"]}, { name: "Opening", slots: ["d1 18:30 1h30m stage"]}, { name: "Closing", slots: ["d5 10:00 1h stage"]}, { name: "Stage Teardown", crew: true, slots: ["d5 11:00 4h stage"]}, { name: "Setup Board Games", crew: true, slots: ["d1 11:30 30m summerhouse"]}, { name: "Board Games", slots: [ "d1 12:00 4h summerhouse", "d2 12:00 4h summerhouse", "d3 12:00 4h summerhouse", "d4 12:00 4h summerhouse", ], }, { name: "Teardown Board Games", crew: true, slots: ["d4 16:00 30m summerhouse"]}, { name: "📷meet", slots: ["d3 19:00 1h10m summerhouse"]}, { name: "Prepare Karaoke", crew: true, slots: [ "d3 20:00 1h clubhouse", "d4 20:00 1h clubhouse", ], }, { name: "Karaoke", slots: [ "d3 21:00 2h clubhouse", "d4 21:00 2h clubhouse", ], }, { name: "Dance", slots: ["d1 20:00 3h stage"]}, { name: "Prepare Charity Auction", crew: true, slots: ["d4 10:00 3h stage"]}, { name: "Charity Auction", slots: ["d4 13:00 2h stage"]}, { name: "Tournament Preparation", crew: true, slots: ["d2 15:00 2h stage"]}, { name: "Tournament", slots: ["d2 17:00 2h stage"]}, { name: "Canoe Trip", slots: ["d4 11:00 4h30m outside"]}, { name: "Dinner Preparations", crew: true, slots: [ "d1 14:00 2h campfire", "d2 14:00 2h campfire", "d3 14:00 2h campfire", "d4 14:00 2h campfire", ], }, { name: "Dinner", slots: [ "d1 16:00 1h campfire", "d2 16:00 1h campfire", "d3 16:00 1h campfire", "d4 16:00 1h campfire", ], }, { name: "Dinner Cleanup", crew: true, slots: [ "d1 17:00 1h campfire", "d2 17:00 1h campfire", "d3 17:00 1h campfire", "d4 17:00 1h campfire", ], }, { name: "Prepare Film Night", crew: true, slots: ["d4 19:00 2h stage"]}, { name: "Film Night", slots: ["d4 21:00 2h stage"]}, { name: "Prepare Group Photo", crew: true, slots: ["d3 17:00 1h stage"]}, { name: "Group Photo", slots: ["d3 18:00 45m stage"]}, { name: "Setup Artist Alley", crew: true, slots: ["d4 10:00 2h clubhouse"]}, { name: "Artist Alley", slots: ["d4 12:00 4h clubhouse"]}, { name: "Teardown Artist Alley", crew: true, slots: ["d4 16:00 1h clubhouse"]}, { name: "Feedback Panel", slots: ["d5 12:00 1h clubhouse"]}, ].map(({ slots, ...rest }) => ({ ...rest, id: eventId++, slots: slots.map(slot => `${slot} ${slotId++}`), })); const idMedic = 1; const idSecurity = 2; const roles = [ { id: idMedic, name: "Medic", updatedAt: "d-2 12:34" }, { id: idSecurity, name: "Security", updatedAt: "d-2 12:39" }, ] const shifts = [ { name: "Medic Early", roleId: idMedic, slots: [ "d1 12:00 4h", "d2 12:00 4h", "d3 12:00 4h", "d4 11:00 5h", "d5 10:00 3h", ], }, { name: "Medic Late", roleId: idMedic, slots: [ "d1 16:00 7h", "d2 16:00 6h", "d3 16:00 8h", "d4 16:00 7h", ], }, { name: "Security Early", roleId: idSecurity, slots: [ "d1 12:00 6h", "d2 12:00 6h", "d3 12:00 6h", "d4 11:00 7h", "d5 10:00 3h", ], }, { name: "Security Late", roleId: idSecurity, slots: [ "d1 18:00 5h", "d2 18:00 4h", "d3 18:00 6h", "d4 18:00 5h", ], }, ].map(({ slots, ...rest }) => ({ ...rest, id: eventId++, slots: slots.map(slot => `${slot} ${slotId++}`), })); function toIso(date: Date) { return date.toISOString().replace(":00.000Z", "Z"); } function toDate(origin: Date, day: string, start: string) { const [startHours, startMinutes] = start.split(":").map(time => parseInt(time, 10)); const dayNumber = parseInt(day.slice(1)); const startDate = new Date(origin); startDate.setUTCDate(startDate.getUTCDate() + dayNumber); startDate.setUTCHours(startDate.getUTCHours() + startHours); startDate.setUTCMinutes(startDate.getUTCMinutes() + startMinutes); return startDate; } function toDates(origin: Date, day: string, start: string, duration: string) { const startDate = toDate(origin, day, start); const [_, durationHours, durationMinutes] = /(?:(\d+)h)?(?:(\d+)m)?/.exec(duration)!; const durationTotal = parseInt(durationHours ?? "0") * 60 + parseInt(durationMinutes ?? "0") const endDate = new Date(startDate.getTime() + durationTotal * 60e3); return [startDate, endDate]; } function toSlot(origin: Date, shorthand: string, counts: Map, idToAssigned: Map): ApiScheduleEventSlot { const [day, start, duration, location, idStr] = shorthand.split(" "); const [startDate, endDate] = toDates(origin, day, start, duration); const id = parseInt(idStr, 10); return { id, start: toIso(startDate), end: toIso(endDate), locationIds: [locations.find(l => toId(l.name) === location)!.id], assigned: idToAssigned.get(id), interested: counts.get(id), }; } function toShift(origin: Date, shorthand: string, idToAssigned: Map): ApiScheduleShiftSlot { const [day, start, duration, idStr] = shorthand.split(" "); const [startDate, endDate] = toDates(origin, day, start, duration); const id = parseInt(idStr, 10); return { id, start: toIso(startDate), end: toIso(endDate), assigned: idToAssigned.get(id), }; } function getDemoOrigin() { const origin = new Date(); const utcOffset = 1; origin.setUTCDate(origin.getUTCDate() - 1); // Day 0 is yesterday origin.setUTCHours(-utcOffset); origin.setUTCMinutes(0); origin.setUTCSeconds(0); origin.setUTCMilliseconds(0); return origin; } export function generateDemoSchedule(): ApiSchedule { const origin = getDemoOrigin(); const eventCounts = new Map() const slotCounts = new Map() const accounts = generateDemoAccounts(origin); for (const account of accounts) { for (const id of account.interestedEventIds ?? []) { eventCounts.set(id, (eventCounts.get(id) ?? 0) + 1); } for (const id of account.interestedEventSlotIds ?? []) { slotCounts.set(id, (slotCounts.get(id) ?? 0) + 1); } } function assignSlots(events: { id: number, slots: string[] }[], count: number) { const idToAssigned = new Map(); for (const account of accounts.filter(a => a.type === "crew" || a.type === "admin")) { const assignedIds = new Set; const usedEvents = new Set; const slotsToAdd = Math.floor(random() * count); while (assignedIds.size < slotsToAdd) { const event = events[Math.floor(random() * events.length)]; if (usedEvents.has(event.id)) { continue; } if (event.slots.length === 1 || random() < 0.8) { for (const slot of event.slots) { const id = parseInt(slot.split(" ").slice(-1)[0]); assignedIds.add(id); usedEvents.add(event.id); } } else { for (const slot of event.slots) { if (random() < 0.5) { const id = parseInt(slot.split(" ").slice(-1)[0]); assignedIds.add(id); usedEvents.add(event.id); } } } } for (const id of assignedIds) { const assigned = idToAssigned.get(id); if (assigned) { assigned.push(account.id); } else { idToAssigned.set(id, [account.id]); } } } return idToAssigned; } seed = 2; const eventSlotIdToAssigned = assignSlots(events, 20); seed = 5; const shiftSlotIdToAssigned = assignSlots(shifts, 3); return { id: 111, updatedAt: toIso(toDate(origin, "d-2", "10:01")), events: events.map( ({ id, name, crew, description, slots }) => ({ id, name, crew, description, interested: eventCounts.get(id), slots: slots.map(shorthand => toSlot(origin, shorthand, slotCounts, eventSlotIdToAssigned)), updatedAt: toIso(toDate(origin, "d-1", "15:11")), }) ), locations: locations.map( ({ id, name, description, updatedAt }) => ({ id, name, description, updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])), }) ), roles: roles.map( ({ id, name, updatedAt }) => ({ id, name, updatedAt: toIso(toDate(origin, ...(updatedAt.split(" ")) as [string, string])), }) ), shifts: shifts.map( ({ id, name, roleId, slots }) => ({ id, name, roleId, slots: slots.map(shorthand => toShift(origin, shorthand, shiftSlotIdToAssigned)), updatedAt: toIso(toDate(origin, "d-1", "13:23")), }) ) }; } const names = [ "Leo", "Lisa", "Jack", "Emily", "Roy", "Sofia", "Adam", "Eve", "Max", "Rose", "Hugo", "Maria", "David", "Zoe", "Hunter", "Ria", "Sonny", "Amy", "Kai", "Megan", "Toby", "Katie", "Bob", "Lucy", ]; // MINSTD random implementation for reproducible random numbers. let seed = 1; function random() { const a = 48271; const c = 0; const m = 2 ** 31 -1; return (seed = (a * seed + c) % m | 0) / 2 ** 31; } export function generateDemoAccounts(origin = getDemoOrigin()): ApiAccount[] { seed = 1; const accounts: ApiAccount[] = []; for (const name of names) { accounts.push({ id: accounts.length, updatedAt: toIso(toDate(origin, "d-1", "10:41")), name, type: (["regular", "crew", "crew", "crew", "admin"] as const)[Math.floor(random() ** 3 * 5)], }); } seed = 1; // These have a much higher probability of being in someone's interested list. const desiredEvent = ["opening", "closing", "fursuit-games"].map( id => events.find(e => toId(e.name) === id)!.id ); const nonCrewEvents = events.filter(event => !event.crew); for (const account of accounts) { const interestedEventIds = new Set; const interestedSlotIds = new Set; const usedEvents = new Set; for (const id of desiredEvent) { if (random() < 0.5) { interestedEventIds.add(id); } } const eventsToAdd = Math.floor(random() * 10); while (interestedEventIds.size + interestedSlotIds.size < eventsToAdd) { const event = nonCrewEvents[Math.floor(random() * nonCrewEvents.length)]; if (usedEvents.has(event.id)) { continue; } if (event.slots.length === 1 || random() < 0.8) { interestedEventIds.add(event.id) } else { for (const slot of event.slots) { if (random() < 0.5) { const id = parseInt(slot.split(" ")[4], 10); interestedSlotIds.add(id); } } } } if (interestedEventIds.size) { account.interestedEventIds = [...interestedEventIds]; } if (interestedSlotIds.size) { account.interestedEventSlotIds = [...interestedSlotIds]; } } return accounts; }