Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ASSETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
53 changes: 53 additions & 0 deletions backend/supabase/functions/combat/commit-result/index.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>(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);
}
});
78 changes: 78 additions & 0 deletions backend/supabase/functions/world-tick-ghost/index.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>(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);
}
});
Loading
Loading