Back to Black Site
SOURCE / PINNED RELEASE

Made of little things.

Black Site

Release
666bc7c74ebd…
Author-recorded commit
05e95af79676…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391

Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/armory.ts
import { armoryCatalog } from 'virtual:black-site-catalog';

export type Armory = {
  unlockedThrowables: string[];
  ownedArmors: string[];
  weaponLevels: Record<string, number>;
  armorLevels: Record<string, number>;
};
export type GameProfile = Record<string, unknown>;
export const emptyArmory = (): Armory => ({ unlockedThrowables: [], ownedArmors: [], weaponLevels: {}, armorLevels: {} });

function object(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
      ![Object.prototype, null].includes(Object.getPrototypeOf(value))) throw new Error('armory-invalid');
  return value as Record<string, unknown>;
}
function ids(value: unknown, catalog: string[]): string[] {
  if (!Array.isArray(value) || value.length > catalog.length || value.some(id => typeof id !== 'string' || !catalog.includes(id)) ||
      new Set(value).size !== value.length) throw new Error('armory-invalid');
  return [...value].sort();
}
function levels(value: unknown, catalog: string[], maximum: number): Record<string, number> {
  const result: Record<string, number> = {};
  for (const [id, level] of Object.entries(object(value)).sort(([a], [b]) => a.localeCompare(b))) {
    if (!catalog.includes(id) || typeof level !== 'number' || !Number.isSafeInteger(level) || level < 0 || level > maximum) {
      throw new Error('armory-invalid');
    }
    if (level) result[id] = level;
  }
  return result;
}
export function validateArmory(value: unknown): Armory {
  const record = object(value);
  if (Object.keys(record).sort().join(',') !== 'armorLevels,ownedArmors,unlockedThrowables,weaponLevels') throw new Error('armory-invalid');
  const result = {
    unlockedThrowables: ids(record.unlockedThrowables, armoryCatalog.throwables),
    ownedArmors: ids(record.ownedArmors, armoryCatalog.armors),
    weaponLevels: levels(record.weaponLevels, armoryCatalog.weapons, armoryCatalog.maxWeaponLevel),
    armorLevels: levels(record.armorLevels, armoryCatalog.armors, armoryCatalog.maxArmorLevel),
  };
  if (Object.keys(result.armorLevels).some(id => !result.ownedArmors.includes(id))) throw new Error('armory-invalid');
  return result;
}
export function profileArmory(value: unknown): Armory {
  const profile = object(value);
  return validateArmory({
    unlockedThrowables: profile.unlocked ?? [], ownedArmors: profile.armours ?? [],
    weaponLevels: profile.levels ?? {}, armorLevels: profile.armourLevels ?? {},
  });
}
export function mergeArmory(left: Armory, right: Armory): Armory {
  const max = (a: Record<string, number>, b: Record<string, number>) => Object.fromEntries(
    [...new Set([...Object.keys(a), ...Object.keys(b)])].sort().map(id => [id, Math.max(a[id] ?? 0, b[id] ?? 0)]),
  );
  return validateArmory({
    unlockedThrowables: [...new Set([...left.unlockedThrowables, ...right.unlockedThrowables])],
    ownedArmors: [...new Set([...left.ownedArmors, ...right.ownedArmors])],
    weaponLevels: max(left.weaponLevels, right.weaponLevels), armorLevels: max(left.armorLevels, right.armorLevels),
  });
}
export const armoryDigest = (value: Armory): string => JSON.stringify(validateArmory(value));
export function withArmory(profile: GameProfile, armory: Armory): GameProfile {
  const valid = validateArmory(armory);
  return { ...profile, unlocked: valid.unlockedThrowables, armours: valid.ownedArmors,
    levels: valid.weaponLevels, armourLevels: valid.armorLevels };
}