diff --git a/ASSETS.md b/ASSETS.md index 083fce4..be2ed2e 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -46,10 +46,10 @@ The Babylon comparison path now has a formal package pipeline rather than scene- | Storefront depth and background massing | Original procedural geometry plus existing generated facade WebP | Awnings, columns, sills, roof depth, and skyline enclosure around the existing facade | Consolidate into `block.1208-las-olas.v1` source scene and GLB | | FPS weapon presentation | Original procedural assembly in `OpsWorld.ts` | Visible first-person weapon and local recoil only | Replace with the possessed member package's `firstPersonArmsGlb` and weapon socket contract | -The Quaternius Universal Base Characters pack was evaluated as a permissive cross-engine reference and its primary page/license were checked, but no archive was committed or used in the runtime. The official free-download endpoint rate-limited automated access, and the pack's visual style is not the final benchmark. No marketplace or third-party model is represented as project-owned content. +The Quaternius Universal Base Characters and Universal Animation Library free standard packages were acquired through their official zero-price itch pages. Their creator-provided archive licenses dedicate the included assets to CC0 1.0. One male base and 12 gameplay clips are now combined into the optimized runtime package at `/assets/packages/characters/universal-male/universal-male.v1.glb`; the exact license and strict package manifest ship beside it. This model proves the portable rig pipeline but its superhero costume is explicitly not the final benchmark. No marketplace asset or third-party model is represented as project-owned content. All procedural fallbacks are code, not claimed production art. They exist to remove player-facing capsules/spheres, exercise semantic hit zones, and let the gameplay and package pipeline be verified before licensed source models arrive. ### Evaluated External Character Reference -Quaternius's **Universal Base Characters** page describes six game-ready base characters, 20 hairstyles, an average 13,000-triangle model, humanoid retargeting, FBX/glTF exports, and compatibility claims for Unreal, Unity, and Godot: https://quaternius.com/packs/universalbasecharacters.html. The current Quaternius Asset License permits use and modification in commercial products without attribution but prohibits redistributing the assets themselves as an asset pack: https://quaternius.com/license.html. These links document evaluation only; the files are not part of this repository. +Quaternius's **Universal Base Characters** page describes six game-ready base characters, 20 hairstyles, an average 13,000-triangle model, humanoid retargeting, FBX/glTF exports, and compatibility with Unreal, Unity, and Godot: https://quaternius.com/packs/universalbasecharacters.html. The selected free standard archive includes `CC0 1.0 Universal (CC0 1.0) Public Domain Dedication` and identifies Quaternius as the model creator; that exact license is preserved at `frontend/public/assets/packages/characters/universal-male/LICENSE-QUATERNIUS-CC0.txt`. Acquisition and transformation details are recorded in `docs/PRODUCTION_ART_SOURCES.md` and `docs/PRODUCTION_ART_PACKAGE_V1.md`. diff --git a/backend/supabase/functions/combat/commit-result/index.ts b/backend/supabase/functions/combat/commit-result/index.ts new file mode 100644 index 0000000..29750ce --- /dev/null +++ b/backend/supabase/functions/combat/commit-result/index.ts @@ -0,0 +1,53 @@ +import { z } from 'zod'; +import { corsHeaders } from '../../_shared/cors.ts'; +import { errorResponse, successResponse, parseBody } from '../../_shared/utils.ts'; +import { createSupabaseClient } from '../../_shared/supabaseClient.ts'; + +const EncounterResultSchema = z.object({ + idempotencyKey: z.string().min(1).max(160), + outcome: z.enum(['secured', 'retreated', 'overrun']), + crewDown: z.array(z.string().min(1)).max(32), + oppositionDown: z.array(z.string().min(1)).max(32), + objectiveProgress: z.number().finite().min(0), + heatDelta: z.number().finite().int().min(-5).max(5), + moraleDelta: z.number().finite().int().min(-100).max(100), + pendingIncomeDelta: z.number().finite().int().min(-1000000).max(1000000), + summary: z.string().min(1).max(500), +}); + +const CommitEncounterResultSchema = z.object({ + blockId: z.string().uuid(), + result: EncounterResultSchema, +}); + +/** + * Records an already-resolved deterministic encounter result. The database + * RPC binds the receipt to auth.uid() and makes retries idempotent; this + * endpoint deliberately does not calculate combat or trust a profile ID. + */ +Deno.serve(async (req) => { + if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }); + + try { + const body = await parseBody(req); + const { blockId, result } = CommitEncounterResultSchema.parse(body); + const supabase = createSupabaseClient(req); + const { data: { user }, error: userError } = await supabase.auth.getUser(); + + if (userError || !user) return errorResponse('Unauthorized', 401); + + const { data, error } = await supabase.rpc('commit_encounter_result', { + p_result_key: result.idempotencyKey, + p_block_id: blockId, + p_payload: result, + }); + + if (error) return errorResponse(error.message, 400); + return successResponse({ receipt: data }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return errorResponse(`Validation error: ${error.issues[0]?.message ?? 'Invalid request'}`, 400); + } + return errorResponse(error instanceof Error ? error.message : 'Unexpected error', 500); + } +}); diff --git a/backend/supabase/functions/world-tick-ghost/index.ts b/backend/supabase/functions/world-tick-ghost/index.ts new file mode 100644 index 0000000..a58e5f0 --- /dev/null +++ b/backend/supabase/functions/world-tick-ghost/index.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +import { corsHeaders } from '../_shared/cors.ts'; +import { errorResponse, successResponse, parseBody } from '../_shared/utils.ts'; +import { createAdminSupabaseClient } from '../_shared/supabaseClient.ts'; + +const GhostCrewStateSchema = z.object({ + id: z.string().min(1).max(120), + treasury: z.number().finite().int().min(0).optional(), + roster: z.array(z.object({ + id: z.string().min(1), + name: z.string().min(1).max(100), + role: z.enum(['shooter', 'dealer', 'enforcer']), + level: z.number().finite().int().min(1).max(10), + alive: z.boolean(), + })).max(32).optional(), + ownedBlockIds: z.array(z.string().min(1).max(160)).max(100).optional(), + claimedDnaIds: z.array(z.string().min(1).max(120)).max(100).optional(), + grudge: z.object({ + score: z.number().finite().min(0).max(100), + lastIncidentBlockId: z.string().max(160).optional(), + lastIncidentAt: z.string().datetime().optional(), + }).optional(), + incomePerTick: z.number().finite().int().min(0).max(1000000).optional(), + lastTickAt: z.string().datetime().optional(), + lastMove: z.string().max(500).optional(), +}); + +const WorldEventSchema = z.object({ + eventKey: z.string().min(1).max(220).optional(), + crewId: z.string().min(1).max(120).optional(), + recipientProfileId: z.string().uuid().optional(), + action: z.enum(['claim', 'reinforce', 'attack', 'lay-low', 'system']).optional(), + targetBlockId: z.string().max(160).optional(), + description: z.string().min(1).max(500), + data: z.record(z.unknown()).optional(), + timestamp: z.string().datetime().optional(), +}); + +const WorldTickSchema = z.object({ + tickKey: z.string().min(1).max(220), + seed: z.number().finite().int().optional(), + crews: z.array(GhostCrewStateSchema).min(1).max(20), + events: z.array(WorldEventSchema).max(100).default([]), +}); + +/** + * Commits a world tick selected by trusted server scheduling logic. It has no + * player-accessible execution path: the x-cron-secret must match the deployed + * function secret before a service-role RPC client is created. + */ +Deno.serve(async (req) => { + if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }); + + const expectedSecret = Deno.env.get('WORLD_TICK_SECRET'); + if (!expectedSecret || req.headers.get('x-cron-secret') !== expectedSecret) { + return errorResponse('Unauthorized', 401); + } + + try { + const body = await parseBody(req); + const tick = WorldTickSchema.parse(body); + const supabase = createAdminSupabaseClient(); + const { data, error } = await supabase.rpc('apply_ghost_world_tick', { + p_tick_key: tick.tickKey, + p_crew_states: tick.crews, + p_events: tick.events, + p_seed: tick.seed ?? null, + }); + + if (error) return errorResponse(error.message, 400); + return successResponse({ tick: data }); + } catch (error: unknown) { + if (error instanceof z.ZodError) { + return errorResponse(`Validation error: ${error.issues[0]?.message ?? 'Invalid request'}`, 400); + } + return errorResponse(error instanceof Error ? error.message : 'Unexpected error', 500); + } +}); diff --git a/backend/supabase/migrations/005_authoritative_world_foundation.sql b/backend/supabase/migrations/005_authoritative_world_foundation.sql new file mode 100644 index 0000000..afeadb1 --- /dev/null +++ b/backend/supabase/migrations/005_authoritative_world_foundation.sql @@ -0,0 +1,340 @@ +-- ============================================================ +-- DEALT/SLIDE — Authoritative World Foundation +-- +-- Additive foundation for durable Ghost Crew state, idempotent +-- world ticks, safe Block DNA references, and deterministic +-- encounter-result receipts. This migration does not replace the +-- existing local deterministic combat session. +-- ============================================================ + +-- ─── Canonical Ghost Crew state ────────────────────────────── +CREATE TABLE IF NOT EXISTS public.ghost_crews ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + home_tag TEXT NOT NULL, + personality JSONB NOT NULL DEFAULT '{}'::jsonb, + treasury INTEGER NOT NULL DEFAULT 0 CHECK (treasury >= 0), + roster JSONB NOT NULL DEFAULT '[]'::jsonb, + owned_block_ids JSONB NOT NULL DEFAULT '[]'::jsonb, + claimed_dna_ids JSONB NOT NULL DEFAULT '[]'::jsonb, + grudge JSONB NOT NULL DEFAULT '{"score": 0}'::jsonb, + income_per_tick INTEGER NOT NULL DEFAULT 0 CHECK (income_per_tick >= 0), + last_tick_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_move TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_ghost_crews_updated_at ON public.ghost_crews(updated_at DESC); +DROP TRIGGER IF EXISTS set_ghost_crews_updated_at ON public.ghost_crews; +CREATE TRIGGER set_ghost_crews_updated_at BEFORE UPDATE ON public.ghost_crews + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- ─── Minimal approved Block DNA projection ─────────────────── +-- The source block remains the owner of location and possession data. +-- This table keeps only the gameplay archetype and derived tactical snapshot. +CREATE TABLE IF NOT EXISTS public.claimed_block_dna ( + block_id UUID PRIMARY KEY REFERENCES public.blocks(id) ON DELETE CASCADE, + dna_id TEXT NOT NULL, + tactical_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb, + claimed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +DROP TRIGGER IF EXISTS set_claimed_block_dna_updated_at ON public.claimed_block_dna; +CREATE TRIGGER set_claimed_block_dna_updated_at BEFORE UPDATE ON public.claimed_block_dna + FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column(); + +-- ─── Idempotent server-tick ledger ─────────────────────────── +CREATE TABLE IF NOT EXISTS public.world_ticks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tick_key TEXT NOT NULL UNIQUE, + source TEXT NOT NULL DEFAULT 'ghost-crew' CHECK (source IN ('ghost-crew', 'manual', 'system')), + seed INTEGER, + action_count INTEGER NOT NULL DEFAULT 0 CHECK (action_count >= 0), + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_world_ticks_applied_at ON public.world_ticks(applied_at DESC); + +-- ─── Durable, deduplicated player-visible world events ─────── +CREATE TABLE IF NOT EXISTS public.world_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + event_key TEXT NOT NULL UNIQUE, + tick_id UUID REFERENCES public.world_ticks(id) ON DELETE SET NULL, + crew_id TEXT REFERENCES public.ghost_crews(id) ON DELETE SET NULL, + recipient_profile_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE, + event_type TEXT NOT NULL CHECK (event_type IN ('claim', 'reinforce', 'attack', 'lay-low', 'encounter', 'system')), + target_block_key TEXT, + description TEXT NOT NULL, + data JSONB NOT NULL DEFAULT '{}'::jsonb, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_world_events_recipient ON public.world_events(recipient_profile_id, occurred_at DESC); +CREATE INDEX IF NOT EXISTS idx_world_events_tick ON public.world_events(tick_id, occurred_at DESC); +CREATE INDEX IF NOT EXISTS idx_world_events_crew ON public.world_events(crew_id, occurred_at DESC); + +-- ─── Idempotent unified combat-result receipts ─────────────── +CREATE TABLE IF NOT EXISTS public.encounter_results ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + result_key TEXT NOT NULL UNIQUE, + profile_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE, + block_id UUID NOT NULL REFERENCES public.blocks(id) ON DELETE CASCADE, + outcome TEXT NOT NULL CHECK (outcome IN ('secured', 'retreated', 'overrun')), + payload JSONB NOT NULL, + committed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_encounter_results_profile ON public.encounter_results(profile_id, committed_at DESC); +CREATE INDEX IF NOT EXISTS idx_encounter_results_block ON public.encounter_results(block_id, committed_at DESC); + +-- ─── Row-level security ────────────────────────────────────── +ALTER TABLE public.ghost_crews ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.claimed_block_dna ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.world_ticks ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.world_events ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.encounter_results ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Ghost crews are publicly viewable" ON public.ghost_crews; +CREATE POLICY "Ghost crews are publicly viewable" + ON public.ghost_crews FOR SELECT USING (true); + +DROP POLICY IF EXISTS "Block DNA is publicly viewable" ON public.claimed_block_dna; +CREATE POLICY "Block DNA is publicly viewable" + ON public.claimed_block_dna FOR SELECT USING (true); + +DROP POLICY IF EXISTS "Block owners manage block DNA" ON public.claimed_block_dna; +CREATE POLICY "Block owners manage block DNA" + ON public.claimed_block_dna FOR ALL + USING (EXISTS (SELECT 1 FROM public.blocks WHERE id = block_id AND owner_id = auth.uid())) + WITH CHECK (EXISTS (SELECT 1 FROM public.blocks WHERE id = block_id AND owner_id = auth.uid())); + +DROP POLICY IF EXISTS "World events visible to recipients or public" ON public.world_events; +CREATE POLICY "World events visible to recipients or public" + ON public.world_events FOR SELECT + USING (recipient_profile_id IS NULL OR recipient_profile_id = auth.uid()); + +DROP POLICY IF EXISTS "Encounter receipts visible to participants" ON public.encounter_results; +CREATE POLICY "Encounter receipts visible to participants" + ON public.encounter_results FOR SELECT USING (profile_id = auth.uid()); + +-- No direct policies are added for writes to ghost_crews, world_ticks, +-- world_events, or encounter_results. They are mutated only through the +-- security-definer RPCs below or service-role Edge Functions. + +-- ─── Seed the same deterministic crews used by the local engine ─────── +INSERT INTO public.ghost_crews ( + id, name, home_tag, personality, treasury, roster, owned_block_ids, + claimed_dna_ids, grudge, income_per_tick, last_move +) VALUES +( + 'ghost-nightfall', 'Nightfall Crew', 'downtown', + '{"type":"territory-hungry","aggression":55,"expansionDrive":85,"grudgeWeight":40,"caution":30}'::jsonb, + 2200, + '[{"id":"nf-1","name":"Olas King","role":"enforcer","level":4,"alive":true},{"id":"nf-2","name":"Strip Boss","role":"shooter","level":4,"alive":true},{"id":"nf-3","name":"Beach Boy","role":"dealer","level":3,"alive":true}]'::jsonb, + '[]'::jsonb, '[]'::jsonb, '{"score":0}'::jsonb, 0, 'Controlling downtown Las Olas' +), +( + 'ghost-sistrunk', 'Sistrunk Ghosts', 'eastside', + '{"type":"revenge-driven","aggression":80,"expansionDrive":45,"grudgeWeight":90,"caution":20}'::jsonb, + 1500, + '[{"id":"sg-1","name":"Fed Buster","role":"enforcer","level":4,"alive":true},{"id":"sg-2","name":"All-Day","role":"shooter","level":3,"alive":true},{"id":"sg-3","name":"Zero Fed","role":"shooter","level":3,"alive":true}]'::jsonb, + '[]'::jsonb, '[]'::jsonb, '{"score":15}'::jsonb, 0, 'Watching Sistrunk Blvd' +), +( + 'ghost-riverwalk', 'Riverwalk Money Crew', 'southside', + '{"type":"money-crew","aggression":30,"expansionDrive":55,"grudgeWeight":25,"caution":80}'::jsonb, + 3000, + '[{"id":"rm-1","name":"Lucky 7","role":"dealer","level":4,"alive":true},{"id":"rm-2","name":"Down-Low","role":"dealer","level":3,"alive":true},{"id":"rm-3","name":"Seven-Up","role":"enforcer","level":3,"alive":true}]'::jsonb, + '[]'::jsonb, '[]'::jsonb, '{"score":0}'::jsonb, 0, 'Running the Riverwalk docks' +), +( + 'ghost-chaos', 'Westside Wolves', 'westside', + '{"type":"chaotic","aggression":70,"expansionDrive":65,"grudgeWeight":55,"caution":10}'::jsonb, + 1200, + '[{"id":"ww-1","name":"Cloud 9","role":"shooter","level":2,"alive":true},{"id":"ww-2","name":"Nine-Life","role":"dealer","level":2,"alive":true},{"id":"ww-3","name":"Lil Niner","role":"enforcer","level":2,"alive":true}]'::jsonb, + '[]'::jsonb, '[]'::jsonb, '{"score":5}'::jsonb, 0, 'Tagging the west side' +) +ON CONFLICT (id) DO NOTHING; + +-- ─── Service-only world tick commit ────────────────────────── +CREATE OR REPLACE FUNCTION public.apply_ghost_world_tick( + p_tick_key TEXT, + p_crew_states JSONB, + p_events JSONB DEFAULT '[]'::jsonb, + p_seed INTEGER DEFAULT NULL +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_tick_id UUID; + v_crew JSONB; + v_event JSONB; + v_event_key TEXT; +BEGIN + IF COALESCE(trim(p_tick_key), '') = '' THEN + RAISE EXCEPTION 'tick_key is required'; + END IF; + IF jsonb_typeof(p_crew_states) <> 'array' OR jsonb_typeof(p_events) <> 'array' THEN + RAISE EXCEPTION 'crew_states and events must be JSON arrays'; + END IF; + + INSERT INTO public.world_ticks (tick_key, source, seed, action_count) + VALUES (p_tick_key, 'ghost-crew', p_seed, jsonb_array_length(p_crew_states)) + ON CONFLICT (tick_key) DO NOTHING + RETURNING id INTO v_tick_id; + + IF v_tick_id IS NULL THEN + SELECT id INTO v_tick_id FROM public.world_ticks WHERE tick_key = p_tick_key; + RETURN jsonb_build_object('applied', false, 'tickId', v_tick_id, 'tickKey', p_tick_key); + END IF; + + FOR v_crew IN SELECT value FROM jsonb_array_elements(p_crew_states) + LOOP + UPDATE public.ghost_crews + SET + treasury = GREATEST(0, COALESCE((v_crew->>'treasury')::integer, treasury)), + roster = COALESCE(v_crew->'roster', roster), + owned_block_ids = COALESCE(v_crew->'ownedBlockIds', owned_block_ids), + claimed_dna_ids = COALESCE(v_crew->'claimedDnaIds', claimed_dna_ids), + grudge = COALESCE(v_crew->'grudge', grudge), + income_per_tick = GREATEST(0, COALESCE((v_crew->>'incomePerTick')::integer, income_per_tick)), + last_tick_at = COALESCE(NULLIF(v_crew->>'lastTickAt', '')::timestamptz, NOW()), + last_move = COALESCE(NULLIF(v_crew->>'lastMove', ''), last_move) + WHERE id = v_crew->>'id'; + + IF NOT FOUND THEN + RAISE EXCEPTION 'unknown ghost crew: %', v_crew->>'id'; + END IF; + END LOOP; + + FOR v_event IN SELECT value FROM jsonb_array_elements(p_events) + LOOP + v_event_key := COALESCE( + NULLIF(v_event->>'eventKey', ''), + p_tick_key || ':' || COALESCE(NULLIF(v_event->>'crewId', ''), 'system') || ':' || COALESCE(NULLIF(v_event->>'action', ''), 'event') + ); + INSERT INTO public.world_events ( + event_key, tick_id, crew_id, recipient_profile_id, event_type, + target_block_key, description, data, occurred_at + ) VALUES ( + v_event_key, + v_tick_id, + NULLIF(v_event->>'crewId', ''), + NULLIF(v_event->>'recipientProfileId', '')::uuid, + COALESCE(NULLIF(v_event->>'action', ''), 'system'), + NULLIF(v_event->>'targetBlockId', ''), + COALESCE(NULLIF(v_event->>'description', ''), 'World state changed.'), + COALESCE(v_event->'data', '{}'::jsonb), + COALESCE(NULLIF(v_event->>'timestamp', '')::timestamptz, NOW()) + ) ON CONFLICT (event_key) DO NOTHING; + END LOOP; + + RETURN jsonb_build_object('applied', true, 'tickId', v_tick_id, 'tickKey', p_tick_key); +END; +$$; + +-- ─── Authenticated, idempotent encounter receipt ───────────── +CREATE OR REPLACE FUNCTION public.commit_encounter_result( + p_result_key TEXT, + p_block_id UUID, + p_payload JSONB +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_profile_id UUID := auth.uid(); + v_result_id UUID; + v_existing RECORD; + v_heat_delta INTEGER; + v_morale_delta INTEGER; + v_pending_income_delta INTEGER; +BEGIN + IF v_profile_id IS NULL THEN + RAISE EXCEPTION 'authentication required'; + END IF; + IF COALESCE(trim(p_result_key), '') = '' THEN + RAISE EXCEPTION 'result_key is required'; + END IF; + IF p_payload IS NULL OR jsonb_typeof(p_payload) <> 'object' THEN + RAISE EXCEPTION 'result payload must be an object'; + END IF; + IF COALESCE(p_payload->>'idempotencyKey', '') <> p_result_key THEN + RAISE EXCEPTION 'result key does not match payload idempotency key'; + END IF; + IF COALESCE(p_payload->>'outcome', '') NOT IN ('secured', 'retreated', 'overrun') THEN + RAISE EXCEPTION 'invalid encounter outcome'; + END IF; + IF NOT EXISTS ( + SELECT 1 FROM public.blocks WHERE id = p_block_id AND owner_id = v_profile_id + ) THEN + RAISE EXCEPTION 'block is not owned by authenticated profile'; + END IF; + + INSERT INTO public.encounter_results (result_key, profile_id, block_id, outcome, payload) + VALUES (p_result_key, v_profile_id, p_block_id, p_payload->>'outcome', p_payload) + ON CONFLICT (result_key) DO NOTHING + RETURNING id INTO v_result_id; + + IF v_result_id IS NULL THEN + SELECT id, profile_id, block_id INTO v_existing + FROM public.encounter_results + WHERE result_key = p_result_key; + IF v_existing.profile_id <> v_profile_id OR v_existing.block_id <> p_block_id THEN + RAISE EXCEPTION 'result key belongs to a different participant or block'; + END IF; + RETURN jsonb_build_object('applied', false, 'resultId', v_existing.id, 'resultKey', p_result_key); + END IF; + + v_heat_delta := COALESCE((p_payload->>'heatDelta')::integer, 0); + v_morale_delta := COALESCE((p_payload->>'moraleDelta')::integer, 0); + v_pending_income_delta := COALESCE((p_payload->>'pendingIncomeDelta')::integer, 0); + + UPDATE public.blocks + SET + block_heat = GREATEST(0, LEAST(100, block_heat + (v_heat_delta * 20))), + metadata = jsonb_set( + jsonb_set( + jsonb_set(COALESCE(metadata, '{}'::jsonb), '{morale}', to_jsonb(GREATEST(0, LEAST(100, COALESCE((metadata->>'morale')::integer, 80) + v_morale_delta))), true), + '{pendingIncome}', to_jsonb(GREATEST(0, COALESCE((metadata->>'pendingIncome')::integer, 0) + v_pending_income_delta)), true + ), + '{lastEncounterResultKey}', to_jsonb(p_result_key), true + ) + WHERE id = p_block_id + AND owner_id = v_profile_id + AND COALESCE(metadata->>'lastEncounterResultKey', '') <> p_result_key; + + INSERT INTO public.world_events ( + event_key, recipient_profile_id, event_type, target_block_key, description, data + ) VALUES ( + 'encounter:' || p_result_key, + v_profile_id, + 'encounter', + p_block_id::text, + COALESCE(NULLIF(p_payload->>'summary', ''), 'Encounter result committed.'), + jsonb_build_object( + 'outcome', p_payload->>'outcome', + 'heatDelta', v_heat_delta, + 'moraleDelta', v_morale_delta, + 'pendingIncomeDelta', v_pending_income_delta + ) + ) ON CONFLICT (event_key) DO NOTHING; + + RETURN jsonb_build_object('applied', true, 'resultId', v_result_id, 'resultKey', p_result_key); +END; +$$; + +-- The tick writer is service-only. The result writer validates auth.uid() +-- and block ownership internally before any mutation. +REVOKE ALL ON FUNCTION public.apply_ghost_world_tick(TEXT, JSONB, JSONB, INTEGER) FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.apply_ghost_world_tick(TEXT, JSONB, JSONB, INTEGER) TO service_role; +REVOKE ALL ON FUNCTION public.commit_encounter_result(TEXT, UUID, JSONB) FROM PUBLIC, anon; +GRANT EXECUTE ON FUNCTION public.commit_encounter_result(TEXT, UUID, JSONB) TO authenticated, service_role; + +-- Only public/read-safe event streams are published; direct writes stay behind RPCs. +ALTER PUBLICATION supabase_realtime ADD TABLE public.ghost_crews; +ALTER PUBLICATION supabase_realtime ADD TABLE public.world_events; diff --git a/backend/supabase/migrations/006_authoritative_world_integrity_hardening.sql b/backend/supabase/migrations/006_authoritative_world_integrity_hardening.sql new file mode 100644 index 0000000..0fc4b2f --- /dev/null +++ b/backend/supabase/migrations/006_authoritative_world_integrity_hardening.sql @@ -0,0 +1,175 @@ +-- ============================================================ +-- DEALT/SLIDE — Authoritative World Integrity Hardening +-- +-- Follow-up to 005_authoritative_world_foundation.sql. +-- Tightens bounded result deltas and makes background block projection +-- writes reject stale snapshots after a newer encounter has committed. +-- ============================================================ + +CREATE OR REPLACE FUNCTION public.commit_encounter_result( + p_result_key TEXT, + p_block_id UUID, + p_payload JSONB +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_profile_id UUID := auth.uid(); + v_result_id UUID; + v_existing RECORD; + v_heat_delta INTEGER; + v_morale_delta INTEGER; + v_pending_income_delta INTEGER; +BEGIN + IF v_profile_id IS NULL THEN + RAISE EXCEPTION 'authentication required'; + END IF; + IF COALESCE(trim(p_result_key), '') = '' THEN + RAISE EXCEPTION 'result_key is required'; + END IF; + IF p_payload IS NULL OR jsonb_typeof(p_payload) <> 'object' THEN + RAISE EXCEPTION 'result payload must be an object'; + END IF; + IF COALESCE(p_payload->>'idempotencyKey', '') <> p_result_key THEN + RAISE EXCEPTION 'result key does not match payload idempotency key'; + END IF; + IF COALESCE(p_payload->>'outcome', '') NOT IN ('secured', 'retreated', 'overrun') THEN + RAISE EXCEPTION 'invalid encounter outcome'; + END IF; + + v_heat_delta := COALESCE((p_payload->>'heatDelta')::integer, 0); + v_morale_delta := COALESCE((p_payload->>'moraleDelta')::integer, 0); + v_pending_income_delta := COALESCE((p_payload->>'pendingIncomeDelta')::integer, 0); + IF v_heat_delta NOT BETWEEN -5 AND 5 + OR v_morale_delta NOT BETWEEN -100 AND 100 + OR v_pending_income_delta NOT BETWEEN -1000000 AND 1000000 THEN + RAISE EXCEPTION 'encounter result deltas exceed permitted bounds'; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM public.blocks WHERE id = p_block_id AND owner_id = v_profile_id + ) THEN + RAISE EXCEPTION 'block is not owned by authenticated profile'; + END IF; + + INSERT INTO public.encounter_results (result_key, profile_id, block_id, outcome, payload) + VALUES (p_result_key, v_profile_id, p_block_id, p_payload->>'outcome', p_payload) + ON CONFLICT (result_key) DO NOTHING + RETURNING id INTO v_result_id; + + IF v_result_id IS NULL THEN + SELECT id, profile_id, block_id INTO v_existing + FROM public.encounter_results + WHERE result_key = p_result_key; + IF v_existing.profile_id <> v_profile_id OR v_existing.block_id <> p_block_id THEN + RAISE EXCEPTION 'result key belongs to a different participant or block'; + END IF; + RETURN jsonb_build_object('applied', false, 'resultId', v_existing.id, 'resultKey', p_result_key); + END IF; + + UPDATE public.blocks + SET + block_heat = GREATEST(0, LEAST(100, block_heat + (v_heat_delta * 20))), + metadata = jsonb_set( + jsonb_set( + jsonb_set(COALESCE(metadata, '{}'::jsonb), '{morale}', to_jsonb(GREATEST(0, LEAST(100, COALESCE((metadata->>'morale')::integer, 80) + v_morale_delta))), true), + '{pendingIncome}', to_jsonb(GREATEST(0, COALESCE((metadata->>'pendingIncome')::integer, 0) + v_pending_income_delta)), true + ), + '{lastEncounterResultKey}', to_jsonb(p_result_key), true + ) + WHERE id = p_block_id AND owner_id = v_profile_id; + + INSERT INTO public.world_events ( + event_key, recipient_profile_id, event_type, target_block_key, description, data + ) VALUES ( + 'encounter:' || p_result_key, + v_profile_id, + 'encounter', + p_block_id::text, + COALESCE(NULLIF(p_payload->>'summary', ''), 'Encounter result committed.'), + jsonb_build_object( + 'outcome', p_payload->>'outcome', + 'heatDelta', v_heat_delta, + 'moraleDelta', v_morale_delta, + 'pendingIncomeDelta', v_pending_income_delta + ) + ) ON CONFLICT (event_key) DO NOTHING; + + RETURN jsonb_build_object('applied', true, 'resultId', v_result_id, 'resultKey', p_result_key); +END; +$$; + +-- Atomically accepts the current local block projection only when it is based +-- on the current server encounter receipt key. This is used by the existing +-- debounced sync path, which may otherwise arrive after an encounter RPC. +CREATE OR REPLACE FUNCTION public.persist_player_block_projection( + p_block_id UUID, + p_address TEXT, + p_lng DOUBLE PRECISION, + p_lat DOUBLE PRECISION, + p_status public.block_status, + p_block_heat INTEGER, + p_base_income INTEGER, + p_metadata JSONB, + p_client_result_key TEXT DEFAULT NULL +) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + v_profile_id UUID := auth.uid(); + v_server_result_key TEXT; +BEGIN + IF v_profile_id IS NULL THEN + RAISE EXCEPTION 'authentication required'; + END IF; + IF p_block_heat NOT BETWEEN 0 AND 100 THEN + RAISE EXCEPTION 'block heat must be between 0 and 100'; + END IF; + IF p_base_income < 0 OR p_base_income > 1000000 THEN + RAISE EXCEPTION 'base income is outside permitted bounds'; + END IF; + + SELECT metadata->>'lastEncounterResultKey' INTO v_server_result_key + FROM public.blocks + WHERE id = p_block_id AND owner_id = v_profile_id + FOR UPDATE; + + IF FOUND AND COALESCE(v_server_result_key, '') <> COALESCE(p_client_result_key, '') THEN + RETURN jsonb_build_object('applied', false, 'reason', 'stale_encounter_projection'); + END IF; + + INSERT INTO public.blocks ( + id, address, location, owner_id, status, block_heat, base_income, metadata, updated_at + ) VALUES ( + p_block_id, + p_address, + ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography, + v_profile_id, + p_status, + p_block_heat, + p_base_income, + COALESCE(p_metadata, '{}'::jsonb), + NOW() + ) + ON CONFLICT (id) DO UPDATE SET + address = EXCLUDED.address, + location = EXCLUDED.location, + status = EXCLUDED.status, + block_heat = EXCLUDED.block_heat, + base_income = EXCLUDED.base_income, + metadata = EXCLUDED.metadata, + updated_at = NOW() + WHERE public.blocks.owner_id = v_profile_id; + + RETURN jsonb_build_object('applied', true); +END; +$$; + +REVOKE ALL ON FUNCTION public.persist_player_block_projection(UUID, TEXT, DOUBLE PRECISION, DOUBLE PRECISION, public.block_status, INTEGER, INTEGER, JSONB, TEXT) FROM PUBLIC, anon; +GRANT EXECUTE ON FUNCTION public.persist_player_block_projection(UUID, TEXT, DOUBLE PRECISION, DOUBLE PRECISION, public.block_status, INTEGER, INTEGER, JSONB, TEXT) TO authenticated, service_role; diff --git a/docs/AI_MANUS_AUTHORITATIVE_WORLD_DESIGN.md b/docs/AI_MANUS_AUTHORITATIVE_WORLD_DESIGN.md new file mode 100644 index 0000000..a86cf57 --- /dev/null +++ b/docs/AI_MANUS_AUTHORITATIVE_WORLD_DESIGN.md @@ -0,0 +1,48 @@ +# Authoritative World Foundation Design + +**Owner:** Manus AI +**Branch:** `feat/authoritative-world-foundation` +**Base:** `main-tL2525` at `2d80761440be87702802d0c59d26cfad3717bb1c` +**Scope:** A minimal, additive server-backed foundation for durable Ghost Crew state, world events, and idempotent encounter-result recording. This design deliberately does **not** turn the game into real-time multiplayer or replace the existing deterministic combat simulation. + +## Current state and ownership + +The client currently owns the working strategy loop. `ghostCrewStore.ts` persists named crews, turf, treasury, grudges, and a tick counter to browser storage; it projects rival territory into `blockStore`. `ghostCrewEngine.ts` provides the deterministic decision and transition functions (`decideGhostAction`, `applyGhostAction`, `buildGhostBlock`, and `addGrudge`). `blockStore.ts` owns the local territory projection and already prevents duplicate local `CombatResult` application using `appliedEncounterResultKeys`. + +`prepareEncounter.ts` transforms block strategy state into the deterministic combat input, and `BlockModeView.tsx` is the player-facing result boundary. It currently applies the outcome to the local block store and player heat. The existing Supabase block persistence service is best-effort and serializes selected strategy fields into `blocks.metadata`; the two hydration hooks tolerate offline operation. Existing server world ticking and combat action functions are legacy paths and should not be used to resolve the modern deterministic encounter. + +> **Boundary:** the combat domain remains pure TypeScript. React, Zustand, Phaser, Babylon, Supabase, and HTTP are all outside it. The server records and applies completed outcomes; it does not simulate the client combat session in this first foundation. + +## Minimal durable model + +The new additive migration contains five tables. + +| Table | Purpose | Visibility/write model | +|---|---|---| +| `ghost_crews` | Canonical rival identity and coarse strategic state: name, faction, treasury, aggression, roster summary, grudge, owned DNA IDs, and last resolved tick | Publicly readable. Server-only writes. | +| `claimed_block_dna` | The game-safe DNA/archetype reference and a derived tactical snapshot for a claimed block. No detailed address is copied. | Publicly readable for visible blocks. Owner-only writes through the existing block ownership model. | +| `world_ticks` | Idempotency ledger for a player-visible world tick, keyed by `tick_key` | Server-only. | +| `world_events` | Immutable player-visible rival/world event stream. Each event may optionally identify its affected player profile. | An event is visible when public or addressed to the authenticated player. Server-only writes. | +| `encounter_results` | Idempotency receipt for a final `CombatResult`, keyed by a caller-supplied `result_key` | Participant-only reads. Server-only writes. | + +The new tables record only fictional block IDs, DNA IDs, and strategic metadata. They never require a private street address, home address, or a real-world target description. + +## Idempotency and transaction model + +A server-generated or trusted server-scheduled world tick uses a stable `tick_key`, for example `ghost:global:2026-09-01T22:00Z`. `apply_world_tick` inserts the key once under a unique constraint. Repeating the same key returns the original tick record and does not create a second world event. A valid action payload must be deterministic before the RPC is called: the client/rule engine supplies the result of the existing seeded Ghost Crew decision logic; the RPC only atomically records a validated, bounded action and immutable event. This keeps the change compatible with current scheduling while preventing duplicate observable outcomes. + +A completed encounter generates one `result_key`, normally `CombatResult.id`, and submits the already-determined outcome to `commit_encounter_result`. The function verifies that the claimed block belongs to the authenticated participant and inserts a receipt with `ON CONFLICT DO NOTHING`. A retry returns the original receipt without rewriting state. The initial server receipt intentionally does not duplicate local cash, crew, or block projection writes because existing schemas vary between legacy endpoints. The frontend remains immediately responsive through the existing local result application; the receipt provides the durable reconciliation anchor for the next synchronization increment. + +## Frontend synchronization model + +The new service is disabled in demo mode and catches configuration/network errors. It supports three narrow operations: load public Ghost Crew/world-event state, request/record a tick only when authorized server infrastructure invokes it, and record one encounter-result receipt. A hydration hook seeds locally only when the server has no visible state, then overlays durable crew/event state after authentication. It does not create another game store; it calls the existing Ghost Crew store’s dedicated replacement method. Realtime subscriptions are deferred until the server event producer and RLS ownership are proven in a live Supabase project; the first slice safely hydrates on authenticated boot and on page reload. + +## Migration and rollback + +Migration `005_authoritative_world_foundation.sql` is additive: it creates tables, indexes, policies, and RPCs without altering existing tables or deleting data. Its rollback is a separate manual migration that removes only these new objects after dependent function removal. No existing legacy world tick, combat endpoint, client persistence function, or asset manifest is changed by the migration. + +## Tests and acceptance + +Pure helpers test action normalization, result-key selection, and duplicate-safe state adaptation. Service tests mock the Supabase interface and assert demo/no-client no-op behavior, successful receipt processing, and duplicate receipts. Store tests assert remote Ghost Crew state is replaceable without breaking local actions. End-to-end database/RLS verification remains a deployment gate because it requires a configured Supabase project. + +The initial acceptance scenario is: a player sees stable Ghost Crew state after authenticated hydration, a completed encounter records one durable receipt, a retry returns the same receipt, and all current gameplay tests still pass. The next increment may use the world event ledger to drive an authenticated server world scheduler and reconcile final cash/territory effects from the receipt. diff --git a/docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.md b/docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.md new file mode 100644 index 0000000..4a9a289 --- /dev/null +++ b/docs/INTEGRATION_ALPHA_STABILIZATION_REPORT.md @@ -0,0 +1,53 @@ +# Alpha Stabilization Integration Report + +**Integration branch:** `integration/alpha-stabilization` +**Base branch:** `main-tL2525` +**Date:** September 2, 2026 + +## Scope + +This integration branch combines three independently reviewed, non-overlapping pull requests into one testable release candidate. The merge was assembled only in an isolated local worktree; it does not modify the remote default branch. + +| Source pull request | Integrated purpose | Result | +|---|---|---| +| [#121](https://github.com/BrandDead/slide/pull/121) | Production art package pipeline, lifecycle hardening, portable character/PBR packages | Merged cleanly into the integration branch | +| [#122](https://github.com/BrandDead/slide/pull/122) | Player-visible startup recovery boundary for missing service configuration | Merged cleanly into the integration branch | +| [#123](https://github.com/BrandDead/slide/pull/123) | Additive authoritative Ghost Crew and encounter-result foundation | Merged cleanly into the integration branch | + +> There were no textual merge conflicts. The integration does not add a second game engine, state manager, real-time combat system, payment system, or broad multiplayer system. + +## Combined validation + +The combined branch was validated after all three pull-request heads had been merged locally. + +| Gate | Result | Evidence | +|---|---|---| +| Frontend validation | Passed | `npm run validate`: 50 test files and 716 tests passed; lint completed with the repository’s existing warnings but no errors; asset audit reported zero errors; five packages passed validation | +| Production build | Passed | Standard production build completed successfully | +| Demo production build | Passed | Explicit `VITE_DEMO_MODE=1` production build completed successfully | +| Backend validation | Passed | `pytest -q`: 42 tests passed; pre-existing `datetime.utcnow()` deprecation warnings remain | +| Migration safety review | Passed | `005_authoritative_world_foundation.sql` is additive and contains no destructive SQL patterns | +| Non-demo startup path | Passed | Missing service configuration renders the styled recovery state with a retry control, rather than a blank frame | +| Demo DEALT path | Passed | DEALT card UI initialized and completed a deterministic failure-state transition when seeded inventory was insufficient | +| Strategy-to-action path | Passed to prerequisite gate | Map/War Room initialized; Ghost Crew target handoff opened the loadout screen and correctly required a driver plus shooter before launch | + +## Known non-blocking warnings + +The integration retains repository warnings that predate this release candidate. These include 194 frontend lint warnings, legacy static runtime asset references, large bundle chunk warnings, and backend `datetime.utcnow()` deprecation warnings. None failed lint, tests, asset checks, or the production builds. They should be scheduled as technical-debt work, not addressed through unreviewed release-candidate changes. + +## Release gates still required + +The integration branch is a reviewable candidate, not a production deployment. The following gates must be completed before merging to the default branch or applying live world-state schema changes. + +| Gate | Required outcome | +|---|---| +| Source PR #123 automated review | Resolve or explicitly waive the pending Cursor Bugbot Autofix status before final merge approval | +| Integration PR automation | Ensure its GitHub CI, Vercel, and automated review checks complete successfully | +| Human review | Review the three source scopes and this combined report together | +| Database deployment | Apply migration `005_authoritative_world_foundation.sql` first to a non-production Supabase environment and verify RLS plus repeated-RPC idempotency | +| Edge-function deployment | Configure `WORLD_TICK_SECRET` only in server-side deployment secrets before deploying the Ghost Crew tick endpoint | +| Final release decision | Merge the integration pull request only after all checks and deployment gates are accepted | + +## Recommended merge policy + +Use this integration pull request as the **only** path to merge #121, #122, and #123 into `main-tL2525`. Do not separately merge the original source PRs afterward. After the integration PR is merged and its deployment is verified, close the three source PRs as superseded and delete only their merged remote branches following a separate confirmation. diff --git a/docs/PRODUCTION_ART_PACKAGE_V1.md b/docs/PRODUCTION_ART_PACKAGE_V1.md new file mode 100644 index 0000000..15ea00e --- /dev/null +++ b/docs/PRODUCTION_ART_PACKAGE_V1.md @@ -0,0 +1,85 @@ +# Production Art Package V1 + +**Status:** Implemented on `feat/production-art-package-v1` +**Scope:** One legally portable rigged-character pipeline plus CC0 PBR surfaces for the 1208 Las Olas Babylon comparison client. + +## Outcome + +The Modern Ops renderer now loads a strict character package before the scene becomes ready. The package contains a real 65-joint humanoid rig and 12 selected gameplay animation groups in one optimized GLB. Every deployed combatant receives an independently animated rig, physical hit proxies attached to named bones, a visible weapon attached to the right-hand bone, shadows, and the existing selection marker. If any manifest, GLB, skeleton, or animation requirement fails, the scene keeps the previously verified articulated fallback and records a diagnostic instead of crashing. + +The 1208 street now uses compact CC0 asphalt, concrete, and brick color, OpenGL-normal, and roughness maps. These replace flat colors while preserving the authoritative grid, cover metadata, vehicle metadata, line-of-sight, cameras, and result boundary. + +| Package component | Runtime result | +|---|---:| +| Optimized rigged character GLB | 1,358,092 bytes | +| Character skeleton | 65 joints | +| Included animation groups | 12 | +| CC0 PBR maps | 9 WebP files, approximately 278 KB | +| Total package directory | 13 files, 1.57 MB | +| Global shipped asset total | 7.38 MB of 20 MB | +| Package schemas validated | 5 packages across 4 schemas | + +## Included Character States + +| Semantic state | Packaged animation | +|---|---| +| Idle | `Idle_Loop` | +| Walk | `Walk_Loop` | +| Sprint | `Sprint_Loop` | +| Crouch | `Crouch_Idle_Loop` | +| Aim | `Pistol_Aim_Neutral` | +| Fire | `Pistol_Shoot` | +| Reload | `Pistol_Reload` | +| Hit | `Hit_Chest` | +| Downed | `Death01` | + +The package contract also supplies controlled fallbacks for strafe, aim-walk, and cover transitions until an authored final rig includes dedicated clips. + +## Runtime Architecture + +`build-character-package.mjs` verifies that the base model and animation source have identical joint-name sets, copies only required animation channels, deduplicates and prunes data, and emits one intermediate GLB. The documented optimization pass converts textures to 512-pixel WebP while disabling mesh compression, simplification, joining, flattening, instancing, and palette operations that could disturb the rig or named bones. + +`loadPackagedCharacterTemplate()` loads the GLB once into a Babylon `AssetContainer`. It validates every animation required by the package, then clones the skeleton and animation groups for each combatant. Renderer-visible meshes are non-pickable; invisible boxes attached to the authored head, torso, arm, and leg bones provide stable renderer-neutral hit-zone candidates. This prevents texture or mesh changes from altering the shared damage contract. + +The canvas exposes only the active package ID as a diagnostic. The browser gauntlet requires `character.universal-male.pipeline-v1` before accepting the TPS, FPS, tactical, possession, firing, result, and return evidence. + +## Provenance and Reproducibility + +The character and animations are from Quaternius CC0 standard packages. The runtime package includes the creator-provided CC0 license. The road, concrete, and brick maps are from ambientCG CC0 archives, with per-source archive and runtime-file checksums in `pbr-sources.json`. Acquisition references and source checksums are recorded in `PRODUCTION_ART_SOURCES.md`. + +No marketplace, ripped GTA/FiveM content, login-bound Mixamo asset, or unverified mirror is included. The source archives are deliberately excluded from Git; the repository contains the deterministic build tools, optimized runtime package, exact license, package manifest, and provenance locks. + +## Verification + +The isolated live-browser gauntlet proved all of the following in WebGL2: + +| Gate | Result | +|---|---| +| Production package active | Passed | +| Third-person possession | Passed | +| Switch from Lil Dre to Kilo | Passed | +| First-person camera and firing | Passed | +| Tactical commander view | Passed | +| Result produced and applied once | Passed | +| Return to strategy block | Passed | +| Fatal local runtime exceptions | 0 | + +The focused unit test verifies the shipped manifest, CC0 license, engine portability, named skeleton hit zones, fire/reload/downed clips, and a GLB smaller than 1.5 MB. Package validation and global asset-budget auditing are part of the existing quality gate. + +## Visual Limitation and Decision + +The free standard character is a pipeline stand-in, **not final art**. Its only male full-body option is a bare-torso superhero model. Two procedural streetwear overlay experiments were rejected after live TPS review because their bone-local orientation produced inferior silhouettes. Those overlays are not included. + +This milestone proves that a commissioned or licensed clothed member can drop into the game without rewriting combat, possession, hit resolution, animation dispatch, FPS/TPS/tactical cameras, or persistence. The next paid or commissioned deliverable should target this exact contract: + +| Required final deliverable | Acceptance gate | +|---|---| +| One recognizable clothed hero | Matches Contacts identity in face, hair, silhouette, and palette | +| One visually distinct rival | Reads correctly at tactical and TPS distances | +| Same 65-bone-compatible or mapped rig | Imports through package validation without gameplay changes | +| Dedicated locomotion and gun set | Idle, walk, sprint, aim, aim-walk, fire, reload, hit, downed, crouch, and cover | +| Named hit bones and right-hand weapon socket | Passes existing physical-ray browser test | +| Source and cross-engine rights | Editable source, FBX/GLB, no engine restriction, commercial redistribution in compiled game | +| Web LOD0 | Under 65,000 triangles, 120 bones, 2–4K source textures with a 512–1024 web export | + +The user should approve a final character source only after it is tested against this package, not on screenshots alone. diff --git a/docs/PRODUCTION_ART_SOURCES.md b/docs/PRODUCTION_ART_SOURCES.md new file mode 100644 index 0000000..17deb33 --- /dev/null +++ b/docs/PRODUCTION_ART_SOURCES.md @@ -0,0 +1,29 @@ +# Production Art Sources — Portable Package V1 + +**Author:** Manus AI +**Date:** 2026-08-29 + +This milestone uses only primary-source assets that can be incorporated into a commercial cross-engine pipeline without an additional purchase. The assets remain visually provisional; their purpose is to exercise the real rig, animation, GLB, PBR, provenance, and optimization path that final commissioned assets must follow. + +| Source | Selected content | License and portability | Acquisition evidence | +|---|---|---|---| +| Quaternius Universal Base Characters | Free standard package; selected `Superhero_Male_FullBody` humanoid base; glTF/FBX and textures | CC0; commercial use allowed; humanoid rig intended for retargeting in Babylon/web, Unreal, Unity, and Godot [1] | Official itch product ID `3822259`; free upload ID `15861669`; archive 128,968,391 bytes; SHA-256 `fdbf1804c90dfc1ea03e992bff7da2dfd1a79318e13270a660180f9308455f40` | +| Quaternius Universal Animation Library | Free standard package; 43 animation groups including idle, walk, jog, sprint, pistol aim/fire/reload, hit, crouch, driving, and death | CC0; commercial use allowed; current library documents compatibility with the Universal Base Characters rig and Unreal/Godot/Unity exports [2] | Free upload ID `17958403`; archive 15,904,933 bytes; SHA-256 `cc73fc4e495b82958207316596317a3f40b9fa38065bde1027937452da537724` | +| ambientCG | `Asphalt033`, `Concrete034`, and `Bricks097` 1K JPG PBR packages | CC0; downloadable maps may be modified, distributed, and included in a commercial game [3] | Official API archives; SHA-256: Asphalt `c71801b342dbea594dbdd0bd2ddc0a6d13f813c923fca408b9f5b9ee5e58aba2`; Concrete `5839d284d94ffb8d2a56df742ec522b13dd311c52dbd42b8fd33f0409ceedb81`; Bricks `97b5df360161e48bcfed609aef361e8115b680ddc0aea4ad9321d4b00e222ad0`. Runtime-map checksums ship in `pbr-sources.json`. | +| Kenney City Kit Roads | Modular road, sidewalk, signs, traffic lights, utility poles, wires, construction props, and dumpster GLB/FBX models | CC0; personal and commercial use; no attribution required [4] [5] | Official archive SHA-256 `22058af3d68173a7cf9bda9f0e243a8cef6bd68168c302ebc76327063849674e`; evaluated but not imported in V1 because the PBR procedural block retained the existing semantic anchors. | + +The Quaternius base character and Universal Animation Library contain matching 65-joint skeleton name sets. The selected base model has 69 total nodes and no embedded animations. The animation GLB has 67 nodes and 43 animation groups. This exact-name compatibility permits deterministic transfer or runtime retargeting rather than manual per-clip remapping. + +The emitted runtime character GLB has SHA-256 `f800a143bec5241cf21090c0921eedaaf6f2bc9c73261462b197668b2037f06b`. + +The Quaternius source glTF references `T_Hair_1_Normal_png.png` and `T_Eye_Normal_png.png`, while the standard archive contains `T_Hair_1_Normal.png` and `T_Eye_Normal.png`. The packaging step must normalize those two URIs or provide deterministic aliases. This source mismatch must remain documented rather than silently repaired by hand. + +The free character is a **real rigged asset but not the target hero design**. It is suitable for proving animation, possession, hit-zone, LOD, and cross-engine import contracts. It must not be presented as the final recognizable member. The no-purchase road kit is similarly useful for semantic props and modular validation but is not a substitute for the commissioned high-fidelity 1208 block. + +## References + +[1]: https://quaternius.com/packs/universalbasecharacters.html "Quaternius — Universal Base Characters" +[2]: https://quaternius.itch.io/universal-animation-library "Quaternius — Universal Animation Library" +[3]: https://docs.ambientcg.com/license/ "ambientCG License" +[4]: https://kenney.nl/assets/city-kit-roads "Kenney — City Kit Roads" +[5]: https://kenney.nl/support "Kenney Support — Commercial Use and Attribution" diff --git a/docs/PRODUCTION_ART_V1_VERIFICATION.md b/docs/PRODUCTION_ART_V1_VERIFICATION.md new file mode 100644 index 0000000..881fd41 --- /dev/null +++ b/docs/PRODUCTION_ART_V1_VERIFICATION.md @@ -0,0 +1,53 @@ +# Production Art V1 — Verification + +**Branch:** `feat/production-art-package-v1` +**Date:** 2026-08-29 + +## Automated Gates + +| Gate | Result | +|---|---| +| ESLint | 0 errors; 194 pre-existing warnings | +| TypeScript | Passed | +| Vitest | 48 files, 708 tests passed | +| Runtime image and package budget | 110 manifest assets plus 13 package files; 7.38 MB / 20 MB; 0 errors; 0 warnings | +| Package schema validation | 5 packages against 4 schemas passed | +| Production build | Passed in 23.63 seconds after browser/acquisition process cleanup; passed again after automated-review fixes | +| Backend offline pytest | 42 passed; 93 existing `datetime.utcnow()` warnings | + +The first production-build attempt terminated during chunk rendering under sandbox memory pressure after all frontend tests and asset gates had passed. No code change was made to conceal it. The temporary development server and completed acquisition browsers were stopped; the identical production source then built successfully with 2.8 GB available. + +## Live WebGL2 Gauntlet + +The reusable `scripts/verify-showdown.mjs` gate now resolves its output directory relative to the repository and requires the loaded canvas package ID to equal `character.universal-male.pipeline-v1`. + +| Interaction | Result | +|---|---| +| Real strategy shell to 1208 Strip and OPS 3D launch | Passed | +| Production character package active | Passed | +| Third-person possession | Passed | +| Possession transfer: Lil Dre → Kilo | Passed | +| First-person camera and physical fire | Passed | +| Tactical commander camera | Passed | +| Retreat result generated | Passed | +| Result applied once to block | Passed | +| Return to strategy view | Passed | +| Fatal local runtime exceptions | 0 | + +Final screenshots are stored in `docs/evidence/production-art-v1/`. + +## Asset Pipeline Gates + +The runtime package is 1,358,092 bytes and contains 65 named joints plus 12 animation groups. A unit test enforces a GLB smaller than 1.5 MB, CC0 provenance, zero engine restrictions, named head/arm hit-zone mapping, and required fire, reload, and downed clips. The package validator verifies the runtime JSON against `character-package.schema.json` and confirms the referenced GLB exists. + +The asset audit now counts all files under `public/assets/packages` in the global shipping budget without treating package manifests, licenses, PBR maps, or GLBs as unregistered image orphans. + +## Automated Review Remediation + +Cursor Bugbot identified two valid lifecycle issues on PR #121. First, an interrupted transient animation could finish later and restart idle over a newer clip. The completion callback now returns unless its animation group is still the active group. Second, invisible physical hit proxies could remain pickable after an actor was downed or while the possessed body was hidden in first person. Snapshot and camera synchronization now toggle both rendered meshes and hit proxies together. + +After both fixes, TypeScript, all 708 tests, both asset gates, the production build, and the complete WebGL2 gauntlet passed again. The final browser evidence reports the production package active, TPS/FPS/tactical continuity, member transfer, result application and strategy return, with zero fatal local runtime exceptions. + +## Honest Visual Gate + +The final captures prove the rigged-character and PBR pipeline but do not meet the cinematic visual target. The free source character’s superhero costume is unsuitable for recognizable street members. Two bone-attached primitive-clothing experiments were tested and removed after live TPS review because they degraded silhouette. The next character must be licensed or commissioned against the package contract and judged in all three cameras. diff --git a/docs/PRODUCTION_ART_VISUAL_REVIEW.md b/docs/PRODUCTION_ART_VISUAL_REVIEW.md new file mode 100644 index 0000000..a5200e0 --- /dev/null +++ b/docs/PRODUCTION_ART_VISUAL_REVIEW.md @@ -0,0 +1,14 @@ +# Production Art V1 — Visual Review + +**Evidence date:** 2026-08-29 +**Final captures:** `docs/evidence/production-art-v1/` + +The runtime conclusively loads `character.universal-male.pipeline-v1` and renders independently animated rigs in tactical, first-person, and third-person modes. Scale, ground placement, physical bone hit zones, right-hand weapon attachment, camera possession, member switching, PBR road/concrete/brick response, and the exactly-once result return are functional. This is a production-pipeline improvement over capsules and renderer-owned character assumptions. + +The selected free standard Quaternius asset is not suitable as final member art. Its only male full-body option is a bare-torso superhero wearing briefs. The underlying UV and final TPS capture confirm that the asset demonstrates the rig and animation path but does not represent the game’s character-driven streetwear vision. + +Two bone-attached procedural clothing experiments were tested in the live TPS view. Although both followed the skeleton and preserved the browser gauntlet, their local bone orientation produced oversized torsos and disconnected shoes, degrading silhouette and member identity. Both experiments were removed. This confirms that procedural clothing cannot rescue an unsuitable source body at target quality. + +The environment’s imported asphalt, concrete, and brick maps create denser surfaces and preserve the invisible gameplay grid. The block still remains below the visual target in modeled storefront depth, background-city density, wet reflections, vegetation, vehicle fidelity, lighting complexity, and authored human detail. + +The approved next visual gate is therefore one authored clothed hero and one distinct rival that satisfy `character-package.schema.json`. They must be evaluated live in tactical, FPS, and TPS—not as standalone renders—before the roster is scaled. diff --git a/docs/PROJECT_LOG.md b/docs/PROJECT_LOG.md index 5e83333..915eb1f 100644 --- a/docs/PROJECT_LOG.md +++ b/docs/PROJECT_LOG.md @@ -68,6 +68,38 @@ Payments/monetization P0s are separately listed in `docs/MVP_STATUS_AND_DEV_PLAN ## Log +### 2026-08-29 — Portable production-art package v1 + +- Merged PRs #119 and #120 in order, then created `feat/production-art-package-v1` + from the updated protected default branch. The Modern Ops contracts, aimed fire, + tactical/FPS/TPS possession, and exactly-once result boundary remain unchanged. +- Acquired the free Quaternius Universal Base Characters and Universal Animation + Library packages through their official zero-price itch paths. Their included + creator license is CC0 1.0. A deterministic build script verifies the matching + 65-joint rigs and copies 12 selected locomotion, pistol, hit, crouch, and death + clips into one optimized 1,358,092-byte GLB. +- Modern Ops now loads the strict runtime package before scene readiness, parses the + GLB once into an `AssetContainer`, clones a rig and animation groups per combatant, + attaches physical head/torso/arm/leg hit proxies to named bones, attaches the + presentation weapon to `hand_r`, and drives animation from existing combat events. + Any package failure keeps the verified articulated fallback instead of crashing. +- Added ambientCG CC0 asphalt, concrete, and brick color/normal/roughness maps to the + 1208 block. Nine 512px WebP maps replace flat street materials. Package files now + count against the global asset gate: 13 files / 1.57 MB, total shipped assets + 7.38 MB / 20 MB, zero audit warnings. +- The live isolated WebGL2 gauntlet verified the production package ID, TPS, FPS, + tactical commander view, possession transfer from Lil Dre to Kilo, firing, result + application, and return to strategy with zero local runtime exceptions. A + procedural bone-attached streetwear experiment was rejected and removed after + TPS review; the free superhero model proves the pipeline but is not final art. + Final character procurement must now satisfy the package specification in + `docs/PRODUCTION_ART_PACKAGE_V1.md` rather than being selected from screenshots. +- PR #121 review found and resolved two lifecycle defects before merge: interrupted + transient clips can no longer restart idle over a newer animation, and downed or + first-person-hidden member hit proxies now become non-pickable with their rendered + meshes. TypeScript, 708 tests, both asset gates, the production build, and the full + WebGL2 gauntlet passed again after the fixes. + ### 2026-08-29 — 1208 Las Olas visual showdown foundation and multiview verification - Stacked the showdown branch on PR #119 rather than rewriting its verified Modern Ops diff --git a/docs/evidence/production-art-v1/fps-rigged-character-pbr.jpg b/docs/evidence/production-art-v1/fps-rigged-character-pbr.jpg new file mode 100644 index 0000000..210c963 Binary files /dev/null and b/docs/evidence/production-art-v1/fps-rigged-character-pbr.jpg differ diff --git a/docs/evidence/production-art-v1/result-boundary.jpg b/docs/evidence/production-art-v1/result-boundary.jpg new file mode 100644 index 0000000..e9b7068 Binary files /dev/null and b/docs/evidence/production-art-v1/result-boundary.jpg differ diff --git a/docs/evidence/production-art-v1/strategy-return.jpg b/docs/evidence/production-art-v1/strategy-return.jpg new file mode 100644 index 0000000..bb91b5d Binary files /dev/null and b/docs/evidence/production-art-v1/strategy-return.jpg differ diff --git a/docs/evidence/production-art-v1/tactical-rigged-character-pbr.jpg b/docs/evidence/production-art-v1/tactical-rigged-character-pbr.jpg new file mode 100644 index 0000000..8579e7a Binary files /dev/null and b/docs/evidence/production-art-v1/tactical-rigged-character-pbr.jpg differ diff --git a/docs/evidence/production-art-v1/tps-rigged-character-pbr.jpg b/docs/evidence/production-art-v1/tps-rigged-character-pbr.jpg new file mode 100644 index 0000000..604938d Binary files /dev/null and b/docs/evidence/production-art-v1/tps-rigged-character-pbr.jpg differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d56c7a3..8f589e3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,9 @@ "zustand": "^4.4.7" }, "devDependencies": { + "@gltf-transform/cli": "^4.4.2", + "@gltf-transform/core": "^4.4.2", + "@gltf-transform/functions": "^4.4.2", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -467,6 +470,16 @@ "specificity": "bin/cli.js" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", @@ -607,6 +620,198 @@ "node": ">=20.19.0" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@donmccurdy/caporal": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@donmccurdy/caporal/-/caporal-0.0.10.tgz", + "integrity": "sha512-VdaJjOqYFYcDTM5We2x8cL+B+U9UFQvq0iW2ypqEGX31SDf2oYFjEg7INEJorBwJJnFNXV8jXoYGzJyLeYl1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "^8.1.0", + "@types/lodash": "^4.14.197", + "@types/node": "20.5.6", + "@types/table": "5.0.0", + "@types/wrap-ansi": "^8.0.1", + "chalk": "3.0.0", + "glob": "^10.3.3", + "lodash": "^4.17.21", + "table": "5.4.6", + "winston": "3.10.0", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/@types/node": { + "version": "20.5.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", + "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@donmccurdy/caporal/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@donmccurdy/caporal/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@donmccurdy/caporal/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/@emnapi/runtime": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", @@ -1206,82 +1411,53 @@ } } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "node_modules/@gltf-transform/cli": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gltf-transform/cli/-/cli-4.4.2.tgz", + "integrity": "sha512-DBez2yawK9uXjefRBOJfFxkeTl2Jr4BZG8oOtHZd8if5KQaIdGN99ombzjf2ugjC7Lms9QSpY2pTBANE3H+kGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@donmccurdy/caporal": "~0.0.10", + "@gltf-transform/core": "^4.4.2", + "@gltf-transform/extensions": "^4.4.2", + "@gltf-transform/functions": "^4.4.2", + "@types/language-tags": "~1.0.4", + "@types/micromatch": "~4.0.10", + "@types/node-fetch": "~2.6.13", + "@types/prompts": "^2.4.9", + "@types/tmp": "~0.2.6", + "cli-table3": "~0.6.5", + "csv-stringify": "~6.6.0", + "draco3dgltf": "~1.5.7", + "gltf-validator": "~2.0.0-dev.3.10", + "keyframe-resample": "~0.1.0", + "ktx-parse": "^1.1.0", + "language-tags": "^2.1.0", + "listr2": "~8.3.3", + "meshoptimizer": "~1.0.1", + "micromatch": "~4.0.8", + "mikktspace": "~1.1.1", + "node-fetch": "~3.3.2", + "prompts": "^2.4.2", + "sharp": "~0.34.5", + "tmp": "~0.2.5", + "watlas": "^1.0.1" }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" + "bin": { + "gltf-transform": "bin/cli.js" }, "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "node": ">=20" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "url": "https://github.com/sponsors/donmccurdy" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", - "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -1292,19 +1468,19 @@ "darwin" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.2" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", - "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -1315,39 +1491,19 @@ "darwin" ], "engines": { - "node": ">=20.9.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.2" - } - }, - "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", - "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "dependencies": { - "@img/sharp-wasm32": "0.35.3" - }, - "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", - "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -1361,10 +1517,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", - "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -1378,10 +1534,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", - "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -1395,10 +1551,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", - "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -1412,10 +1568,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", - "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -1429,10 +1585,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", - "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", "cpu": [ "riscv64" ], @@ -1446,10 +1602,672 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", - "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@gltf-transform/cli/node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/@gltf-transform/core": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gltf-transform/core/-/core-4.4.2.tgz", + "integrity": "sha512-qsWKwNSwK+2s834Mt4xbYcyHqCrgNFP7hIv5s487JxebngRfDgelpghNF+kSswGb2/NuapasfK3UViFoSJJoMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "property-graph": "^4.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@gltf-transform/extensions": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gltf-transform/extensions/-/extensions-4.4.2.tgz", + "integrity": "sha512-HJH1FM+edC5eNvl6xO0SOXJ/j/3oDoIpSu150OTdJaLBoM3TgCCGIfh4wyhgWAqZrkvgHKVGiZKxcKV5LkgPCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gltf-transform/core": "^4.4.2", + "ktx-parse": "^1.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@gltf-transform/functions": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gltf-transform/functions/-/functions-4.4.2.tgz", + "integrity": "sha512-dclXgv9TshMaWBqPDUYd4xTwBQ2PpuR8p0Y9pokrRzGQDUPXRP6lTDzbqT0UmEmxFSvRyPJjvOWUmzeiRafpvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gltf-transform/core": "^4.4.2", + "@gltf-transform/extensions": "^4.4.2", + "ktx-parse": "^1.1.0", + "ndarray": "^1.0.19", + "ndarray-lanczos": "^0.3.0", + "ndarray-pixels": "^5.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/donmccurdy" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", "cpu": [ "s390x" ], @@ -1795,6 +2613,109 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2064,6 +2985,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@remix-run/router": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", @@ -2442,6 +3374,17 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -2698,6 +3641,13 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/braces": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", + "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -2723,6 +3673,17 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -2745,12 +3706,50 @@ "@types/node": "*" } }, + "node_modules/@types/language-tags": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/language-tags/-/language-tags-1.0.4.tgz", + "integrity": "sha512-20PQbifv3v/djCT+KlXybv0KqO5ofoR1qD1wkinN59kfggTPVTWGmPFgL/1yWuDyRcsQP/POvkqK+fnl5nOwTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/micromatch": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", + "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/braces": "*" + } + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "license": "MIT" }, + "node_modules/@types/ndarray": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/ndarray/-/ndarray-1.1.0.tgz", + "integrity": "sha512-go+C6vHBUuO9jH6Tj6qEZ63JEGoMSxFdmd+6QAtthGD6xumBfIz3RbNDDs2aoRVLf9naH8lk5jnWy2UHHKgYQQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", @@ -2760,12 +3759,51 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "license": "MIT" }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -2810,6 +3848,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/table": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/table/-/table-5.0.0.tgz", + "integrity": "sha512-fQLtGLZXor264zUPWI95WNDsZ3QV43/c0lJpR/h1hhLJumXRmHNsrvBfEzW2YMhb0EWCsn4U6h82IgwsajAuTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/uuid": { "version": "9.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", @@ -2817,6 +3876,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/wrap-ansi": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-8.0.2.tgz", + "integrity": "sha512-mdaFibQzYYqJF8Sc9By84eBLtDQD9Hnko0yXuezHBU5f5KdtQk+j5hPRQc1j4ayzdqGddKON6PjeuzxD5U1hPg==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -3208,6 +4274,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3308,6 +4390,23 @@ "node": ">=12" } }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3719,6 +4818,109 @@ "node": ">= 6" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3746,6 +4948,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3766,6 +4982,59 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3886,6 +5155,33 @@ "devOptional": true, "license": "MIT" }, + "node_modules/csv-stringify": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.6.0.tgz", + "integrity": "sha512-YW32lKOmIBgbxtu3g5SaiqWNwa/9ISQt2EcgOq0+RAIFufFp9is6tqNnKahqE5kuKvrnYAzs28r+s6pXJR8Vcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cwise-compiler": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", + "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uniq": "^1.0.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -4118,6 +5414,13 @@ "url": "https://dotenvx.com" } }, + "node_modules/draco3dgltf": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3dgltf/-/draco3dgltf-1.5.7.tgz", + "integrity": "sha512-LeqcpmoHIyYUi0z70/H3tMkGj8QhqVxq6FJGPjlzR24BNkQ6jyMheMvFKJBI0dzGZrEOUyQEmZ8axM1xRrbRiw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4138,6 +5441,13 @@ "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", "license": "ISC" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.402", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", @@ -4152,6 +5462,13 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -4196,6 +5513,19 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -4657,6 +5987,37 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4722,6 +6083,13 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -4742,6 +6110,23 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", @@ -4755,7 +6140,20 @@ "mime-types": "^2.1.35" }, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" } }, "node_modules/fraction.js": { @@ -4855,6 +6253,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -5009,6 +6420,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gltf-validator": { + "version": "2.0.0-dev.3.10", + "resolved": "https://registry.npmjs.org/gltf-validator/-/gltf-validator-2.0.0-dev.3.10.tgz", + "integrity": "sha512-odJ4k0tRkGXiDGn78yDBg+fBbAIvBnXxh3RwAta0emSxGtyagFE8B4xELB1oYe3S5RD8Ci3uZAsZaascH2LAEQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5260,6 +6678,13 @@ "dev": true, "license": "ISC" }, + "node_modules/iota-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", + "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5279,6 +6704,13 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -5363,6 +6795,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5370,6 +6815,22 @@ "dev": true, "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -5524,92 +6985,419 @@ "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", "license": "ISC" }, + "node_modules/keyframe-resample": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/keyframe-resample/-/keyframe-resample-0.1.0.tgz", + "integrity": "sha512-z1au6Q6qCWP734q44t/5SyGwNydC7MvOWo2ZETAz7ZqAgtcyb5EGO7jwgk3DEH7CaDtb0DkGec9iwVpeDWX5dA==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ktx-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ktx-parse/-/ktx-parse-1.1.0.tgz", + "integrity": "sha512-mKp3y+FaYgR7mXWAbyyzpa/r1zDWeaunH+INJO4fou3hb45XuNSwar+7llrRyvpMWafxSIi99RNFJ05MHedaJQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-2.1.0.tgz", + "integrity": "sha512-D4CgpyCt+61f6z2jHjJS1OmZPviAWM57iJ9OKdFFWSNgS7Udj9QVWqyGs/cveVNF57XpZmhSvMdVIV5mjLA7Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { - "node": ">=14" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } }, "node_modules/loose-envify": { "version": "1.4.0", @@ -5799,6 +7587,13 @@ "node": ">= 8" } }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "dev": true, + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -5813,6 +7608,13 @@ "node": ">=8.6" } }, + "node_modules/mikktspace": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/mikktspace/-/mikktspace-1.1.1.tgz", + "integrity": "sha512-w+n5O2YLFsv/aRi3QUF0jxR+mEsl2J08ZkRItJHE08MWqiDIy8amyZN9vetBfbLhlWGMb8G3mzSPwtKHvBF7hQ==", + "dev": true, + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -5834,6 +7636,19 @@ "node": ">= 0.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -5891,6 +7706,16 @@ "node": ">= 6" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5940,6 +7765,91 @@ "dev": true, "license": "MIT" }, + "node_modules/ndarray": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.1.1.tgz", + "integrity": "sha512-AsE8w/M2MS+0pD3nP5qbRRtJFeJJJaJ+qXZaZr0XjVGXhy6LIj2TERTlkpdK2AI3WufoOP+pwPk3q2ilAJcZ9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "iota-array": "^1.0.0", + "is-buffer": "^1.0.2" + } + }, + "node_modules/ndarray-lanczos": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ndarray-lanczos/-/ndarray-lanczos-0.3.0.tgz", + "integrity": "sha512-5kBmmG3Zvyj77qxIAC4QFLKuYdDIBJwCG+DukT6jQHNa1Ft74/hPH1z5mbQXeHBt8yvGPBGVrr3wEOdJPYYZYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ndarray": "^1.0.11", + "ndarray": "^1.0.19" + } + }, + "node_modules/ndarray-ops": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ndarray-ops/-/ndarray-ops-1.2.2.tgz", + "integrity": "sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cwise-compiler": "^1.0.0" + } + }, + "node_modules/ndarray-pixels": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ndarray-pixels/-/ndarray-pixels-5.2.0.tgz", + "integrity": "sha512-lTh4tFKziAatVTa9crIsidUyn+lqujVOQpzfdBWvdFu2wo9Uo6z261lVX7SgMyP89xGmj3TMTPbbxl9YDnV4SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ndarray": "^1.0.14", + "ndarray": "^1.0.19", + "ndarray-ops": "^1.2.2", + "sharp": "^0.35.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -6016,6 +7926,32 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6084,6 +8020,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6163,6 +8106,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6465,6 +8432,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-graph": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/property-graph/-/property-graph-4.1.0.tgz", + "integrity": "sha512-AvPcP7XECNWy4LGmFQ77k7un4lSKM4eS29PTvW4ck95uYeLxXPWJM7hLuBqK91FaHqCcgJvIUCuNJjjxKE7VKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/protocol-buffers-schema": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", @@ -6753,6 +8741,21 @@ "node": ">=8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6857,6 +8860,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6868,6 +8888,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6931,10 +8958,44 @@ "fsevents": "~2.3.2" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true, "funding": [ { @@ -6950,19 +9011,16 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } + "license": "MIT" }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/saxes": { @@ -7092,6 +9150,26 @@ "dev": true, "license": "ISC" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -7102,6 +9180,49 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/socket.io-client": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", @@ -7199,6 +9320,16 @@ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -7213,6 +9344,16 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7228,6 +9369,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7241,6 +9398,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -7340,6 +9511,146 @@ "dev": true, "license": "MIT" }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/table/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -7404,6 +9715,13 @@ "dev": true, "license": "MIT" }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7552,6 +9870,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7610,6 +9938,16 @@ "node": ">=8" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -7711,6 +10049,13 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", + "dev": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -8384,6 +10729,23 @@ "node": ">=18" } }, + "node_modules/watlas": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/watlas/-/watlas-1.0.1.tgz", + "integrity": "sha512-TmB++BFqEQY19cPNby5iszACZd8t6uauB7YdObkqhWarhed79OMPS8wufoDSo0p85A/8QW/aqZwEJf+nD4emSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", @@ -8452,6 +10814,44 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz", + "integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8480,6 +10880,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index f2e764e..6562592 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,9 @@ "zustand": "^4.4.7" }, "devDependencies": { + "@gltf-transform/cli": "^4.4.2", + "@gltf-transform/core": "^4.4.2", + "@gltf-transform/functions": "^4.4.2", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-color.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-color.webp new file mode 100644 index 0000000..493cdcd Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-color.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-normal.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-normal.webp new file mode 100644 index 0000000..b5a523f Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-normal.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-roughness.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-roughness.webp new file mode 100644 index 0000000..a7a3d6f Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/asphalt033-roughness.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-color.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-color.webp new file mode 100644 index 0000000..d42dd7f Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-color.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-normal.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-normal.webp new file mode 100644 index 0000000..1573cb2 Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-normal.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-roughness.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-roughness.webp new file mode 100644 index 0000000..a89c5c3 Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/bricks097-roughness.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-color.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-color.webp new file mode 100644 index 0000000..a10f37e Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-color.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-normal.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-normal.webp new file mode 100644 index 0000000..8a105ca Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-normal.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-roughness.webp b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-roughness.webp new file mode 100644 index 0000000..bd63da2 Binary files /dev/null and b/frontend/public/assets/packages/blocks/las-olas-1208/materials/concrete034-roughness.webp differ diff --git a/frontend/public/assets/packages/blocks/las-olas-1208/materials/pbr-sources.json b/frontend/public/assets/packages/blocks/las-olas-1208/materials/pbr-sources.json new file mode 100644 index 0000000..60a9a97 --- /dev/null +++ b/frontend/public/assets/packages/blocks/las-olas-1208/materials/pbr-sources.json @@ -0,0 +1,114 @@ +{ + "schemaVersion": 1, + "generator": "frontend/scripts/assets/prepare-pbr-textures.py", + "assets": [ + { + "assetId": "Asphalt033", + "sourceArchive": "Asphalt033_1K-JPG.zip", + "sourceSha256": "c71801b342dbea594dbdd0bd2ddc0a6d13f813c923fca408b9f5b9ee5e58aba2", + "license": "CC0-1.0", + "sourceUrl": "https://ambientcg.com/a/Asphalt033", + "maps": [ + { + "map": "color", + "sourceFile": "Asphalt033_1K-JPG_Color.jpg", + "runtimeFile": "asphalt033-color.webp", + "width": 512, + "height": 512, + "bytes": 17566, + "sha256": "b26fb79701b95061400dc92a72c86009dfe7e68264e7931c0b0c498ee9542875" + }, + { + "map": "normal", + "sourceFile": "Asphalt033_1K-JPG_NormalGL.jpg", + "runtimeFile": "asphalt033-normal.webp", + "width": 512, + "height": 512, + "bytes": 71870, + "sha256": "ac3862f5eb29b26aaa303bfda1678fd10551d0eed91681f77fa5e80803e2cf39" + }, + { + "map": "roughness", + "sourceFile": "Asphalt033_1K-JPG_Roughness.jpg", + "runtimeFile": "asphalt033-roughness.webp", + "width": 512, + "height": 512, + "bytes": 46458, + "sha256": "d65ddbab9f86a9bee19aaed58256ef7db341a802d93064d7388b5fa0b0b8000c" + } + ] + }, + { + "assetId": "Concrete034", + "sourceArchive": "Concrete034_1K-JPG.zip", + "sourceSha256": "5839d284d94ffb8d2a56df742ec522b13dd311c52dbd42b8fd33f0409ceedb81", + "license": "CC0-1.0", + "sourceUrl": "https://ambientcg.com/a/Concrete034", + "maps": [ + { + "map": "color", + "sourceFile": "Concrete034_1K-JPG_Color.jpg", + "runtimeFile": "concrete034-color.webp", + "width": 512, + "height": 256, + "bytes": 7814, + "sha256": "09399e857ac48047d95d9d1023b6cc756726a94edcaa27d6ce17718c4dcba39c" + }, + { + "map": "normal", + "sourceFile": "Concrete034_1K-JPG_NormalGL.jpg", + "runtimeFile": "concrete034-normal.webp", + "width": 512, + "height": 256, + "bytes": 17534, + "sha256": "111af614841a597e7d767506208b6b710fbfae756aae81d331d5d5c65fffb32c" + }, + { + "map": "roughness", + "sourceFile": "Concrete034_1K-JPG_Roughness.jpg", + "runtimeFile": "concrete034-roughness.webp", + "width": 512, + "height": 256, + "bytes": 10072, + "sha256": "7fa4e905894fd98539e3d83ee88e7dc7c4ee2317cd96b660a417a464645cf34f" + } + ] + }, + { + "assetId": "Bricks097", + "sourceArchive": "Bricks097_1K-JPG.zip", + "sourceSha256": "97b5df360161e48bcfed609aef361e8115b680ddc0aea4ad9321d4b00e222ad0", + "license": "CC0-1.0", + "sourceUrl": "https://ambientcg.com/a/Bricks097", + "maps": [ + { + "map": "color", + "sourceFile": "Bricks097_1K-JPG_Color.jpg", + "runtimeFile": "bricks097-color.webp", + "width": 512, + "height": 256, + "bytes": 41352, + "sha256": "37203da23d0c3a4af18293246e1cbe9cf809f7755100dc11873281e40047c676" + }, + { + "map": "normal", + "sourceFile": "Bricks097_1K-JPG_NormalGL.jpg", + "runtimeFile": "bricks097-normal.webp", + "width": 512, + "height": 256, + "bytes": 29292, + "sha256": "d6e25eec6d928e34da65613ebc8f29b4cfb92d53c1846e7d1f4c40e6e61ebe1f" + }, + { + "map": "roughness", + "sourceFile": "Bricks097_1K-JPG_Roughness.jpg", + "runtimeFile": "bricks097-roughness.webp", + "width": 512, + "height": 256, + "bytes": 35868, + "sha256": "afb8fc4a1a52246a4d7e1fbc6e9189c350f3d2cc7b6da169111c7bdc526dbbec" + } + ] + } + ] +} diff --git a/frontend/public/assets/packages/characters/universal-male/LICENSE-QUATERNIUS-CC0.txt b/frontend/public/assets/packages/characters/universal-male/LICENSE-QUATERNIUS-CC0.txt new file mode 100644 index 0000000..26189f4 --- /dev/null +++ b/frontend/public/assets/packages/characters/universal-male/LICENSE-QUATERNIUS-CC0.txt @@ -0,0 +1,22 @@ +This is the standard FREE version of the Universal Base Characters Kit, which only contains +a portion of the models . You can buy the SOURCE version which has all the models with rigged .blends, +and Unity(URP), Unreal Engine and Godot projects +with custom shaders already set up. + +You can get the other version from the website https://quaternius.com + +------------------------------------------------------- +License: +CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication +https://creativecommons.org/publicdomain/zero/1.0/ + +------------------------------------------------------ +Models by @Quaternius +Consider supporting me on Patreon! + +https://www.patreon.com/quaternius + +------------------------------------------------------- +Join the Discord Server: +https://discord.gg/vJqnRUYRfT diff --git a/frontend/public/assets/packages/characters/universal-male/package.v1.json b/frontend/public/assets/packages/characters/universal-male/package.v1.json new file mode 100644 index 0000000..a5340bb --- /dev/null +++ b/frontend/public/assets/packages/characters/universal-male/package.v1.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": 1, + "packageId": "character.universal-male.pipeline-v1", + "memberId": "shared-male-production-base", + "displayName": "Universal Male Production Base", + "role": "shooter", + "wardrobeIds": ["quaternius.superhero-male.dark"], + "materialVariantIds": ["skin.deep", "outfit.dark"], + "portraitAssetId": "generated.characters.portraits.character_shooter_male_portrait_v001", + "source": { + "editableFiles": [ + "external/quaternius/Universal Base Characters[Standard].zip", + "external/quaternius/Universal Animation Library[Standard].zip" + ], + "exportFbx": "external/quaternius/Universal Base Characters[Standard]/Base Characters/Unity/Superhero_Male_FullBody.fbx" + }, + "runtime": { + "babylonGlb": "/assets/packages/characters/universal-male/universal-male.v1.glb", + "unrealSkeletalMesh": "/Game/Dealt/Characters/UniversalMale/SK_UniversalMale" + }, + "skeleton": { + "profileId": "quaternius.universal-humanoid.v3", + "rootBone": "root", + "forwardAxis": "+Z", + "upAxis": "+Y", + "metersPerUnit": 1 + }, + "hitZones": { + "head": "Head", + "torso": "spine_03", + "leftArm": "upperarm_l", + "rightArm": "upperarm_r", + "leftLeg": "thigh_l", + "rightLeg": "thigh_r" + }, + "animations": { + "idle": "Idle_Loop", + "walk": "Walk_Loop", + "sprint": "Sprint_Loop", + "strafe": "Walk_Loop", + "aim": "Pistol_Aim_Neutral", + "aimWalk": "Jog_Fwd_Loop", + "fire": "Pistol_Shoot", + "reload": "Pistol_Reload", + "crouch": "Crouch_Idle_Loop", + "hit": "Hit_Chest", + "downed": "Death01", + "coverEnter": "Crouch_Fwd_Loop", + "coverIdle": "Crouch_Idle_Loop", + "coverExit": "Crouch_Fwd_Loop" + }, + "weaponSockets": { + "rightHand": "hand_r", + "leftHandIk": "hand_l", + "holster": "pelvis" + }, + "lods": [ + { "level": 0, "maxTriangles": 15000, "maxBones": 65, "screenCoverage": 0.35 } + ], + "provenance": { + "creator": "Quaternius", + "sourceUrl": "https://quaternius.com/packs/universalbasecharacters.html", + "licenseId": "CC0-1.0", + "commercialUse": true, + "engineRestrictions": [], + "aiInvolvement": "none", + "modificationNotes": "Free standard Universal Base Characters male model combined with selected free Universal Animation Library clips by the deterministic build-character-package.mjs pipeline. Textures were resized to 512px WebP; mesh compression and geometry simplification were disabled. Runtime SHA-256: f800a143bec5241cf21090c0921eedaaf6f2bc9c73261462b197668b2037f06b. This is a legal rigged pipeline stand-in, not the final recognizable Lil Dre art." + } +} diff --git a/frontend/public/assets/packages/characters/universal-male/universal-male.v1.glb b/frontend/public/assets/packages/characters/universal-male/universal-male.v1.glb new file mode 100644 index 0000000..9b24d0a Binary files /dev/null and b/frontend/public/assets/packages/characters/universal-male/universal-male.v1.glb differ diff --git a/frontend/scripts/assets/audit.mjs b/frontend/scripts/assets/audit.mjs index 2c9ecf0..090a846 100644 --- a/frontend/scripts/assets/audit.mjs +++ b/frontend/scripts/assets/audit.mjs @@ -25,6 +25,7 @@ import { ASSET_CLASSES, RUNTIME_BUDGET_MB, FRINGE_THRESHOLD, analysePixels } fro const JSON_OUT = process.argv.includes('--json'); const FRONTEND = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../..'); const RUNTIME_DIR = path.join(FRONTEND, 'public/assets/runtime'); +const PACKAGE_DIR = path.join(FRONTEND, 'public/assets/packages'); const MANIFEST = path.join(FRONTEND, 'src/assets/runtimeManifest.json'); const EXCEPTIONS = path.join(FRONTEND, 'scripts/assets/exceptions.json'); @@ -61,6 +62,7 @@ async function main() { let totalBytes = 0; let orphanBytes = 0; + let packageBytes = 0; let checked = 0; for (const entry of manifest.entries) { @@ -115,17 +117,22 @@ async function main() { } } await walk(RUNTIME_DIR); + const runtimeFileCount = onDisk.length; + await walk(PACKAGE_DIR); const manifestPaths = new Set(manifest.entries.map((e) => path.join(FRONTEND, 'public', e.runtimePath.replace(/^\//, '')))); for (const p of onDisk) { if (!manifestPaths.has(p)) { - warn('W_ORPHAN', `Runtime file not in manifest: ${path.relative(FRONTEND, p)}`); - // Orphans SHIP to the browser, so they must count against the - // budget. Excluding them let ~31 MB of raw PNG land on main while - // the gate still reported 5.35/20 MB. + const isPackageFile = p.startsWith(PACKAGE_DIR + path.sep); + if (!isPackageFile) warn('W_ORPHAN', `Runtime file not in manifest: ${path.relative(FRONTEND, p)}`); + // Unregistered runtime files and production-package files SHIP to + // the browser, so both must count against the global budget. Package + // contents are validated by validate-packages.mjs rather than the + // image-only runtime manifest. try { const oStat = await fs.stat(p); totalBytes += oStat.size; - orphanBytes += oStat.size; + if (isPackageFile) packageBytes += oStat.size; + else orphanBytes += oStat.size; } catch { /* unreadable file already reported elsewhere */ } } } @@ -137,7 +144,14 @@ async function main() { fail('E_BUDGET', `Runtime assets ${totalMB.toFixed(2)} MB exceed the ${budgetMB} MB budget.`); } - report({ checked, totalMB, budgetMB, orphanMB: orphanBytes / 1048576 }); + report({ + checked, + packageFiles: onDisk.length - runtimeFileCount, + packageMB: packageBytes / 1048576, + totalMB, + budgetMB, + orphanMB: orphanBytes / 1048576, + }); } function report(summary = {}) { @@ -148,6 +162,7 @@ function report(summary = {}) { console.log('─'.repeat(70)); if (summary.checked !== undefined) { console.log(`Assets checked : ${summary.checked}`); + if (summary.packageFiles) console.log(`Package files : ${summary.packageFiles} (${summary.packageMB.toFixed(2)} MB)`); console.log(`Runtime total : ${summary.totalMB.toFixed(2)} MB / ${summary.budgetMB} MB budget`); if (summary.orphanMB > 0.01) { console.log(` ...of which unregistered (orphan): ${summary.orphanMB.toFixed(2)} MB`); diff --git a/frontend/scripts/assets/build-character-package.mjs b/frontend/scripts/assets/build-character-package.mjs new file mode 100644 index 0000000..d1b63c0 --- /dev/null +++ b/frontend/scripts/assets/build-character-package.mjs @@ -0,0 +1,108 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { NodeIO } from '@gltf-transform/core'; +import { dedup, prune, resample } from '@gltf-transform/functions'; + +const [baseModelPath, animationLibraryPath, outputPath] = process.argv.slice(2); +if (!baseModelPath || !animationLibraryPath || !outputPath) { + throw new Error('Usage: node build-character-package.mjs '); +} + +const REQUIRED_ANIMATIONS = [ + 'Idle_Loop', + 'Walk_Loop', + 'Jog_Fwd_Loop', + 'Sprint_Loop', + 'Crouch_Idle_Loop', + 'Crouch_Fwd_Loop', + 'Pistol_Aim_Neutral', + 'Pistol_Shoot', + 'Pistol_Reload', + 'Hit_Chest', + 'Hit_Head', + 'Death01', +]; + +const io = new NodeIO(); +const targetDocument = await io.read(baseModelPath); +const animationDocument = await io.read(animationLibraryPath); +const targetRoot = targetDocument.getRoot(); +const animationRoot = animationDocument.getRoot(); +const targetBuffer = targetRoot.listBuffers()[0] ?? targetDocument.createBuffer('runtime-buffer'); +const targetNodes = new Map(targetRoot.listNodes().map((node) => [node.getName(), node])); +const sourceJointNames = new Set(animationRoot.listSkins().flatMap((skin) => skin.listJoints().map((joint) => joint.getName()))); +const targetJointNames = new Set(targetRoot.listSkins().flatMap((skin) => skin.listJoints().map((joint) => joint.getName()))); +const missingTargetJoints = [...sourceJointNames].filter((name) => !targetJointNames.has(name)); +const missingSourceJoints = [...targetJointNames].filter((name) => !sourceJointNames.has(name)); +if (missingTargetJoints.length || missingSourceJoints.length) { + throw new Error(`Skeleton mismatch. Missing in target: ${missingTargetJoints.join(', ')}; missing in animation source: ${missingSourceJoints.join(', ')}`); +} + +function copyAccessor(sourceAccessor, suffix) { + const sourceArray = sourceAccessor.getArray(); + if (!sourceArray) throw new Error(`Animation accessor ${sourceAccessor.getName() || suffix} has no array.`); + const targetAccessor = targetDocument + .createAccessor(`${sourceAccessor.getName() || 'animation-accessor'}-${suffix}`) + .setArray(sourceArray.slice()) + .setType(sourceAccessor.getType()) + .setNormalized(sourceAccessor.getNormalized()) + .setBuffer(targetBuffer); + return targetAccessor; +} + +const sourceAnimations = new Map(animationRoot.listAnimations().map((animation) => [animation.getName(), animation])); +const missingAnimations = REQUIRED_ANIMATIONS.filter((name) => !sourceAnimations.has(name)); +if (missingAnimations.length) throw new Error(`Animation library is missing required clips: ${missingAnimations.join(', ')}`); + +for (const animationName of REQUIRED_ANIMATIONS) { + const sourceAnimation = sourceAnimations.get(animationName); + const targetAnimation = targetDocument.createAnimation(animationName); + const samplerMap = new Map(); + const accessorMap = new Map(); + + for (const [samplerIndex, sourceSampler] of sourceAnimation.listSamplers().entries()) { + const sourceInput = sourceSampler.getInput(); + const sourceOutput = sourceSampler.getOutput(); + if (!sourceInput || !sourceOutput) throw new Error(`${animationName} contains an incomplete sampler.`); + const input = accessorMap.get(sourceInput) ?? copyAccessor(sourceInput, `${animationName}-input-${samplerIndex}`); + const output = accessorMap.get(sourceOutput) ?? copyAccessor(sourceOutput, `${animationName}-output-${samplerIndex}`); + accessorMap.set(sourceInput, input); + accessorMap.set(sourceOutput, output); + const targetSampler = targetDocument + .createAnimationSampler(`${animationName}-sampler-${samplerIndex}`) + .setInput(input) + .setOutput(output) + .setInterpolation(sourceSampler.getInterpolation()); + targetAnimation.addSampler(targetSampler); + samplerMap.set(sourceSampler, targetSampler); + } + + for (const [channelIndex, sourceChannel] of sourceAnimation.listChannels().entries()) { + const sourceTarget = sourceChannel.getTargetNode(); + const sourceSampler = sourceChannel.getSampler(); + const targetNode = sourceTarget ? targetNodes.get(sourceTarget.getName()) : null; + const targetSampler = sourceSampler ? samplerMap.get(sourceSampler) : null; + if (!sourceTarget || !targetNode || !targetSampler) { + throw new Error(`${animationName} channel ${channelIndex} cannot be mapped to the base skeleton.`); + } + targetAnimation.addChannel( + targetDocument + .createAnimationChannel(`${animationName}-channel-${channelIndex}`) + .setSampler(targetSampler) + .setTargetNode(targetNode) + .setTargetPath(sourceChannel.getTargetPath()), + ); + } +} + +await targetDocument.transform(resample(), dedup(), prune()); +await fs.mkdir(path.dirname(outputPath), { recursive: true }); +await io.write(outputPath, targetDocument); + +console.log(JSON.stringify({ + outputPath, + animationCount: targetDocument.getRoot().listAnimations().length, + animationNames: targetDocument.getRoot().listAnimations().map((animation) => animation.getName()), + jointCount: targetJointNames.size, + nodeCount: targetDocument.getRoot().listNodes().length, +})); diff --git a/frontend/scripts/assets/inspect-skeleton.mjs b/frontend/scripts/assets/inspect-skeleton.mjs new file mode 100644 index 0000000..65adae5 --- /dev/null +++ b/frontend/scripts/assets/inspect-skeleton.mjs @@ -0,0 +1,18 @@ +import { NodeIO } from '@gltf-transform/core'; + +const input = process.argv[2]; +if (!input) throw new Error('Usage: node inspect-skeleton.mjs '); +const io = new NodeIO(); +const document = await io.read(input); +const root = document.getRoot(); +const nodes = root.listNodes().map((node) => node.getName()).filter(Boolean); +const skins = root.listSkins().map((skin) => ({ + name: skin.getName(), + joints: skin.listJoints().map((joint) => joint.getName()), +})); +const animations = root.listAnimations().map((animation) => ({ + name: animation.getName(), + targets: [...new Set(animation.listChannels().map((channel) => channel.getTargetNode()?.getName()).filter(Boolean))], + paths: [...new Set(animation.listChannels().map((channel) => channel.getTargetPath()))], +})); +console.log(JSON.stringify({ input, nodes, skins, animations }, null, 2)); diff --git a/frontend/scripts/assets/prepare-pbr-textures.py b/frontend/scripts/assets/prepare-pbr-textures.py new file mode 100644 index 0000000..c42721e --- /dev/null +++ b/frontend/scripts/assets/prepare-pbr-textures.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Prepare compact runtime PBR maps from ambientCG 1K-JPG archives.""" + +from __future__ import annotations + +import hashlib +import json +import sys +import zipfile +from io import BytesIO +from pathlib import Path + +from PIL import Image + +MAP_SUFFIXES = { + "color": "_Color.jpg", + "normal": "_NormalGL.jpg", + "roughness": "_Roughness.jpg", +} + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def prepare_archive(archive_path: Path, output_dir: Path) -> dict[str, object]: + asset_id = archive_path.name.split("_1K-JPG.zip", 1)[0] + output_stem = asset_id.lower() + written: list[dict[str, object]] = [] + + with zipfile.ZipFile(archive_path) as archive: + names = archive.namelist() + for map_name, suffix in MAP_SUFFIXES.items(): + matches = [name for name in names if name.endswith(suffix)] + if len(matches) != 1: + raise RuntimeError(f"{archive_path}: expected one {suffix} map, found {matches}") + source_name = matches[0] + source_bytes = archive.read(source_name) + with Image.open(BytesIO(source_bytes)) as image: + image = image.convert("RGB") + image.thumbnail((512, 512), Image.Resampling.LANCZOS) + output_path = output_dir / f"{output_stem}-{map_name}.webp" + output_dir.mkdir(parents=True, exist_ok=True) + image.save(output_path, "WEBP", quality=86, method=6) + output_bytes = output_path.read_bytes() + written.append( + { + "map": map_name, + "sourceFile": source_name, + "runtimeFile": output_path.name, + "width": image.width, + "height": image.height, + "bytes": len(output_bytes), + "sha256": digest(output_bytes), + } + ) + + return { + "assetId": asset_id, + "sourceArchive": archive_path.name, + "sourceSha256": digest(archive_path.read_bytes()), + "license": "CC0-1.0", + "sourceUrl": f"https://ambientcg.com/a/{asset_id}", + "maps": written, + } + + +def main() -> None: + if len(sys.argv) < 3: + raise SystemExit("Usage: prepare-pbr-textures.py [...]") + output_dir = Path(sys.argv[1]).resolve() + archives = [Path(value).resolve() for value in sys.argv[2:]] + manifest = { + "schemaVersion": 1, + "generator": "frontend/scripts/assets/prepare-pbr-textures.py", + "assets": [prepare_archive(archive, output_dir) for archive in archives], + } + (output_dir / "pbr-sources.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + print(json.dumps(manifest, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/frontend/scripts/assets/validate-packages.mjs b/frontend/scripts/assets/validate-packages.mjs index a875d70..763ede5 100644 --- a/frontend/scripts/assets/validate-packages.mjs +++ b/frontend/scripts/assets/validate-packages.mjs @@ -46,8 +46,12 @@ const targets = [ kind: path.basename(filePath).split('.')[0], runtime: false, })), - ...collectJsonFiles(path.join(publicRoot, 'assets/packages/characters')).map((filePath) => ({ filePath, kind: 'character', runtime: true })), - ...collectJsonFiles(path.join(publicRoot, 'assets/packages/blocks')).map((filePath) => ({ filePath, kind: 'block', runtime: true })), + ...collectJsonFiles(path.join(publicRoot, 'assets/packages/characters')) + .filter((filePath) => path.basename(filePath).startsWith('package.')) + .map((filePath) => ({ filePath, kind: 'character', runtime: true })), + ...collectJsonFiles(path.join(publicRoot, 'assets/packages/blocks')) + .filter((filePath) => path.basename(filePath).startsWith('package.')) + .map((filePath) => ({ filePath, kind: 'block', runtime: true })), ]; const failures = []; diff --git a/frontend/src/App.css b/frontend/src/App.css index 4ff3712..04d5878 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -211,3 +211,85 @@ body { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } } + +/* Bootstrap failure state — must remain usable before the App module loads. */ +.startup-error-screen { + min-height: 100%; + width: 100%; + display: grid; + place-items: center; + padding: 24px; + background: + radial-gradient(circle at 50% 0%, rgba(227, 38, 54, 0.18), transparent 42%), + linear-gradient(160deg, #0d0e14 0%, #050505 62%, #16080b 100%); +} + +.startup-error-card { + width: min(100%, 440px); + border: 1px solid rgba(255, 91, 91, 0.48); + border-radius: 16px; + padding: 32px 28px; + background: rgba(10, 10, 14, 0.95); + box-shadow: 0 20px 70px rgba(0, 0, 0, 0.54), 0 0 32px rgba(255, 44, 44, 0.12); + text-align: center; +} + +.startup-error-eyebrow { + display: block; + margin-bottom: 12px; + color: #ff7777; + font-family: 'Oswald', sans-serif; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.24em; +} + +.startup-error-card h1 { + margin: 0; + color: #ffffff; + font-family: 'Bebas Neue', 'Oswald', sans-serif; + font-size: clamp(30px, 8vw, 44px); + font-weight: 400; + letter-spacing: 0.055em; + line-height: 1; +} + +.startup-error-card p { + margin: 20px auto 0; + max-width: 34ch; + color: rgba(255, 255, 255, 0.78); + font-size: 16px; + line-height: 1.5; +} + +.startup-error-retry { + margin-top: 28px; + min-height: 44px; + border: 1px solid #ff6565; + border-radius: 8px; + padding: 0 20px; + background: rgba(222, 44, 44, 0.16); + color: #ffffff; + cursor: pointer; + font-family: 'Oswald', sans-serif; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.12em; +} + +.startup-error-retry:hover, +.startup-error-retry:focus-visible { + outline: none; + background: rgba(222, 44, 44, 0.35); + box-shadow: 0 0 0 3px rgba(255, 111, 111, 0.25); +} + +@media (max-width: 360px) { + .startup-error-card { + padding: 28px 20px; + } + + .startup-error-card p { + font-size: 15px; + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1635eed..c058719 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import { useHeatDecay } from './hooks/useHeatDecay'; import { useRaidCheck } from './hooks/useRaidCheck'; import { useBlockSync } from './hooks/useBlockSync'; import { useEmpireHydration } from './hooks/useEmpireHydration'; +import { useGhostCrewSync } from './hooks/useGhostCrewSync'; import { useSoundManager } from './hooks/useSoundManager'; import { useSalarySystem } from './hooks/useSalarySystem'; import PayrollModal from './components/economy/PayrollModal'; @@ -139,14 +140,17 @@ const App: React.FC = () => { const { raidBlockId, clearRaid } = useRaidCheck(); useBlockSync(!IS_DEMO_MODE); useEmpireHydration(Boolean(authUser) && authChecked && !IS_DEMO_MODE); + useGhostCrewSync(authUser?.id ?? null, Boolean(authUser) && authChecked && !IS_DEMO_MODE); useSoundManager(); const salarySystem = useSalarySystem(); useNPCRetaliation(); // NPC AI Tick — drives rival gang behavior every 30s const playerBlockIds = useTerritoryStore((s) => s.blocks.map((b) => b.id)); useNPCTick(playerBlockIds); - // Ghost Crew world tick — persistent rivals that claim/attack turf (#81) - useGhostTick(); + // Demo/offline sessions retain the local deterministic rival loop. An + // authenticated production session hydrates durable crew state instead, so + // browser ticks cannot drift ahead of the server-led world timeline. + useGhostTick(IS_DEMO_MODE || !authChecked || !authUser); const { completeStep } = useTutorialProgressStore(); diff --git a/frontend/src/components/map/BlockModeView.tsx b/frontend/src/components/map/BlockModeView.tsx index 16af64e..5fb0e4c 100644 --- a/frontend/src/components/map/BlockModeView.tsx +++ b/frontend/src/components/map/BlockModeView.tsx @@ -28,6 +28,7 @@ import DrugAssignmentPanel from './DrugAssignmentPanel'; import { PoliceRaidGame } from '../topdown/PoliceRaidGame'; import UnifiedEncounter from '../encounter/UnifiedEncounter'; import { vaultDeposit } from '../../utils/moneyRouter'; +import { commitEncounterResult } from '../../services/worldPersistence.service'; import './BlockModeView.css'; const ModernOpsEncounter = lazy(() => import('../ops/ModernOpsEncounter')); @@ -235,6 +236,12 @@ const BlockModeView: React.FC = ({ if (!selectedBlockId) return; const activeBlock = blocks[selectedBlockId]; applyEncounterResult(selectedBlockId, result); + // Keep the encounter UI responsive. The server receipt uses the same + // deterministic key and rejects duplicate/replayed results; local block + // sync carries the matching projected state in the background. + void commitEncounterResult(selectedBlockId, result).catch((error: unknown) => { + console.warn('[BlockModeView] Encounter receipt was not persisted:', error); + }); updatePlayer({ heat: Math.max(0, Math.min(5, (player.heat ?? 0) + result.heatDelta)) }); setShowEncounter(false); setShowModernOps(false); diff --git a/frontend/src/components/system/StartupErrorBoundary.test.tsx b/frontend/src/components/system/StartupErrorBoundary.test.tsx new file mode 100644 index 0000000..5bbbca6 --- /dev/null +++ b/frontend/src/components/system/StartupErrorBoundary.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import StartupErrorBoundary from './StartupErrorBoundary'; + +function ThrowError({ message }: { message: string }): never { + throw new Error(message); +} + +describe('StartupErrorBoundary', () => { + let consoleError: ReturnType; + + beforeEach(() => { + consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + }); + + afterEach(() => { + consoleError.mockRestore(); + }); + + it('explains missing Supabase configuration instead of leaving a blank application frame', () => { + render( + + + + ); + + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /system configuration required/i })).toBeInTheDocument(); + expect(screen.getByText(/missing a required service configuration/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /retry startup/i })).toBeInTheDocument(); + }); + + it('shows a neutral recovery state for unexpected bootstrap failures', () => { + render( + + + + ); + + expect(screen.getByRole('heading', { name: /system unavailable/i })).toBeInTheDocument(); + expect(screen.getByText(/could not finish starting/i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/system/StartupErrorBoundary.tsx b/frontend/src/components/system/StartupErrorBoundary.tsx new file mode 100644 index 0000000..ca0ab56 --- /dev/null +++ b/frontend/src/components/system/StartupErrorBoundary.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +type StartupErrorBoundaryProps = { + children: React.ReactNode; +}; + +type StartupErrorBoundaryState = { + error: Error | null; +}; + +function isConfigurationError(error: Error): boolean { + return /missing supabase environment variables/i.test(error.message); +} + +/** + * Protects the React bootstrap from module-load and render failures. + * + * App is intentionally lazy loaded from `main.tsx`. This boundary therefore + * remains available when App's Supabase dependency rejects during import, + * which would otherwise leave the player on an empty frame. + */ +export class StartupErrorBoundary extends React.Component< + StartupErrorBoundaryProps, + StartupErrorBoundaryState +> { + state: StartupErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): StartupErrorBoundaryState { + return { error }; + } + + private retry = () => { + window.location.reload(); + }; + + render() { + const { error } = this.state; + + if (!error) { + return this.props.children; + } + + const configurationError = isConfigurationError(error); + + return ( +
+
+ DEALT / SLIDE +

+ {configurationError ? 'SYSTEM CONFIGURATION REQUIRED' : 'SYSTEM UNAVAILABLE'} +

+

+ {configurationError + ? 'This game environment is missing a required service configuration. Please contact the deployment owner or try again after the environment is configured.' + : 'SLIDE could not finish starting. Please try again. If the problem continues, contact the deployment owner.'} +

+ +
+
+ ); + } +} + +export default StartupErrorBoundary; diff --git a/frontend/src/game/assets/__tests__/assetPackages.test.ts b/frontend/src/game/assets/__tests__/assetPackages.test.ts index 76c51f2..8463ca9 100644 --- a/frontend/src/game/assets/__tests__/assetPackages.test.ts +++ b/frontend/src/game/assets/__tests__/assetPackages.test.ts @@ -1,3 +1,5 @@ +import fs from 'node:fs'; +import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { anchorsForZone, @@ -109,6 +111,23 @@ describe('production asset packages', () => { expect(isBlockPackageV1({ schemaVersion: 1, packageId: 'broken' })).toBe(false); }); + it('ships a validated CC0 rig with real gameplay clips and a web-budgeted GLB', () => { + const packagePath = path.resolve(process.cwd(), 'public/assets/packages/characters/universal-male/package.v1.json'); + const runtimePackage = JSON.parse(fs.readFileSync(packagePath, 'utf8')) as CharacterPackageV1; + const glbPath = path.resolve(process.cwd(), 'public', runtimePackage.runtime.babylonGlb.replace(/^\//, '')); + + expect(isCharacterPackageV1(runtimePackage)).toBe(true); + expect(runtimePackage.provenance.licenseId).toBe('CC0-1.0'); + expect(runtimePackage.provenance.engineRestrictions).toEqual([]); + expect(runtimePackage.skeleton.rootBone).toBe('root'); + expect(hitZoneForNode(runtimePackage, 'Head')).toBe('head'); + expect(hitZoneForNode(runtimePackage, 'upperarm_r')).toBe('arm'); + expect(runtimePackage.animations.fire).toBe('Pistol_Shoot'); + expect(runtimePackage.animations.reload).toBe('Pistol_Reload'); + expect(runtimePackage.animations.downed).toBe('Death01'); + expect(fs.statSync(glbPath).size).toBeLessThan(1_500_000); + }); + it('maps authored character nodes to renderer-neutral hit zones', () => { expect(hitZoneForNode(character, 'hit_head')).toBe('head'); expect(hitZoneForNode(character, 'hit_arm_r')).toBe('arm'); diff --git a/frontend/src/game/ops/OpsCharacterFactory.ts b/frontend/src/game/ops/OpsCharacterFactory.ts index 68b9c53..9a76772 100644 --- a/frontend/src/game/ops/OpsCharacterFactory.ts +++ b/frontend/src/game/ops/OpsCharacterFactory.ts @@ -15,13 +15,32 @@ export interface OpsImpactMetadata { hitZone?: CombatHitZone; } +export type OpsActorAnimationState = + | 'idle' + | 'walk' + | 'sprint' + | 'strafe' + | 'aim' + | 'aimWalk' + | 'fire' + | 'reload' + | 'crouch' + | 'hit' + | 'downed' + | 'coverEnter' + | 'coverIdle' + | 'coverExit'; + export interface OpsActorVisual { root: TransformNode; meshes: AbstractMesh[]; + hitMeshes?: AbstractMesh[]; marker: Mesh; target: Vector3; weapon: TransformNode; presentation: 'articulated-fallback' | 'production-glb'; + playAnimation(state: OpsActorAnimationState): void; + dispose(): void; } export type RegisterOpsMaterial = (material: StandardMaterial) => void; @@ -189,6 +208,8 @@ export function createArticulatedActorFallback( target: root.position.clone(), weapon, presentation: 'articulated-fallback', + playAnimation: () => undefined, + dispose: () => root.dispose(false, false), }; } diff --git a/frontend/src/game/ops/OpsPackagedAssetLoader.ts b/frontend/src/game/ops/OpsPackagedAssetLoader.ts index c6dc7e9..917a936 100644 --- a/frontend/src/game/ops/OpsPackagedAssetLoader.ts +++ b/frontend/src/game/ops/OpsPackagedAssetLoader.ts @@ -1,16 +1,24 @@ import '@babylonjs/loaders/glTF'; +import type { AnimationGroup } from '@babylonjs/core/Animations/animationGroup'; +import type { AssetContainer, InstantiatedEntries } from '@babylonjs/core/assetContainer'; +import type { Bone } from '@babylonjs/core/Bones/bone'; import { Color3 } from '@babylonjs/core/Maths/math.color'; import { Vector3 } from '@babylonjs/core/Maths/math.vector'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; +import type { Mesh } from '@babylonjs/core/Meshes/mesh'; import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { TransformNode } from '@babylonjs/core/Meshes/transformNode'; import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader'; import type { Scene } from '@babylonjs/core/scene'; import type { BlockPackageV1, CharacterPackageV1 } from '../assets/assetPackages'; -import { hitZoneForNode } from '../assets/assetPackages'; -import type { Combatant } from '../combat/types'; -import type { OpsActorVisual, OpsImpactMetadata, RegisterOpsMaterial } from './OpsCharacterFactory'; +import type { CombatHitZone, Combatant } from '../combat/types'; +import type { + OpsActorAnimationState, + OpsActorVisual, + OpsImpactMetadata, + RegisterOpsMaterial, +} from './OpsCharacterFactory'; function splitAssetUrl(url: string): { rootUrl: string; fileName: string } { const separator = url.lastIndexOf('/'); @@ -21,42 +29,46 @@ function splitAssetUrl(url: string): { rootUrl: string; fileName: string } { }; } -function setActorMetadata(mesh: AbstractMesh, actor: Combatant, packageDefinition: CharacterPackageV1): void { - mesh.metadata = { - impactKind: 'actor', - entityId: actor.id, - hitZone: hitZoneForNode(packageDefinition, mesh.name) ?? 'torso', - } satisfies OpsImpactMetadata; - mesh.isPickable = true; - mesh.receiveShadows = true; +function uniqueMeshes(nodes: InstantiatedEntries['rootNodes']): AbstractMesh[] { + const meshes = new Set(); + nodes.forEach((node) => { + if ('getBoundingInfo' in node) meshes.add(node as AbstractMesh); + node.getChildMeshes(false).forEach((mesh) => meshes.add(mesh)); + }); + return [...meshes]; } -export async function loadPackagedCharacter( - scene: Scene, - packageDefinition: CharacterPackageV1, - actor: Combatant, - position: Vector3, - registerMaterial: RegisterOpsMaterial, -): Promise { - const root = new TransformNode(`ops-actor-${actor.id}`, scene); - root.position.copyFrom(position); - root.scaling.setAll(1 / packageDefinition.skeleton.metersPerUnit); +function findAnimation(groups: AnimationGroup[], expectedName: string): AnimationGroup | undefined { + return groups.find((group) => group.name === expectedName) + ?? groups.find((group) => group.name.startsWith(`${expectedName}-`)) + ?? groups.find((group) => group.name.includes(expectedName)); +} - const { rootUrl, fileName } = splitAssetUrl(packageDefinition.runtime.babylonGlb); - const imported = await SceneLoader.ImportMeshAsync(null, rootUrl, fileName, scene); - imported.meshes.forEach((mesh) => { - if (!mesh.parent) mesh.parent = root; - setActorMetadata(mesh, actor, packageDefinition); - }); +const HIT_PROXY_SPECS: Array<{ + packageKey: keyof CharacterPackageV1['hitZones']; + zone: CombatHitZone; + dimensions: { width: number; height: number; depth: number }; + offset: Vector3; +}> = [ + { packageKey: 'head', zone: 'head', dimensions: { width: 0.38, height: 0.44, depth: 0.38 }, offset: new Vector3(0, 0.1, 0) }, + { packageKey: 'torso', zone: 'torso', dimensions: { width: 0.7, height: 0.82, depth: 0.36 }, offset: new Vector3(0, -0.12, 0) }, + { packageKey: 'leftArm', zone: 'arm', dimensions: { width: 0.22, height: 0.62, depth: 0.22 }, offset: new Vector3(0, -0.23, 0) }, + { packageKey: 'rightArm', zone: 'arm', dimensions: { width: 0.22, height: 0.62, depth: 0.22 }, offset: new Vector3(0, -0.23, 0) }, + { packageKey: 'leftLeg', zone: 'leg', dimensions: { width: 0.28, height: 0.78, depth: 0.3 }, offset: new Vector3(0, -0.34, 0) }, + { packageKey: 'rightLeg', zone: 'leg', dimensions: { width: 0.28, height: 0.78, depth: 0.3 }, offset: new Vector3(0, -0.34, 0) }, +]; - const weapon = new TransformNode(`ops-weapon-root-${actor.id}`, scene); - weapon.parent = root; - const weaponSocket = packageDefinition.weaponSockets?.rightHand; - const socketNode = weaponSocket - ? imported.transformNodes.find((node) => node.name === weaponSocket) - : undefined; - if (socketNode) weapon.parent = socketNode; +function findBone(entries: InstantiatedEntries, name: string): Bone | undefined { + return entries.skeletons.flatMap((skeleton) => skeleton.bones).find((bone) => bone.name === name); +} +function createMarker( + scene: Scene, + actor: Combatant, + root: TransformNode, + localGroundY: number, + registerMaterial: RegisterOpsMaterial, +): Mesh { const markerMaterial = new StandardMaterial(`ops-marker-material-${actor.id}`, scene); markerMaterial.diffuseColor = Color3.Black(); markerMaterial.specularColor = Color3.Black(); @@ -67,21 +79,208 @@ export async function loadPackagedCharacter( const marker = MeshBuilder.CreateTorus(`ops-marker-${actor.id}`, { diameter: 1.2, thickness: 0.045, tessellation: 32 }, scene); marker.parent = root; marker.rotation.x = Math.PI / 2; - marker.position.y = -0.67; + marker.position.y = localGroundY + 0.025; marker.material = markerMaterial; marker.isPickable = false; + return marker; +} + +function createHitProxies( + scene: Scene, + packageDefinition: CharacterPackageV1, + actor: Combatant, + entries: InstantiatedEntries, + renderMeshes: AbstractMesh[], + registerMaterial: RegisterOpsMaterial, +): AbstractMesh[] { + const skinnedMesh = renderMeshes.find((mesh) => 'skeleton' in mesh && Boolean((mesh as Mesh).skeleton)) as Mesh | undefined; + if (!skinnedMesh) return []; - const idleGroup = imported.animationGroups.find((group) => group.name === packageDefinition.animations.idle); - idleGroup?.start(true); + const proxyMaterial = new StandardMaterial(`ops-hit-proxy-material-${actor.id}`, scene); + proxyMaterial.alpha = 0; + proxyMaterial.disableColorWrite = true; + proxyMaterial.disableDepthWrite = true; + registerMaterial(proxyMaterial); + + return HIT_PROXY_SPECS.flatMap((spec) => { + const bone = findBone(entries, packageDefinition.hitZones[spec.packageKey]); + if (!bone) return []; + const proxy = MeshBuilder.CreateBox(`ops-hit-${spec.zone}-${spec.packageKey}-${actor.id}`, spec.dimensions, scene); + proxy.material = proxyMaterial; + proxy.position.copyFrom(spec.offset); + proxy.attachToBone(bone, skinnedMesh); + proxy.metadata = { + impactKind: 'actor', + entityId: actor.id, + hitZone: spec.zone, + } satisfies OpsImpactMetadata; + proxy.isPickable = true; + return [proxy]; + }); +} + +function createWeapon( + scene: Scene, + packageDefinition: CharacterPackageV1, + actor: Combatant, + entries: InstantiatedEntries, + renderMeshes: AbstractMesh[], + root: TransformNode, + registerMaterial: RegisterOpsMaterial, +): TransformNode { + const metal = new StandardMaterial(`ops-package-weapon-${actor.id}`, scene); + metal.diffuseColor = new Color3(0.035, 0.04, 0.045); + metal.specularColor = new Color3(0.62, 0.64, 0.68); + registerMaterial(metal); + + const receiver = MeshBuilder.CreateBox(`ops-package-weapon-receiver-${actor.id}`, { + width: 0.13, + height: 0.17, + depth: 0.58, + }, scene); + receiver.material = metal; + receiver.isPickable = false; + const barrel = MeshBuilder.CreateBox(`ops-package-weapon-barrel-${actor.id}`, { + width: 0.052, + height: 0.052, + depth: 0.42, + }, scene); + barrel.parent = receiver; + barrel.position.z = 0.43; + barrel.material = metal; + barrel.isPickable = false; + + const socketName = packageDefinition.weaponSockets?.rightHand; + const socket = socketName ? findBone(entries, socketName) : undefined; + const skinnedMesh = renderMeshes.find((mesh) => 'skeleton' in mesh && Boolean((mesh as Mesh).skeleton)) as Mesh | undefined; + if (socket && skinnedMesh) { + receiver.attachToBone(socket, skinnedMesh); + receiver.position.set(0.02, 0.03, 0.2); + receiver.rotation.set(Math.PI / 2, 0, 0); + } else { + receiver.parent = root; + receiver.position.set(0.27, 1.05, 0.35); + } + return receiver; +} + +export interface OpsCharacterTemplate { + instantiate(actor: Combatant, position: Vector3): OpsActorVisual; + dispose(): void; +} + +export async function loadPackagedCharacterTemplate( + scene: Scene, + packageDefinition: CharacterPackageV1, + registerMaterial: RegisterOpsMaterial, +): Promise { + const { rootUrl, fileName } = splitAssetUrl(packageDefinition.runtime.babylonGlb); + const container: AssetContainer = await SceneLoader.LoadAssetContainerAsync(rootUrl, fileName, scene); + const requiredClips = new Set(Object.values(packageDefinition.animations)); + const availableClips = new Set(container.animationGroups.map((group) => group.name)); + const missingClips = [...requiredClips].filter((clip) => !availableClips.has(clip)); + if (missingClips.length > 0) { + container.dispose(); + throw new Error(`Character package ${packageDefinition.packageId} is missing animations: ${missingClips.join(', ')}`); + } return { - root, - meshes: imported.meshes, - marker, - target: root.position.clone(), - weapon, - presentation: 'production-glb', + instantiate(actor, position) { + const entries = container.instantiateModelsToScene( + (sourceName) => `${sourceName}-${actor.id}`, + false, + { doNotInstantiate: true }, + ); + const root = new TransformNode(`ops-actor-${actor.id}`, scene); + root.position.copyFrom(position); + root.scaling.setAll(1 / packageDefinition.skeleton.metersPerUnit); + const modelRoot = new TransformNode(`ops-model-${actor.id}`, scene); + modelRoot.parent = root; + entries.rootNodes.forEach((node) => { + if (!node.parent) node.parent = modelRoot; + }); + + const renderMeshes = uniqueMeshes(entries.rootNodes); + renderMeshes.forEach((mesh) => { + mesh.isPickable = false; + mesh.receiveShadows = true; + }); + root.computeWorldMatrix(true); + renderMeshes.forEach((mesh) => mesh.computeWorldMatrix(true)); + const minimumY = Math.min(...renderMeshes.map((mesh) => mesh.getBoundingInfo().boundingBox.minimumWorld.y)); + const localGroundY = 0.06 - root.position.y; + if (Number.isFinite(minimumY)) modelRoot.position.y += 0.06 - minimumY; + + const hitMeshes = createHitProxies(scene, packageDefinition, actor, entries, renderMeshes, registerMaterial); + const marker = createMarker(scene, actor, root, localGroundY, registerMaterial); + const weapon = createWeapon(scene, packageDefinition, actor, entries, renderMeshes, root, registerMaterial); + const groups = entries.animationGroups; + let activeGroup: AnimationGroup | undefined; + let activeState: OpsActorAnimationState | undefined; + let transient = false; + + const playAnimation = (state: OpsActorAnimationState): void => { + if (transient && !['fire', 'reload', 'hit', 'downed'].includes(state)) return; + if (state === activeState && (activeGroup?.isPlaying || state === 'downed')) return; + const clipName = packageDefinition.animations[state]; + const nextGroup = findAnimation(groups, clipName); + if (!nextGroup) return; + activeGroup?.stop(); + activeGroup = nextGroup; + activeState = state; + const loop = ['idle', 'walk', 'sprint', 'strafe', 'aim', 'aimWalk', 'crouch', 'coverIdle'].includes(state); + transient = ['fire', 'reload', 'hit'].includes(state); + nextGroup.start(loop); + if (transient) { + nextGroup.onAnimationGroupEndObservable.addOnce(() => { + if (activeGroup !== nextGroup) return; + transient = false; + activeState = undefined; + playAnimation('idle'); + }); + } + }; + playAnimation('idle'); + + return { + root, + meshes: renderMeshes, + hitMeshes, + marker, + target: root.position.clone(), + weapon, + presentation: 'production-glb', + playAnimation, + dispose() { + activeGroup?.stop(); + groups.forEach((group) => group.stop()); + hitMeshes.forEach((mesh) => mesh.dispose(false, false)); + entries.dispose(); + root.dispose(false, false); + }, + }; + }, + dispose() { + container.dispose(); + }, + }; +} + +export async function loadPackagedCharacter( + scene: Scene, + packageDefinition: CharacterPackageV1, + actor: Combatant, + position: Vector3, + registerMaterial: RegisterOpsMaterial, +): Promise { + const template = await loadPackagedCharacterTemplate(scene, packageDefinition, registerMaterial); + const visual = template.instantiate(actor, position); + const disposeVisual = visual.dispose; + visual.dispose = () => { + disposeVisual(); + template.dispose(); }; + return visual; } export interface OpsBlockVisual { diff --git a/frontend/src/game/ops/OpsPbrMaterials.ts b/frontend/src/game/ops/OpsPbrMaterials.ts new file mode 100644 index 0000000..f6e1470 --- /dev/null +++ b/frontend/src/game/ops/OpsPbrMaterials.ts @@ -0,0 +1,40 @@ +import { Color3 } from '@babylonjs/core/Maths/math.color'; +import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial'; +import { Texture } from '@babylonjs/core/Materials/Textures/texture'; +import type { Scene } from '@babylonjs/core/scene'; + +export type OpsPbrSurfaceId = 'asphalt033' | 'concrete034' | 'bricks097'; + +const PBR_ROOT = '/assets/packages/blocks/las-olas-1208/materials'; + +export function createOpsPbrMaterial( + scene: Scene, + surfaceId: OpsPbrSurfaceId, + name: string, + tiling: { u: number; v: number }, +): PBRMaterial { + const material = new PBRMaterial(name, scene); + const albedo = new Texture(`${PBR_ROOT}/${surfaceId}-color.webp`, scene, true, false); + const normal = new Texture(`${PBR_ROOT}/${surfaceId}-normal.webp`, scene, true, false); + const roughness = new Texture(`${PBR_ROOT}/${surfaceId}-roughness.webp`, scene, true, false); + [albedo, normal, roughness].forEach((texture) => { + texture.uScale = tiling.u; + texture.vScale = tiling.v; + }); + + material.albedoTexture = albedo; + material.bumpTexture = normal; + material.metallicTexture = roughness; + material.metallic = 0; + material.roughness = 0.88; + material.useRoughnessFromMetallicTextureGreen = true; + material.useMetallnessFromMetallicTextureBlue = false; + material.useAmbientOcclusionFromMetallicTextureRed = false; + material.invertNormalMapX = false; + material.invertNormalMapY = false; + material.environmentIntensity = surfaceId === 'asphalt033' ? 0.72 : 0.42; + material.albedoColor = surfaceId === 'asphalt033' + ? new Color3(0.2, 0.23, 0.27) + : Color3.White(); + return material; +} diff --git a/frontend/src/game/ops/OpsWorld.ts b/frontend/src/game/ops/OpsWorld.ts index 87357d8..3384e32 100644 --- a/frontend/src/game/ops/OpsWorld.ts +++ b/frontend/src/game/ops/OpsWorld.ts @@ -9,6 +9,7 @@ import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'; import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; import { Color3 } from '@babylonjs/core/Maths/math.color'; import { Matrix, Vector3 } from '@babylonjs/core/Maths/math.vector'; +import type { Material } from '@babylonjs/core/Materials/material'; import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; import { Texture } from '@babylonjs/core/Materials/Textures/texture'; import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; @@ -18,6 +19,7 @@ import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder'; import { TransformNode } from '@babylonjs/core/Meshes/transformNode'; import type { Observer } from '@babylonjs/core/Misc/observable'; import type { Scene } from '@babylonjs/core/scene'; +import { loadCharacterPackage } from '../assets/assetPackages'; import type { CombatEvent, CombatImpactCandidate, @@ -39,9 +41,12 @@ import { createWetPuddle, } from './OpsEnvironmentFactory'; import { OpsInput } from './OpsInput'; +import { loadPackagedCharacterTemplate, type OpsCharacterTemplate } from './OpsPackagedAssetLoader'; +import { createOpsPbrMaterial } from './OpsPbrMaterials'; import { gridToWorld, movementToGridStep, OPS_CELL_SIZE } from './opsCoordinates'; const FACADE_URL = '/assets/runtime/generated/environments/street/block_modern_ops_storefront_v001.webp'; +const PRODUCTION_CHARACTER_PACKAGE_URL = '/assets/packages/characters/universal-male/package.v1.json'; const MOVE_INTERVAL_MS = 155; const SIM_TICK_MS = 100; const OPS_FIRE_RANGE = 40; @@ -78,8 +83,9 @@ export class OpsWorld { private readonly input: OpsInput; private readonly actorVisuals = new Map(); private readonly effects: TimedEffect[] = []; - private readonly materials: StandardMaterial[] = []; + private readonly materials: Material[] = []; private readonly observers: Observer[] = []; + private characterTemplate?: OpsCharacterTemplate; private readonly shadowGenerator: ShadowGenerator; private readonly tacticalCamera: ArcRotateCamera; private readonly firstPersonCamera: UniversalCamera; @@ -101,7 +107,7 @@ export class OpsWorld { constructor( private readonly scene: Scene, - canvas: HTMLCanvasElement, + private readonly canvas: HTMLCanvasElement, private readonly controller: CombatSessionController, private readonly preparation: EncounterPreparation, private readonly options: OpsWorldOptions = {}, @@ -136,6 +142,32 @@ export class OpsWorld { })); } + async initializeProductionAssets( + packageUrl = PRODUCTION_CHARACTER_PACKAGE_URL, + ): Promise { + try { + const packageDefinition = await loadCharacterPackage(packageUrl); + const template = await loadPackagedCharacterTemplate( + this.scene, + packageDefinition, + (actorMaterial) => this.materials.push(actorMaterial), + ); + if (this.disposed) { + template.dispose(); + return; + } + this.characterTemplate?.dispose(); + this.characterTemplate = template; + this.canvas.dataset.characterPackage = packageDefinition.packageId; + this.actorVisuals.forEach((visual) => visual.dispose()); + this.actorVisuals.clear(); + this.syncSnapshot(this.snapshot); + } catch (error) { + delete this.canvas.dataset.characterPackage; + console.warn('Modern Ops production character package could not load; keeping articulated fallbacks.', error); + } + } + dispose(): void { if (this.disposed) return; this.disposed = true; @@ -143,6 +175,11 @@ export class OpsWorld { this.input.dispose(); this.observers.forEach((observer) => this.scene.onBeforeRenderObservable.remove(observer)); this.effects.forEach((effect) => effect.mesh.dispose()); + this.actorVisuals.forEach((visual) => visual.dispose()); + this.actorVisuals.clear(); + this.characterTemplate?.dispose(); + this.characterTemplate = undefined; + delete this.canvas.dataset.characterPackage; this.materials.forEach((item) => item.dispose()); if (document.pointerLockElement) void document.exitPointerLock?.(); } @@ -170,15 +207,18 @@ export class OpsWorld { this.scene.clearColor.set(0.012, 0.025, 0.045, 1); this.scene.ambientColor = new Color3(0.08, 0.11, 0.16); + const asphalt = createOpsPbrMaterial(this.scene, 'asphalt033', 'ops-road-pbr', { u: 10, v: 10 }); + const concrete = createOpsPbrMaterial(this.scene, 'concrete034', 'ops-concrete-pbr', { u: 5, v: 5 }); + const brick = createOpsPbrMaterial(this.scene, 'bricks097', 'ops-brick-pbr', { u: 4, v: 3 }); const palette = { - street: material(this.scene, 'ops-road', new Color3(0.035, 0.055, 0.075), new Color3(0.95, 0.95, 1)), - curb: material(this.scene, 'ops-curb', new Color3(0.19, 0.21, 0.23), new Color3(0.4, 0.4, 0.44)), - sidewalk: material(this.scene, 'ops-sidewalk', new Color3(0.13, 0.18, 0.2), new Color3(0.5, 0.54, 0.58)), - storefront: material(this.scene, 'ops-storefront-ground', new Color3(0.11, 0.09, 0.14)), - alley: material(this.scene, 'ops-alley', new Color3(0.035, 0.08, 0.085)), - parking: material(this.scene, 'ops-parking', new Color3(0.095, 0.09, 0.085), new Color3(0.38, 0.35, 0.34)), - rooftop: material(this.scene, 'ops-rooftop', new Color3(0.105, 0.08, 0.13)), - building: material(this.scene, 'ops-building', new Color3(0.055, 0.045, 0.075)), + street: asphalt, + curb: concrete, + sidewalk: concrete, + storefront: brick, + alley: asphalt, + parking: asphalt, + rooftop: concrete, + building: brick, }; this.materials.push(...Object.values(palette)); @@ -466,7 +506,7 @@ export class OpsWorld { const activeIds = new Set(snapshot.combatants.map((actor) => actor.id)); this.actorVisuals.forEach((visual, id) => { if (activeIds.has(id)) return; - visual.root.dispose(false, true); + visual.dispose(); this.actorVisuals.delete(id); }); @@ -479,21 +519,27 @@ export class OpsWorld { visual.target.copyFrom(actorPosition(actor)); const selected = actor.id === this.controller.getHudState().selectedId; visual.marker.setEnabled(selected && !actor.isDown); - visual.root.rotation.z = actor.isDown ? Math.PI / 2 : 0; + visual.root.rotation.z = visual.presentation === 'articulated-fallback' && actor.isDown ? Math.PI / 2 : 0; + if (actor.isDown) visual.playAnimation('downed'); visual.meshes.forEach((mesh) => { mesh.visibility = actor.isDown ? 0.42 : 1; mesh.isPickable = !actor.isDown; }); + visual.hitMeshes?.forEach((mesh) => { + mesh.isPickable = !actor.isDown; + }); }); } private createActorVisual(actor: Combatant): OpsActorVisual { - const visual = createArticulatedActorFallback( - this.scene, - actor, - actorPosition(actor), - (actorMaterial) => this.materials.push(actorMaterial), - ); + const visual = this.characterTemplate + ? this.characterTemplate.instantiate(actor, actorPosition(actor)) + : createArticulatedActorFallback( + this.scene, + actor, + actorPosition(actor), + (actorMaterial) => this.materials.push(actorMaterial), + ); visual.meshes.forEach((mesh) => this.shadowGenerator.addShadowCaster(mesh)); return visual; } @@ -509,6 +555,7 @@ export class OpsWorld { private renderEvent(event: CombatEvent): void { if (event.type === 'weapon-fired' && event.actorId) { + this.actorVisuals.get(event.actorId)?.playAnimation('fire'); if (event.actorId === this.controller.getControlledCrewId()) this.weaponKick = 0.13; const source = this.actorVisuals.get(event.actorId)?.root.position; const impactPoint = event.impact @@ -527,6 +574,10 @@ export class OpsWorld { } } + if (event.type === 'reload-start' && event.actorId) this.actorVisuals.get(event.actorId)?.playAnimation('reload'); + if (event.type === 'impact-actor' && event.targetId) this.actorVisuals.get(event.targetId)?.playAnimation('hit'); + if (event.type === 'actor-downed' && event.targetId) this.actorVisuals.get(event.targetId)?.playAnimation('downed'); + const visibleImpactTypes = new Set([ 'impact-actor', 'impact-cover', @@ -611,9 +662,10 @@ export class OpsWorld { this.actorVisuals.forEach((visual, actorId) => { const movement = visual.target.subtract(visual.root.position); const actor = this.snapshot.combatants.find((candidate) => candidate.id === actorId); - if (!actor?.isDown && movement.lengthSquared() > 0.0025) { - visual.root.rotation.y = Math.atan2(movement.x, movement.z); - } + const moving = !actor?.isDown && movement.lengthSquared() > 0.0025; + if (moving) visual.root.rotation.y = Math.atan2(movement.x, movement.z); + if (actor?.isDown) visual.playAnimation('downed'); + else visual.playAnimation(moving ? 'walk' : 'idle'); visual.root.position.copyFrom(Vector3.Lerp(visual.root.position, visual.target, Math.min(1, boundedDelta * 11))); }); this.updateCamera(); @@ -731,6 +783,9 @@ export class OpsWorld { mesh.visibility = selected.isDown ? 0.42 : 1; mesh.isPickable = !selected.isDown; }); + actor.hitMeshes?.forEach((mesh) => { + mesh.isPickable = !selected.isDown; + }); return; } @@ -741,6 +796,9 @@ export class OpsWorld { mesh.visibility = 0; mesh.isPickable = false; }); + actor.hitMeshes?.forEach((mesh) => { + mesh.isPickable = false; + }); return; } @@ -754,6 +812,9 @@ export class OpsWorld { mesh.visibility = selected.isDown ? 0.42 : 1; mesh.isPickable = !selected.isDown; }); + actor.hitMeshes?.forEach((mesh) => { + mesh.isPickable = !selected.isDown; + }); } private updateEffects(delta: number): void { diff --git a/frontend/src/game/ops/createModernOpsScene.ts b/frontend/src/game/ops/createModernOpsScene.ts index f1395f1..91bc806 100644 --- a/frontend/src/game/ops/createModernOpsScene.ts +++ b/frontend/src/game/ops/createModernOpsScene.ts @@ -34,6 +34,7 @@ export async function createModernOpsScene( glow.intensity = 0.52; const world = new OpsWorld(scene, canvas, controller, preparation, options); + await world.initializeProductionAssets(); await scene.whenReadyAsync(); let disposed = false; diff --git a/frontend/src/hooks/useBlockSync.ts b/frontend/src/hooks/useBlockSync.ts index c594916..03e4d3b 100644 --- a/frontend/src/hooks/useBlockSync.ts +++ b/frontend/src/hooks/useBlockSync.ts @@ -65,6 +65,10 @@ export function useBlockSync(enabled = true) { pendingIncome: partial.pendingIncome ?? 0, streetBackdropUrl: partial.streetBackdropUrl, topdownBgUrl: partial.topdownBgUrl, + dnaId: partial.dnaId, + incomeMultiplier: partial.incomeMultiplier, + heatDecayMultiplier: partial.heatDecayMultiplier, + maxMembers: partial.maxMembers, }); } } catch (err) { diff --git a/frontend/src/hooks/useGhostCrewSync.ts b/frontend/src/hooks/useGhostCrewSync.ts new file mode 100644 index 0000000..e289d9f --- /dev/null +++ b/frontend/src/hooks/useGhostCrewSync.ts @@ -0,0 +1,32 @@ +import { useEffect } from 'react'; +import { useGhostStore } from '../stores/ghostCrewStore'; +import { loadAuthoritativeWorld } from '../services/worldPersistence.service'; +import { IS_DEMO_MODE } from '../utils/demoSeed'; + +/** + * Hydrates the existing Ghost Crew store from the additive authoritative-world + * tables. The local Ghost Crew engine remains the deterministic offline/demo + * fallback; this hook never starts a new tick or owns a second cache. + */ +export function useGhostCrewSync(profileId: string | null, enabled: boolean): void { + const replaceAuthoritativeState = useGhostStore((state) => state.replaceAuthoritativeState); + + useEffect(() => { + if (!enabled || IS_DEMO_MODE || !profileId) return; + + let cancelled = false; + void loadAuthoritativeWorld(profileId) + .then(({ crews, feed }) => { + if (!cancelled) replaceAuthoritativeState(crews, feed); + }) + .catch((error: unknown) => { + // The local persistent state remains a safe offline fallback until the + // configured project has the additive migration and edge functions. + console.warn('[GhostCrewSync] Authoritative world hydration skipped:', error); + }); + + return () => { + cancelled = true; + }; + }, [enabled, profileId, replaceAuthoritativeState]); +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 1dc4351..472bf34 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; +import StartupErrorBoundary from './components/system/StartupErrorBoundary'; + const App = React.lazy(() => import('./App')); const ShooterGraphicsLab = React.lazy(() => import('./components/dev/ShooterGraphicsLab')); @@ -12,14 +14,16 @@ import './styles/mobile.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - {graphicsLabEnabled ? ( - }> - - - ) : ( - }> - - - )} + + {graphicsLabEnabled ? ( + }> + + + ) : ( + }> + + + )} + ); diff --git a/frontend/src/services/blockPersistence.service.test.ts b/frontend/src/services/blockPersistence.service.test.ts new file mode 100644 index 0000000..cbd8fc3 --- /dev/null +++ b/frontend/src/services/blockPersistence.service.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { rpc, from, upsert } = vi.hoisted(() => ({ + rpc: vi.fn(), + from: vi.fn(), + upsert: vi.fn(), +})); + +vi.mock('./supabase', () => ({ + supabase: { rpc, from }, +})); + +import { persistBlock } from './blockPersistence.service'; + +const block = { + id: '11111111-1111-4111-8111-111111111111', + address: 'Fictional Home Block', + lat: 25.7617, + lng: -80.1918, + owner: 'player' as const, + grid: [], + placements: [], + incomePerTick: 120, + heat: 2, + morale: 80, + members: 0, + viewMode: 'topdown' as const, + pendingIncome: 90, + appliedEncounterResultKeys: ['result-1'], +}; + +describe('blockPersistence.service authoritative projection', () => { + beforeEach(() => { + rpc.mockReset(); + from.mockReset(); + upsert.mockReset(); + from.mockReturnValue({ upsert }); + upsert.mockResolvedValue({ error: null }); + }); + + it('uses the atomic projection RPC for UUID-backed player blocks', async () => { + rpc.mockResolvedValue({ data: { applied: true }, error: null }); + + await persistBlock(block, '11111111-1111-4111-8111-111111111111'); + + expect(rpc).toHaveBeenCalledWith('persist_player_block_projection', expect.objectContaining({ + p_block_id: block.id, + p_client_result_key: 'result-1', + p_block_heat: 40, + p_base_income: 120, + })); + expect(from).not.toHaveBeenCalled(); + }); + + it('does not fall through to an unguarded upsert when the server rejects a stale projection', async () => { + rpc.mockResolvedValue({ data: { applied: false, reason: 'stale_encounter_projection' }, error: null }); + + await persistBlock(block, '11111111-1111-4111-8111-111111111111'); + + expect(from).not.toHaveBeenCalled(); + }); + + it('retains the legacy upsert path only when the new RPC is absent from an older server schema', async () => { + rpc.mockResolvedValue({ data: null, error: { code: 'PGRST202', message: 'Function not found' } }); + + await persistBlock(block, '11111111-1111-4111-8111-111111111111'); + + expect(from).toHaveBeenCalledWith('blocks'); + expect(upsert).toHaveBeenCalledWith(expect.objectContaining({ + id: block.id, + metadata: expect.objectContaining({ lastEncounterResultKey: 'result-1' }), + }), { onConflict: 'id' }); + }); +}); diff --git a/frontend/src/services/blockPersistence.service.ts b/frontend/src/services/blockPersistence.service.ts index 94e491e..fc20f73 100644 --- a/frontend/src/services/blockPersistence.service.ts +++ b/frontend/src/services/blockPersistence.service.ts @@ -10,6 +10,8 @@ import { supabase } from './supabase'; import type { BlockData, BlockPlacement } from '../types/block.types'; +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + // ─── Helpers ───────────────────────────────────────────────── function isSupabaseConfigured(): boolean { @@ -22,6 +24,17 @@ function isSupabaseConfigured(): boolean { } } +function isUuid(value: string): boolean { + return UUID_PATTERN.test(value); +} + +function isMissingProjectionRpc(error: { code?: string }): boolean { + // PGRST202 is returned while the PostgREST schema cache cannot find the + // function. 42883 is PostgreSQL's undefined-function error. Any other + // failure must not fall through to an unguarded write. + return error.code === 'PGRST202' || error.code === '42883'; +} + // ─── Block CRUD ────────────────────────────────────────────── /** @@ -31,31 +44,92 @@ function isSupabaseConfigured(): boolean { export async function persistBlock(block: BlockData, userId: string): Promise { if (!isSupabaseConfigured()) return; - const { error } = await (supabase as any).from('blocks').upsert( - { - id: block.id, - address: block.address, - // Supabase geography point: POINT(lng lat) - location: `POINT(${block.lng} ${block.lat})`, - owner_id: block.owner === 'player' ? userId : null, - status: block.owner === 'player' ? 'claimed' : block.owner === 'npc' ? 'claimed' : 'unclaimed', - block_heat: block.heat * 20, // 0-5 → 0-100 - base_income: block.incomePerTick, - updated_at: new Date().toISOString(), - // Store grid, placements, morale as JSONB metadata - metadata: { - morale: block.morale, - pendingIncome: block.pendingIncome, - viewMode: block.viewMode, - streetBackdropUrl: block.streetBackdropUrl, - topdownBgUrl: block.topdownBgUrl, + const lastEncounterResultKey = block.appliedEncounterResultKeys?.[block.appliedEncounterResultKeys.length - 1]; + const metadata = { + morale: block.morale, + pendingIncome: block.pendingIncome, + viewMode: block.viewMode, + streetBackdropUrl: block.streetBackdropUrl, + topdownBgUrl: block.topdownBgUrl, + dnaId: block.dnaId, + incomeMultiplier: block.incomeMultiplier, + heatDecayMultiplier: block.heatDecayMultiplier, + maxMembers: block.maxMembers, + lastEncounterResultKey, + }; + const status = block.owner === 'player' ? 'claimed' : block.owner === 'npc' ? 'claimed' : 'unclaimed'; + const db = supabase as any; + + // Blocks created by the authenticated application have UUIDs. When the + // integrity migration exists, use one atomic RPC so a delayed autosave + // cannot overwrite a newer encounter receipt. Demo/local placeholder IDs + // retain the legacy best-effort path below. + let persistedAtomically = false; + if (isUuid(block.id)) { + const { data, error: projectionError } = await db.rpc('persist_player_block_projection', { + p_block_id: block.id, + p_address: block.address, + p_lng: block.lng, + p_lat: block.lat, + p_status: status, + p_block_heat: block.heat * 20, + p_base_income: block.incomePerTick, + p_metadata: metadata, + p_client_result_key: lastEncounterResultKey ?? null, + }); + + if (!projectionError) { + if (data?.applied === false) { + console.warn('[BlockPersistence] Skipped stale block projection after a newer encounter result.'); + return; + } + persistedAtomically = true; + } else if (!isMissingProjectionRpc(projectionError)) { + console.warn('[BlockPersistence] Failed to persist atomic block projection:', projectionError.message); + return; + } + } + + if (!persistedAtomically) { + const { error } = await db.from('blocks').upsert( + { + id: block.id, + address: block.address, + // Supabase geography point: POINT(lng lat) + location: `POINT(${block.lng} ${block.lat})`, + owner_id: block.owner === 'player' ? userId : null, + status, + block_heat: block.heat * 20, // 0-5 → 0-100 + base_income: block.incomePerTick, + updated_at: new Date().toISOString(), + // Store grid, placements, morale as JSONB metadata + metadata, }, - }, - { onConflict: 'id' } - ); + { onConflict: 'id' } + ); - if (error) { - console.warn('[BlockPersistence] Failed to upsert block:', error.message); + if (error) { + console.warn('[BlockPersistence] Failed to upsert block:', error.message); + return; + } + } + + // `claimed_block_dna` intentionally stores only the approved gameplay + // archetype and derived tactical values, not a second copy of location data. + if (block.dnaId) { + const { error: dnaError } = await (supabase as any).from('claimed_block_dna').upsert( + { + block_id: block.id, + dna_id: block.dnaId, + tactical_snapshot: { + incomeMultiplier: block.incomeMultiplier ?? 1, + heatDecayMultiplier: block.heatDecayMultiplier ?? 1, + maxMembers: block.maxMembers ?? null, + }, + }, + { onConflict: 'block_id' }, + ); + if (dnaError) console.warn('[BlockPersistence] Failed to persist Block DNA:', dnaError.message); } } @@ -87,6 +161,10 @@ export async function loadPlayerBlocks(userId: string): Promise ({ rpc: vi.fn() })); + +vi.mock('./supabase', () => ({ + supabase: { rpc }, +})); + +vi.mock('../utils/demoSeed', () => ({ + IS_DEMO_MODE: false, +})); + +import { + commitEncounterResult, + toGhostCrew, + toGhostFeedEvent, + worldPersistenceInternals, +} from './worldPersistence.service'; + +const result = { + idempotencyKey: 'result-001', + outcome: 'secured' as const, + crewDown: ['crew-1'], + oppositionDown: ['rival-1'], + objectiveProgress: 1, + heatDelta: 1, + moraleDelta: -2, + pendingIncomeDelta: 125, + summary: 'Block secured.', +}; + +describe('worldPersistence.service', () => { + beforeEach(() => { + rpc.mockReset(); + }); + + it('maps durable Ghost Crew data into the existing deterministic domain shape', () => { + const crew = toGhostCrew({ + id: 'ghost-nightfall', + name: 'Nightfall Crew', + home_tag: 'downtown', + personality: { type: 'territory-hungry', aggression: 55, expansionDrive: 85, grudgeWeight: 40, caution: 30 }, + treasury: 2200, + roster: [{ id: 'nf-1', name: 'Olas King', role: 'enforcer', level: 4, alive: true }], + owned_block_ids: ['ghost-las-olas'], + claimed_dna_ids: ['las-olas'], + grudge: { score: 25 }, + income_per_tick: 120, + last_tick_at: '2026-09-01T00:00:00.000Z', + last_move: 'Claimed a block.', + }); + + expect(crew).toMatchObject({ + id: 'ghost-nightfall', + homeTag: 'downtown', + ownedBlockIds: ['ghost-las-olas'], + claimedDnaIds: ['las-olas'], + incomePerTick: 120, + grudge: { score: 25 }, + }); + }); + + it('maps a server world event without exposing an address or creating a new feed contract', () => { + const event = toGhostFeedEvent( + { + id: 'event-1', + crew_id: 'ghost-nightfall', + event_type: 'attack', + target_block_key: 'block-uuid-only', + description: 'Nightfall Crew is applying pressure.', + occurred_at: '2026-09-01T00:00:00.000Z', + }, + [{ + id: 'ghost-nightfall', + name: 'Nightfall Crew', + homeTag: 'downtown', + personality: { type: 'territory-hungry', aggression: 55, expansionDrive: 85, grudgeWeight: 40, caution: 30 }, + treasury: 0, + roster: [], + ownedBlockIds: [], + claimedDnaIds: [], + grudge: { score: 0 }, + incomePerTick: 0, + lastTickAt: '2026-09-01T00:00:00.000Z', + }], + ); + + expect(event).toMatchObject({ + id: 'server-event-1', + crewName: 'Nightfall Crew', + action: 'attack', + targetBlockId: 'block-uuid-only', + }); + }); + + it('does not submit a local placeholder block to the authoritative receipt endpoint', async () => { + await expect(commitEncounterResult('home-block', result)).resolves.toBeNull(); + expect(rpc).not.toHaveBeenCalled(); + }); + + it('submits one typed receipt keyed by the deterministic result idempotency key', async () => { + rpc.mockResolvedValue({ + data: { applied: true, resultId: 'receipt-1', resultKey: result.idempotencyKey }, + error: null, + }); + + await expect(commitEncounterResult('11111111-1111-4111-8111-111111111111', result)).resolves.toEqual({ + applied: true, + resultId: 'receipt-1', + resultKey: result.idempotencyKey, + }); + expect(rpc).toHaveBeenCalledWith('commit_encounter_result', { + p_result_key: result.idempotencyKey, + p_block_id: '11111111-1111-4111-8111-111111111111', + p_payload: result, + }); + }); + + it('recognizes UUID block IDs only', () => { + expect(worldPersistenceInternals.isUuid('11111111-1111-4111-8111-111111111111')).toBe(true); + expect(worldPersistenceInternals.isUuid('home-block')).toBe(false); + }); +}); diff --git a/frontend/src/services/worldPersistence.service.ts b/frontend/src/services/worldPersistence.service.ts new file mode 100644 index 0000000..c932bdc --- /dev/null +++ b/frontend/src/services/worldPersistence.service.ts @@ -0,0 +1,157 @@ +// ============================================================ +// DEALT/SLIDE — Authoritative World Persistence +// +// Thin transport layer for the additive authoritative-world schema. +// It deliberately does not resolve combat, award money, or own a second +// client-state cache. The deterministic combat domain and Zustand stores +// remain the source of local interaction state. +// ============================================================ + +import { supabase } from './supabase'; +import { IS_DEMO_MODE } from '../utils/demoSeed'; +import type { CombatResult } from '../game/combat/types'; +import type { GhostFeedEvent } from '../stores/ghostCrewStore'; +import type { + GhostActionType, + GhostCrew, + GhostMember, + GrudgeEntry, + Personality, +} from '../utils/ghostCrewEngine'; + +const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +export type EncounterReceipt = { + applied: boolean; + resultId: string; + resultKey: string; +}; + +type WorldEventRow = { + id: string; + crew_id: string | null; + event_type: GhostActionType | 'encounter' | 'system'; + target_block_key: string | null; + description: string; + occurred_at: string; +}; + +type GhostCrewRow = { + id: string; + name: string; + home_tag: string; + personality: Personality; + treasury: number; + roster: GhostMember[]; + owned_block_ids: string[]; + claimed_dna_ids: string[]; + grudge: GrudgeEntry; + income_per_tick: number; + last_tick_at: string; + last_move: string | null; +}; + +function isUuid(value: string): boolean { + return UUID_PATTERN.test(value); +} + +function asArray(value: unknown): T[] { + return Array.isArray(value) ? value as T[] : []; +} + +function asRecord(value: unknown, fallback: T): T { + return value && typeof value === 'object' && !Array.isArray(value) ? value as T : fallback; +} + +/** Maps the additive database row to the existing Ghost Crew domain type. */ +export function toGhostCrew(row: GhostCrewRow): GhostCrew { + return { + id: row.id, + name: row.name, + homeTag: row.home_tag, + personality: asRecord(row.personality, { + type: 'territory-hungry', aggression: 50, expansionDrive: 50, grudgeWeight: 50, caution: 50, + }), + treasury: Math.max(0, Number(row.treasury) || 0), + roster: asArray(row.roster), + ownedBlockIds: asArray(row.owned_block_ids), + claimedDnaIds: asArray(row.claimed_dna_ids), + grudge: asRecord(row.grudge, { score: 0 }), + incomePerTick: Math.max(0, Number(row.income_per_tick) || 0), + lastTickAt: row.last_tick_at, + lastMove: row.last_move ?? undefined, + }; +} + +/** Maps a safe player-visible server event to the existing Ghost Feed shape. */ +export function toGhostFeedEvent(row: WorldEventRow, crews: GhostCrew[]): GhostFeedEvent { + const crew = row.crew_id ? crews.find((candidate) => candidate.id === row.crew_id) : undefined; + return { + id: `server-${row.id}`, + crewId: row.crew_id ?? 'system', + crewName: crew?.name ?? 'City Feed', + action: row.event_type === 'encounter' || row.event_type === 'system' ? 'lay-low' : row.event_type, + description: row.description, + targetBlockId: row.target_block_key ?? undefined, + timestamp: Date.parse(row.occurred_at) || Date.now(), + }; +} + +/** + * Load only the public rival state and events visible to this authenticated + * profile. The caller overlays this data onto the existing Ghost Crew store. + */ +export async function loadAuthoritativeWorld(profileId: string): Promise<{ + crews: GhostCrew[]; + feed: GhostFeedEvent[]; +}> { + if (IS_DEMO_MODE || !profileId) return { crews: [], feed: [] }; + + const db = supabase as any; + const [{ data: crewRows, error: crewError }, { data: eventRows, error: eventError }] = await Promise.all([ + db.from('ghost_crews').select('*').order('name', { ascending: true }), + db.from('world_events') + .select('id, crew_id, event_type, target_block_key, description, occurred_at') + .order('occurred_at', { ascending: false }) + .limit(50), + ]); + + if (crewError) throw crewError; + if (eventError) throw eventError; + + const crews = asArray(crewRows).map(toGhostCrew); + const feed = asArray(eventRows).map((row) => toGhostFeedEvent(row, crews)); + return { crews, feed }; +} + +/** + * Persist a single idempotent receipt for an already-resolved deterministic + * encounter. The function does not wait on the UI and never mutates the + * combat-domain result. + */ +export async function commitEncounterResult( + blockId: string, + result: CombatResult, +): Promise { + if (IS_DEMO_MODE || !isUuid(blockId) || !result.idempotencyKey) return null; + + const { data, error } = await (supabase as any).rpc('commit_encounter_result', { + p_result_key: result.idempotencyKey, + p_block_id: blockId, + p_payload: result, + }); + + if (error) throw error; + if (!data || typeof data !== 'object') { + throw new Error('Encounter receipt response was invalid.'); + } + + return { + applied: Boolean(data.applied), + resultId: String(data.resultId ?? ''), + resultKey: String(data.resultKey ?? result.idempotencyKey), + }; +} + +/** Exported only for focused unit tests and integration adapters. */ +export const worldPersistenceInternals = { isUuid }; diff --git a/frontend/src/stores/__tests__/ghostCrewStore.test.ts b/frontend/src/stores/__tests__/ghostCrewStore.test.ts index 230a9bf..5d2a9db 100644 --- a/frontend/src/stores/__tests__/ghostCrewStore.test.ts +++ b/frontend/src/stores/__tests__/ghostCrewStore.test.ts @@ -70,4 +70,54 @@ describe('ghostCrewStore', () => { expect(owner!.ownedBlockIds).toContain(npcBlock.id); } }); + + it('overlays durable Ghost Crew data while retaining a local fallback for an empty server response', () => { + useGhostStore.getState().seedCrews(); + const originalCount = Object.keys(useGhostStore.getState().crews).length; + const crew = { + ...useGhostStore.getState().crews['ghost-nightfall'], + treasury: 3333, + lastMove: 'Applied from authoritative world.', + }; + + useGhostStore.getState().replaceAuthoritativeState([crew], [{ + id: 'server-event-1', + crewId: crew.id, + crewName: crew.name, + action: 'reinforce', + description: crew.lastMove!, + timestamp: Date.now(), + }]); + + expect(useGhostStore.getState().crews[crew.id]).toEqual(crew); + expect(Object.keys(useGhostStore.getState().crews)).toHaveLength(originalCount); + expect(useGhostStore.getState().feed[0]?.id).toBe('server-event-1'); + + useGhostStore.getState().replaceAuthoritativeState([], []); + expect(Object.keys(useGhostStore.getState().crews)).toHaveLength(originalCount); + expect(originalCount).toBeGreaterThan(1); + }); +}); + + +describe('authoritative Ghost Crew hydration ordering', () => { + beforeEach(resetStores); + + it('keeps a newer local crew record when a stale remote snapshot arrives', () => { + useGhostStore.getState().seedCrews(); + const local = { + ...useGhostStore.getState().crews['ghost-nightfall'], + treasury: 7777, + lastTickAt: '2026-09-02T20:30:00.000Z', + }; + useGhostStore.setState({ crews: { ...useGhostStore.getState().crews, [local.id]: local } }); + + useGhostStore.getState().replaceAuthoritativeState([{ + ...local, + treasury: 100, + lastTickAt: '2026-09-02T20:00:00.000Z', + }], []); + + expect(useGhostStore.getState().crews[local.id].treasury).toBe(7777); + }); }); diff --git a/frontend/src/stores/ghostCrewStore.ts b/frontend/src/stores/ghostCrewStore.ts index 08011db..ebb04dc 100644 --- a/frontend/src/stores/ghostCrewStore.ts +++ b/frontend/src/stores/ghostCrewStore.ts @@ -59,6 +59,8 @@ export interface GhostStoreActions { recordPlayerAttack(crewId: string, blockId: string): void; /** The crew that owns a given block, if any. */ crewForBlock(blockId: string): GhostCrew | undefined; + /** Overlay the latest durable state fetched from the authoritative world. */ + replaceAuthoritativeState(crews: GhostCrew[], feed: GhostFeedEvent[]): void; setTickActive(active: boolean): void; } @@ -186,6 +188,36 @@ export const useGhostStore = create()( ); }, + replaceAuthoritativeState(crews, feed) { + set( + (state) => { + if (crews.length === 0 && feed.length === 0) return state; + + const nextCrews = { ...state.crews }; + for (const remoteCrew of crews) { + const localCrew = state.crews[remoteCrew.id]; + // Browser state may have progressed while the initial fetch was + // in flight. Keep the newest complete crew record; authenticated + // sessions disable local ticking once hydration is active. + const localTick = localCrew ? Date.parse(localCrew.lastTickAt) : Number.NEGATIVE_INFINITY; + const remoteTick = Date.parse(remoteCrew.lastTickAt); + nextCrews[remoteCrew.id] = localCrew && localTick > remoteTick ? localCrew : remoteCrew; + } + + const seen = new Set(); + const mergedFeed = [...feed, ...state.feed].filter((event) => { + if (seen.has(event.id)) return false; + seen.add(event.id); + return true; + }).slice(0, FEED_LIMIT); + + return { crews: nextCrews, feed: mergedFeed }; + }, + false, + 'ghost/replaceAuthoritativeState', + ); + }, + setTickActive(active) { set({ tickActive: active }, false, 'ghost/setTickActive'); }, @@ -209,7 +241,7 @@ const GHOST_TICK_MS = 30_000; // 30 s real time = one world tick * tick. The store persists, so rivals keep their turf and grudges across * sessions. */ -export function useGhostTick(): void { +export function useGhostTick(enabled = true): void { const { crews, seedCrews, setTickActive } = useGhostStore(); // Seed once on first mount. @@ -221,6 +253,10 @@ export function useGhostTick(): void { const tick = useCallback(() => useGhostStore.getState().runTick(), []); useEffect(() => { + if (!enabled) { + setTickActive(false); + return; + } setTickActive(true); // First tick is delayed so the player isn't ambushed on load. const interval = setInterval(tick, GHOST_TICK_MS); @@ -228,7 +264,7 @@ export function useGhostTick(): void { clearInterval(interval); setTickActive(false); }; - }, [tick, setTickActive]); + }, [enabled, tick, setTickActive]); } // ─── Selectors ─────────────────────────────────────────────── diff --git a/scripts/verify-showdown.mjs b/scripts/verify-showdown.mjs index 152460d..5f751d0 100644 --- a/scripts/verify-showdown.mjs +++ b/scripts/verify-showdown.mjs @@ -1,6 +1,9 @@ import { mkdir, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const outputDir = '/home/ubuntu/work/slide-live/verification'; +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const outputDir = path.join(repositoryRoot, 'verification'); await mkdir(outputDir, { recursive: true }); const targets = await fetch('http://127.0.0.1:9222/json/list').then((response) => response.json()); const target = targets.find((item) => item.type === 'page' && item.url === 'about:blank') @@ -95,7 +98,7 @@ async function opsState() { status: document.querySelector('.ops-status')?.textContent?.trim() ?? null, result: document.querySelector('.ops-result')?.textContent?.replace(/\\s+/g, ' ').trim() ?? null, canvases: document.querySelectorAll('canvas').length, - opsCanvas: (() => { const canvas = document.querySelector('.modern-ops-canvas'); return canvas ? { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight, webgl2: Boolean(canvas.getContext('webgl2')), webgl: Boolean(canvas.getContext('webgl')) } : null; })() + opsCanvas: (() => { const canvas = document.querySelector('.modern-ops-canvas'); return canvas ? { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight, webgl2: Boolean(canvas.getContext('webgl2')), webgl: Boolean(canvas.getContext('webgl')), characterPackage: canvas.dataset.characterPackage ?? null } : null; })() })`); return JSON.parse(raw ?? '{}'); } @@ -183,6 +186,7 @@ console.log(JSON.stringify(report, null, 2)); socket.close(); if (!thirdPersonBefore.modernOps || thirdPersonBefore.error) process.exitCode = 1; +if (thirdPersonBefore.opsCanvas?.characterPackage !== 'character.universal-male.pipeline-v1') process.exitCode = 1; if (thirdPersonBefore.camera !== 'TPS' || firstPerson.camera !== 'FPS' || tactical.camera !== 'TAC') process.exitCode = 1; if (!switchedMember || thirdPersonBefore.selected === thirdPersonAfterSwitch.selected) process.exitCode = 1; if (!firstPerson.status || firstPerson.status === thirdPersonAfterSwitch.status) process.exitCode = 1;