Something went wrong. Try again.
A Social Deduction / Deception Game
Something went wrong. Try again.
4.5 kB · 117 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118// Mint a `town.deception.resource` record: upload a sprite image and write the record carrying its// display name + plural, then print the new record's AT-URI. That AT-URI is what you paste into a// world.tilemap.map station marker (`town.deception.collect` / `deposit` / `scoreboard`) in the// tilemap.world editor — the game identifies the resource by this URI, and the client fetches this// record for the name/sprite (the sprite serves as both the menu/scoreboard icon and the carry// sprite). Replaces the old browser Scenario editor's resource authoring.//// Auth is an app password (Settings → App Passwords on your PDS), prompted on the TTY (hidden);// defaults target chris.pardy.family on pds.pardisio.xyz. Same pattern as upload-sprites.ts.//// tsx scripts/mint-resource.ts ./wood.png "Wood" "Logs"//// # overrides (env): PDS_HANDLE, PDS_SERVICE, PDS_PASSWORD (skips the prompt)// PDS_HANDLE=you.example.com tsx scripts/mint-resource.ts ./ore.webp "Ore"//// Args: <sprite-image> <name> [plural]. The image must be image/png or image/webp (≤ 1 MB, per the// town.deception.resource lexicon's `sprite` blob constraint).
import { readFile } from 'node:fs/promises'import { extname } from 'node:path'import { createInterface } from 'node:readline'import { AtpAgent } from '@atproto/api'
const COLLECTION = 'town.deception.resource'const DEFAULT_SERVICE = 'https://pds.pardisio.xyz'const DEFAULT_HANDLE = 'chris.pardy.family'const MAX_BLOB_BYTES = 1_000_000 // matches the lexicon's `sprite` maxSize
/** Prompt for a secret on the TTY without echoing keystrokes. */function promptPassword(query: string): Promise<string> { return new Promise((resolve) => { const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true }) let muted = false ;(rl as unknown as { _writeToOutput: (s: string) => void })._writeToOutput = (s: string) => { if (!muted) process.stdout.write(s) } rl.question(query, (value) => { process.stdout.write('\n') rl.close() resolve(value) }) muted = true })}
function env(name: string, fallback?: string): string { const v = process.env[name] ?? fallback if (v === undefined) { console.error(`missing required env var ${name}`) process.exit(1) } return v}
/** image/png or image/webp from the file extension; rejects anything else (the lexicon's accept set). */function mimeFor(file: string): string { const ext = extname(file).toLowerCase() if (ext === '.png') return 'image/png' if (ext === '.webp') return 'image/webp' console.error(`unsupported sprite type ${ext || '(none)'} — expected .png or .webp`) process.exit(1)}
async function main() { const [spritePath, name, plural] = process.argv.slice(2) if (!spritePath || !name) { console.error('usage: tsx scripts/mint-resource.ts <sprite-image> <name> [plural]') process.exit(1) } const mime = mimeFor(spritePath) const bytes = new Uint8Array(await readFile(spritePath)) if (bytes.byteLength === 0) { console.error(`${spritePath} is empty`) process.exit(1) } if (bytes.byteLength > MAX_BLOB_BYTES) { console.error(`${spritePath} is ${bytes.byteLength} bytes — max ${MAX_BLOB_BYTES}`) process.exit(1) }
const identifier = env('PDS_HANDLE', DEFAULT_HANDLE) const password = process.env.PDS_PASSWORD ?? (await promptPassword(`app password for ${identifier}: `)) if (!password) { console.error('no password provided') process.exit(1) }
const agent = new AtpAgent({ service: env('PDS_SERVICE', DEFAULT_SERVICE) }) await agent.login({ identifier, password }) const did = agent.session!.did console.log(`logged in as ${did}`)
const { data: up } = await agent.com.atproto.repo.uploadBlob(bytes, { encoding: mime }) const record = { $type: COLLECTION, name, ...(plural ? { plural } : {}), sprite: up.blob, createdAt: new Date().toISOString(), } const { data: put } = await agent.com.atproto.repo.createRecord({ repo: did, collection: COLLECTION, record, // town.deception.* isn't a PDS-resolvable lexicon, so skip server-side schema validation (same // as the game-declaration / sprite writes). validate: false, }) console.log(` ✓ ${name}${plural ? ` / ${plural}` : ''}`) console.log(` ${put.uri}`) console.log('\nPaste that AT-URI into a station marker (town.deception.collect / deposit / scoreboard).')}
main().catch((err) => { console.error(err) process.exit(1)})