@seekprotocol/sdk 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets.d.ts +3 -0
- package/builders.d.ts +15 -1
- package/compile.d.ts +8 -0
- package/games.d.ts +3 -2
- package/index.d.ts +1 -0
- package/index.js +5 -5
- package/models.d.ts +78 -0
- package/package.json +1 -1
- package/types.d.ts +62 -4
- package/validate.d.ts +33 -1
package/assets.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type GameIssue } from "./games.js";
|
|
2
|
+
import { type ModelReport } from "./models.js";
|
|
2
3
|
export declare const ASSET_LIMITS: {
|
|
3
4
|
readonly imageMaxSide: 4096;
|
|
4
5
|
readonly imageAdviseSide: 2048;
|
|
@@ -49,6 +50,8 @@ export interface GameAssetReport {
|
|
|
49
50
|
seconds: number | null;
|
|
50
51
|
bytes: number;
|
|
51
52
|
}[];
|
|
53
|
+
models: ModelReport[];
|
|
54
|
+
triangles: number;
|
|
52
55
|
decodedBytes: number;
|
|
53
56
|
start: {
|
|
54
57
|
files: string[];
|
package/builders.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Area, ArenaStep, ArriveStep, CatchStep, ChoiceStep, Place, PlayStep, Point, QuestDefinition, Reward, TalkStep, Text, WalkStep } from "./types.js";
|
|
1
|
+
import type { Area, ArenaStep, ArriveStep, BeaconStep, CatchStep, ChoiceStep, GameAsset, Marker, Outcome, Place, PlayStep, Point, QuestDefinition, Reward, RewardCondition, TalkStep, Text, Variant, WalkStep } from "./types.js";
|
|
2
2
|
export declare function t(en: string): Text;
|
|
3
3
|
export declare function quest(definition: QuestDefinition): QuestDefinition;
|
|
4
4
|
export declare const zone: {
|
|
@@ -13,15 +13,26 @@ type Common = {
|
|
|
13
13
|
story?: Text;
|
|
14
14
|
next?: string | null;
|
|
15
15
|
};
|
|
16
|
+
export declare function asset(game: string, file: string): GameAsset;
|
|
17
|
+
export declare function marker(image: GameAsset, model?: GameAsset): Marker;
|
|
18
|
+
export declare function variant(id: string, title: Text, weight?: number): Variant;
|
|
19
|
+
export declare function outcome(id: string, title: Text, minScore: number): Outcome;
|
|
16
20
|
export declare const step: {
|
|
17
21
|
talk(s: Common & {
|
|
18
22
|
giver: string;
|
|
19
23
|
at?: Place;
|
|
24
|
+
model?: GameAsset;
|
|
20
25
|
}): TalkStep;
|
|
21
26
|
arrive(s: Common & {
|
|
22
27
|
at: Place;
|
|
23
28
|
radiusM?: number;
|
|
24
29
|
}): ArriveStep;
|
|
30
|
+
beacon(s: Common & {
|
|
31
|
+
minM?: number;
|
|
32
|
+
maxM?: number;
|
|
33
|
+
radiusM?: number;
|
|
34
|
+
model?: GameAsset;
|
|
35
|
+
}): BeaconStep;
|
|
25
36
|
catch(s: Common & {
|
|
26
37
|
coin?: string;
|
|
27
38
|
count: number;
|
|
@@ -46,6 +57,7 @@ export declare const step: {
|
|
|
46
57
|
game: string;
|
|
47
58
|
minScore: number;
|
|
48
59
|
maxScore: number;
|
|
60
|
+
outcomes?: Outcome[];
|
|
49
61
|
}): PlayStep;
|
|
50
62
|
};
|
|
51
63
|
export declare const reward: {
|
|
@@ -54,5 +66,7 @@ export declare const reward: {
|
|
|
54
66
|
cosmetic(item: string): Reward;
|
|
55
67
|
boost(boost: string, count?: number): Reward;
|
|
56
68
|
pool(coin: string, perPlayer: number, maxPlayers: number): Reward;
|
|
69
|
+
draw(coins: string[], perPlayer: number, maxPlayers: number): Reward;
|
|
70
|
+
when(condition: RewardCondition, r: Reward): Reward;
|
|
57
71
|
};
|
|
58
72
|
export {};
|
package/compile.d.ts
CHANGED
|
@@ -9,7 +9,15 @@ export interface CompileOptions {
|
|
|
9
9
|
version: string;
|
|
10
10
|
build: number;
|
|
11
11
|
games?: Record<string, string>;
|
|
12
|
+
gameTexts?: Record<string, Record<string, string>>;
|
|
12
13
|
}
|
|
14
|
+
export declare const GAME_TEXT_LIMITS: {
|
|
15
|
+
readonly keys: 300;
|
|
16
|
+
readonly chars: 600;
|
|
17
|
+
readonly totalChars: 15000;
|
|
18
|
+
readonly key: RegExp;
|
|
19
|
+
};
|
|
20
|
+
export declare function gameTextIssues(gameId: string, texts: unknown): Issue[];
|
|
13
21
|
export declare function compile(q: QuestDefinition, opts: CompileOptions): QuestBundle;
|
|
14
22
|
export declare function canonicalJson(value: unknown): string;
|
|
15
23
|
export declare function bundleHash(bundle: QuestBundle): Promise<string>;
|
package/games.d.ts
CHANGED
|
@@ -6,15 +6,16 @@ export declare const GAME_APP_ID_PATTERN: RegExp;
|
|
|
6
6
|
export declare const GAME_HOST_SUFFIX = "play.seekprotocol.ai";
|
|
7
7
|
export declare const GAME_PROXY_ORIGIN = "https://api.play.seekprotocol.ai";
|
|
8
8
|
export declare const GAME_LIMITS: {
|
|
9
|
-
readonly files:
|
|
9
|
+
readonly files: 300;
|
|
10
10
|
readonly fileBytes: number;
|
|
11
11
|
readonly totalBytes: number;
|
|
12
|
+
readonly totalAdviseBytes: number;
|
|
12
13
|
readonly pathLength: 120;
|
|
13
14
|
readonly depth: 5;
|
|
14
15
|
readonly segment: RegExp;
|
|
15
16
|
readonly storageBytes: number;
|
|
16
17
|
};
|
|
17
|
-
export type GameFileKind = "html" | "script" | "style" | "data" | "image" | "audio" | "font";
|
|
18
|
+
export type GameFileKind = "html" | "script" | "style" | "data" | "image" | "audio" | "font" | "model";
|
|
18
19
|
export declare const GAME_FILE_TYPES: Record<string, {
|
|
19
20
|
type: string;
|
|
20
21
|
kind: GameFileKind;
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var F=1,W=1,$=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/,Q=["location.markers","spawns.place","catch","steps","arena","rewards.cards","rewards.cosmetics","rewards.boosts","rewards.crypto","gamekit"],U={places:["public","private_with_permission"],effort:["light","moderate","hard"],ai:["none","assisted","generated"]};function tt(e){return{en:e}}function nt(e){return e}var st={anywhere(){return{kind:"anywhere"}},circle(e,t,n){return{kind:"circle",name:e,center:t,radiusM:n}},city(e,t,n=10){return{kind:"circle",name:e,center:t,radiusM:Math.round(n*1e3)}},place(e,t){return{name:e,at:t}}},it={talk(e){return{kind:"talk",...e}},arrive(e){return{kind:"arrive",radiusM:50,...e}},catch(e){return{kind:"catch",coin:"any",...e}},walk(e){return{kind:"walk",verified:!1,...e}},arena(e){return{kind:"arena",wins:1,...e}},choice(e){return{kind:"choice",...e}},play(e){return{kind:"play",...e}}},at={xp(e){return{kind:"xp",amount:e}},card(e){return{kind:"card",card:e}},cosmetic(e){return{kind:"cosmetic",item:e}},boost(e,t=1){return{kind:"boost",boost:e,count:t}},pool(e,t,n){return{kind:"pool",coin:e,perPlayer:t,maxPlayers:n}}};var m={steps:{min:1,max:30},idPattern:/^[a-z0-9][a-z0-9-]{2,39}$/,stepIdPattern:/^[a-z0-9][a-z0-9-]{0,39}$/,title:60,summary:160,story:600,areaRadiusM:{min:100,max:5e4},arriveRadiusM:{min:25,max:500},catchCount:{min:1,max:50},catchWithinM:{min:50,max:2e3},walkSteps:{min:100,max:5e4},arenaWins:{min:1,max:20},choiceOptions:{min:2,max:4},xp:{max:5e3},boostCount:{max:3},rewards:{max:6},windowDays:{max:92}},oe={talk:["location.markers"],arrive:["location.markers"],catch:["catch"],walk:["steps"],arena:["arena"],choice:[],play:["gamekit"]};function ce(e,t){let s=(t.lat-e.lat)*Math.PI/180,a=(t.lng-e.lng)*Math.PI/180,o=Math.sin(s/2)**2+Math.cos(e.lat*Math.PI/180)*Math.cos(t.lat*Math.PI/180)*Math.sin(a/2)**2;return 2*6371e3*Math.asin(Math.min(1,Math.sqrt(o)))}var C=e=>!!e&&Number.isFinite(e.lat)&&Number.isFinite(e.lng)&&Math.abs(e.lat)<=85&&Math.abs(e.lng)<=180;function de(e){let t=new Set;for(let n of e.steps){for(let s of oe[n.kind])t.add(s);n.kind==="catch"&&n.near&&t.add("spawns.place")}for(let n of e.rewards)n.kind==="card"&&t.add("rewards.cards"),n.kind==="cosmetic"&&t.add("rewards.cosmetics"),n.kind==="boost"&&t.add("rewards.boosts"),n.kind==="pool"&&t.add("rewards.crypto");return t}function z(e,t){let n=e[t];return n.kind==="choice"?n.options.map(s=>s.next):n.next!==void 0?[n.next]:[e[t+1]?.id??null]}function K(e){let t=[],n=(i,c,l)=>t.push({path:i,code:c,message:l}),s=(i,c,l,f)=>{(!Number.isFinite(c)||c<(l.min??0)||c>l.max)&&n(i,"out_of_range",`${f} must be between ${l.min??0} and ${l.max}.`)},a=(i,c,l,f)=>{if(!c||typeof c.en!="string"||!c.en.trim()){f&&n(i,"text_missing","Text is required.");return}c.en.length>l&&n(i,"text_too_long",`At most ${l} characters (${c.en.length}).`)};m.idPattern.test(e.id??"")||n("id","bad_id","Use 3 to 40 lowercase letters, digits and hyphens."),a("title",e.title,m.title,!0),a("summary",e.summary,m.summary,!1),e.area?.kind==="circle"?(C(e.area.center)||n("area.center","bad_point","Not a valid coordinate."),s("area.radiusM",e.area.radiusM,m.areaRadiusM,"The area radius")):e.area?.kind!=="anywhere"&&n("area","bad_area","Use zone.anywhere(), zone.city() or zone.circle().");let o=(i,c)=>{if(!C(c))return n(i,"bad_point","Not a valid coordinate.");e.area?.kind==="circle"&&C(e.area.center)&&ce(e.area.center,c)>e.area.radiusM&&n(i,"outside_area",`This place is outside the quest area (${e.area.name}).`)};if(e.window){let i=Date.parse(e.window.from),c=Date.parse(e.window.to);!Number.isFinite(i)||!Number.isFinite(c)||c<=i?n("window","bad_window","The window needs a start before its end."):c-i>m.windowDays.max*864e5&&n("window","window_too_long",`A window lasts at most ${m.windowDays.max} days.`)}for(let[i,c]of(e.capabilities??[]).entries())Q.includes(c)||n(`capabilities[${i}]`,"unknown_capability",`Unknown capability "${c}".`);for(let i of de(e))e.capabilities?.includes(i)||n("capabilities","capability_missing",`The quest uses "${i}" but does not declare it.`);let u=e.steps??[];(u.length<m.steps.min||u.length>m.steps.max)&&n("steps","step_count",`A quest has ${m.steps.min} to ${m.steps.max} steps.`);let d=new Set;if(u.forEach((i,c)=>{let l=`steps[${c}]`;switch(m.stepIdPattern.test(i.id??"")||n(`${l}.id`,"bad_id","Use lowercase letters, digits and hyphens."),d.has(i.id)&&n(`${l}.id`,"duplicate_id",`Step id "${i.id}" is used twice.`),d.add(i.id),a(`${l}.title`,i.title,m.title,!0),a(`${l}.story`,i.story,m.story,!1),i.kind){case"talk":i.giver||n(`${l}.giver`,"giver_missing","Name the quest-giver."),i.at?o(`${l}.at`,i.at.at):e.area?.kind!=="circle"&&n(`${l}.at`,"place_missing","A quest playable anywhere needs a place for its giver.");break;case"arrive":o(`${l}.at`,i.at?.at),s(`${l}.radiusM`,i.radiusM,m.arriveRadiusM,"The arrival radius");break;case"catch":s(`${l}.count`,i.count,m.catchCount,"The number of catches"),i.near&&(o(`${l}.near`,i.near.at),s(`${l}.withinM`,i.withinM??500,m.catchWithinM,"The catch radius"));break;case"walk":s(`${l}.steps`,i.steps,m.walkSteps,"The number of steps");break;case"arena":s(`${l}.wins`,i.wins,m.arenaWins,"The number of wins");break;case"choice":{let f=i.options?.length??0;(f<m.choiceOptions.min||f>m.choiceOptions.max)&&n(`${l}.options`,"option_count","A choice has 2 to 4 options.");let h=new Set;i.options?.forEach((g,D)=>{h.has(g.id)&&n(`${l}.options[${D}].id`,"duplicate_id",`Option "${g.id}" is used twice.`),h.add(g.id),a(`${l}.options[${D}].label`,g.label,40,!0)});break}case"play":i.game?$.test(i.game)||n(`${l}.game`,"bad_game_id","A game id is its folder under games/: 3 to 30 lowercase letters, digits and hyphens."):n(`${l}.game`,"game_missing","Name the minigame."),(!(i.maxScore>0)||!(i.minScore>0)||i.minScore>i.maxScore)&&n(`${l}.minScore`,"bad_score","Scores are positive and minScore is at most maxScore.");break;default:n(`${l}.kind`,"unknown_step",`Unknown step kind "${i.kind}".`)}}),u.forEach((i,c)=>{z(u,c).forEach(l=>{l!==null&&!d.has(l)&&n(`steps[${c}].next`,"unknown_next",`No step "${l}".`)})}),u.length&&!t.some(i=>i.code==="unknown_next"||i.code==="duplicate_id")){let i=new Map(u.map((h,g)=>[h.id,g])),c=new Map,l=!1,f=h=>{if(c.get(h)!=="done"){if(c.get(h)==="open"){l=!0;return}c.set(h,"open");for(let g of z(u,i.get(h)))g!==null&&f(g);c.set(h,"done")}};f(u[0].id),l&&n("steps","loop","Steps lead back to an earlier step. A quest has to end."),u.forEach((h,g)=>{c.has(h.id)||n(`steps[${g}]`,"unreachable",`Step "${h.id}" can never be reached.`)})}let r=e.rewards??[];r.length>m.rewards.max&&n("rewards","reward_count",`At most ${m.rewards.max} rewards.`),r.forEach((i,c)=>{let l=`rewards[${c}]`;i.kind==="xp"&&s(`${l}.amount`,i.amount,{min:1,max:m.xp.max},"XP"),i.kind==="boost"&&s(`${l}.count`,i.count,{min:1,max:m.boostCount.max},"The boost count"),i.kind==="pool"&&((!(i.perPlayer>0)||!(i.maxPlayers>0)||!Number.isInteger(i.maxPlayers))&&n(l,"bad_pool","A pool pays a positive amount to a whole number of players."),(e.minAge??18)<18&&n("minAge","pool_needs_adults","A quest that pays coins is for players of 18 and over."))}),r.some(i=>i.kind==="pool")&&e.repeat&&e.repeat!=="never"&&n("repeat","pool_repeats","A quest that pays coins cannot be repeated.");let p=e.declarations;if(!p||typeof p!="object")n("declarations","declarations_missing","Say what the quest asks of players: places, effort, hazards, AI, sponsoring and game events.");else{let i=l=>{U[l].includes(p[l])||n(`declarations.${l}`,"bad_declaration",`One of ${U[l].join(", ")}.`)};i("places"),i("effort"),i("ai");for(let l of["hazards","sponsored","gameEvents"])typeof p[l]!="boolean"&&n(`declarations.${l}`,"bad_declaration","true or false.");p.rights!==!0&&n("declarations.rights","rights_not_declared","Declare rights: true. Only use texts, images, sounds, fonts, characters, logos and code that are yours or that you have a licence for.");let c=(e.steps??[]).reduce((l,f)=>l+(f.kind==="walk"?f.steps:0),0);p.effort==="light"&&c>1e4&&n("declarations.effort","effort_understated",`The walk steps add up to ${c} steps; that is not light.`)}return t}var le=/^(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/,T=class extends Error{constructor(n){super(`The quest has ${n.length} problem(s):
|
|
2
|
-
${
|
|
3
|
-
`)}`);this.issues=n}};function ue(e,t){if(t===void 0)return[];let n=[];if(!t||typeof t!="object"||Array.isArray(t))return[{path:"games",code:"bad_games",message:"games maps a game id to its hash."}];let s=new Set(e.steps.flatMap(a=>a.kind==="play"?[a.game]:[]));for(let[a,o]of Object.entries(t))$.test(a)||n.push({path:`games.${a}`,code:"bad_game_id",message:"A game id is 3 to 30 lowercase letters, digits and hyphens."}),(typeof o!="string"||!/^[0-9a-f]{64}$/.test(o))&&n.push({path:`games.${a}`,code:"bad_game_hash",message:"A game hash is 64 lowercase hex characters."}),s.has(a)||n.push({path:`games.${a}`,code:"game_unused",message:`No play step uses the game "${a}".`});for(let a of s)a in t||n.push({path:"games",code:"game_missing",message:`The play step's game "${a}" is not in the quest. Put it in games/${a}/.`});return n}function J(e,t){let n=K(e);if(le.test(t.version)||n.push({path:"version",code:"bad_version",message:"Use a version like 1.2.0."}),(!Number.isInteger(t.build)||t.build<1)&&n.push({path:"build",code:"bad_build",message:"The build number is a whole number from 1."}),n.push(...ue(e,t.games)),n.length)throw new T(n);let s={},a=(u,d)=>{if(d)return s[u]=d.en.trim(),u},o=e.steps.map((u,d)=>{let r=`steps.${u.id}`,[p]=z(e.steps,d),{title:i,story:c,next:l,...f}=u,h={...f,title:a(`${r}.title`,i),...c?{story:a(`${r}.story`,c)}:{},next:u.kind==="choice"?null:p??null};return u.kind==="choice"&&(h.options=u.options.map(g=>({id:g.id,label:a(`${r}.options.${g.id}`,g.label),next:g.next}))),h});return{manifest:{format:1,runtime:1,id:e.id,version:t.version,build:t.build,title:a("title",e.title),...e.summary?{summary:a("summary",e.summary)}:{},area:e.area,...e.window?{window:e.window}:{},repeat:e.repeat??"never",minAge:e.minAge??18,declarations:{...e.declarations},capabilities:[...e.capabilities].sort(),rewards:e.rewards,start:e.steps[0].id,...t.games&&Object.keys(t.games).length?{games:{...t.games}}:{}},steps:o,texts:{en:s}}}function M(e){if(e===null||typeof e!="object"){if(typeof e=="number"&&!Number.isFinite(e))throw new Error("Not JSON: a number that is not finite.");return JSON.stringify(e)}return Array.isArray(e)?`[${e.map(M).join(",")}]`:`{${Object.entries(e).filter(([,n])=>n!==void 0).sort(([n],[s])=>n<s?-1:n>s?1:0).map(([n,s])=>`${JSON.stringify(n)}:${M(s)}`).join(",")}}`}var pe=e=>[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join("");async function G(e){let t=new TextEncoder().encode(M(e));return pe(await crypto.subtle.digest("SHA-256",t))}var H=(e,t)=>`seek-bundle-v1:${t}:${e}`,fe=e=>btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),V=e=>{let t=atob(e.replace(/-/g,"+").replace(/_/g,"/")+"=".repeat((4-e.length%4)%4));return Uint8Array.from(t,n=>n.charCodeAt(0))};async function pt(){let e=await crypto.subtle.generateKey({name:"Ed25519"},!0,["sign","verify"]),t=await crypto.subtle.exportKey("jwk",e.publicKey),n=await crypto.subtle.exportKey("jwk",e.privateKey);return{keyId:await me(t),publicJwk:t,privateJwk:n}}async function me(e){let t=await crypto.subtle.digest("SHA-256",V(e.x));return[...new Uint8Array(t)].slice(0,8).map(n=>n.toString(16).padStart(2,"0")).join("")}async function ft(e,t,n,s){let a=await crypto.subtle.importKey("jwk",s,{name:"Ed25519"},!1,["sign"]),o=await crypto.subtle.sign({name:"Ed25519"},a,new TextEncoder().encode(H(e,t)));return{by:t,keyId:n,sig:fe(new Uint8Array(o))}}async function ge(e,t,n){try{let s=await crypto.subtle.importKey("jwk",n,{name:"Ed25519"},!1,["verify"]);return await crypto.subtle.verify({name:"Ed25519"},s,V(t.sig),new TextEncoder().encode(H(e,t.by)))}catch{return!1}}async function mt(e,t,n){return t.by!=="seek"||!Object.hasOwn(n,t.keyId)?!1:ge(e,t,n[t.keyId])}var he="0".repeat(64);async function xe(e){let t=["seek-log-v1",e.seq,e.prev_entry,e.event,e.quest_id,e.version_id,e.hash,e.seek_key_id,e.seek_sig].join(":"),n=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(t));return[...new Uint8Array(n)].map(s=>s.toString(16).padStart(2,"0")).join("")}async function ht(e,t={size:0,head:he}){let{size:n,head:s}=t;for(let a of e){if(a.seq!==n+1)return{ok:!1,seq:a.seq,reason:"bad_sequence"};if(a.prev_entry!==s)return{ok:!1,seq:a.seq,reason:"bad_link"};if(await xe(a)!==a.entry_hash)return{ok:!1,seq:a.seq,reason:"bad_hash"};n=a.seq,s=a.entry_hash}return{ok:!0,size:n,head:s}}function ye(e){let t=e.texts?.en??{},n=o=>o===void 0?void 0:{en:t[o]??""},s=e.manifest,a=e.steps.map(o=>{let{title:u,story:d,next:r,...p}=o,i={...p,title:n(u),...d?{story:n(d)}:{},next:r};return i.kind==="choice"&&o.kind==="choice"&&(i.options=o.options.map(c=>({id:c.id,label:n(c.label),next:c.next}))),i});return{id:s.id,title:n(s.title),...s.summary?{summary:n(s.summary)}:{},area:s.area,...s.window?{window:s.window}:{},repeat:s.repeat,minAge:s.minAge,declarations:s.declarations,capabilities:s.capabilities,steps:a,rewards:s.rewards}}async function X(e,t){let n=(r,p)=>({ok:!1,issues:[{path:"",code:r,message:p}]}),s=e;if(!s||typeof s!="object"||!s.manifest||!Array.isArray(s.steps)||!s.texts?.en)return n("bad_bundle","Not a quest bundle.");if(s.manifest.format!==1)return n("bad_format",`Unknown bundle format ${String(s.manifest.format)}.`);let a,o;try{a=ye(s),o=J(a,{version:s.manifest.version,build:s.manifest.build,...s.manifest.games!==void 0?{games:s.manifest.games}:{}})}catch(r){return r instanceof T?{ok:!1,issues:r.issues}:n("bad_bundle","The bundle could not be read.")}let[u,d]=await Promise.all([G(s),G(o)]);return u!==d?n("not_compiled","The bundle is not what seek build makes. Build it again with the CLI."):t!==void 0&&t!==u?n("hash_mismatch","The bundle does not match its hash."):{ok:!0,hash:u,definition:a}}var we=".seek",L={fileBytes:64*1024*1024,unpackedBytes:128*1024*1024},w=class extends Error{},kt=e=>`${e.manifest.id}-${e.manifest.version}-${e.manifest.build}${we}`;async function Y(e,t){let n=e.getReader(),s=[],a=0;for(;;){let{done:d,value:r}=await n.read();if(d)break;if(a+=r.byteLength,a>t)throw await n.cancel(),new w("This .seek file unpacks to more than a build can be.");s.push(r)}let o=new Uint8Array(a),u=0;for(let d of s)o.set(d,u),u+=d.byteLength;return o}var Z=e=>new Blob([e]).stream();async function At(e){let t=new TextEncoder().encode(JSON.stringify({format:1,bundle:e.bundle,hash:e.hash,signature:e.signature,games:e.games})),n=Z(t).pipeThrough(new CompressionStream("gzip"));return await Y(n,L.fileBytes)}async function _t(e){if(e.byteLength>L.fileBytes)throw new w("This .seek file is too big.");if(e[0]!==31||e[1]!==139)throw new w("This is not a .seek file. Make one with seek build.");let t;try{let o=Z(e).pipeThrough(new DecompressionStream("gzip"));t=JSON.parse(new TextDecoder().decode(await Y(o,L.unpackedBytes)))}catch(o){throw o instanceof w?o:new w("This .seek file is damaged. Build it again.")}if(!t||t.format!==1)throw new w("This .seek file was made by another version of seek. Build it again.");let n=await X(t.bundle,t.hash);if(!n.ok)throw new w(`This .seek file is damaged: ${n.issues[0].message}`);let s=t.bundle.manifest.games??{},a=t.games??{};for(let[o,u]of Object.entries(s))if(!Object.hasOwn(a,o)||a[o].hash!==u)throw new w(`This .seek file is damaged: the game "${o}" is not the one the quest names.`);if(t.signature&&(t.signature.by!=="maker"||typeof t.signature.keyId!="string"||typeof t.signature.sig!="string"))throw new w("This .seek file has a signature that is not a maker's.");return{format:1,bundle:t.bundle,hash:t.hash,signature:t.signature??null,games:a}}var be={imageMaxSide:4096,imageAdviseSide:2048,decodedMaxBytes:96*1024*1024,decodedAdviseBytes:48*1024*1024,startMaxBytes:8*1024*1024,startAdviseBytes:3*1024*1024,wavMaxBytes:2*1024*1024,wavAdviseBytes:256*1024,audioMaxSeconds:600,opaquePngAdviseBytes:300*1024,gifAdviseBytes:1024*1024},ke={maxBytes:1024*1024,minWidth:1280,minHeight:720,maxWidth:3840,maxHeight:2160,ratio:16/9,ratioTolerance:.01,types:["image/png","image/jpeg","image/webp"]},O=(e,t)=>(e[t]<<8|e[t+1])>>>0,I=(e,t)=>(e[t]|e[t+1]<<8)>>>0,q=(e,t)=>(e[t]|e[t+1]<<8|e[t+2]<<16)>>>0,b=(e,t)=>(e[t]<<24|e[t+1]<<16|e[t+2]<<8|e[t+3])>>>0,E=(e,t)=>(e[t]|e[t+1]<<8|e[t+2]<<16|e[t+3]<<24)>>>0,k=(e,t,n)=>String.fromCharCode(...e.subarray(t,t+n));function B(e,t,n=0,s=e.length){let a=[...t].map(o=>o.charCodeAt(0));e:for(let o=Math.max(0,n);o<=Math.min(e.length,s)-a.length;o++){for(let u=0;u<a.length;u++)if(e[o+u]!==a[u])continue e;return o}return-1}function Ae(e){if(e.length<33||k(e,12,4)!=="IHDR")return null;let t=e[25],n=B(e,"IDAT",33),s=B(e,"tRNS",33,n===-1?e.length:n);return{width:b(e,16),height:b(e,20),alpha:t===4||t===6||s!==-1}}function _e(e){let t=2;for(;t+9<e.length;){if(e[t]!==255){t++;continue}let n=e[t+1];if(n===255){t++;continue}if(n===216||n===1||n>=208&&n<=215){t+=2;continue}if(n===217||n===218)return null;if(n>=192&&n<=207&&n!==196&&n!==200&&n!==204)return{height:O(e,t+5),width:O(e,t+7)};t+=2+O(e,t+2)}return null}function Se(e){return e.length>=10?{width:I(e,6),height:I(e,8)}:null}function ve(e){if(e.length<30)return null;let t=k(e,12,4);if(t==="VP8 ")return e[23]!==157||e[24]!==1||e[25]!==42?null:{width:I(e,26)&16383,height:I(e,28)&16383};if(t==="VP8L"){if(e[20]!==47)return null;let n=e[21],s=e[22],a=e[23],o=e[24];return{width:1+((s&63)<<8|n),height:1+((o&15)<<10|a<<2|(s&192)>>6)}}return t==="VP8X"?{width:1+q(e,24),height:1+q(e,27)}:null}function $e(e){let t=null,n=Math.min(e.length,256*1024);for(let s=B(e,"ispe",0,n);s!==-1&&!(s+16>e.length);s=B(e,"ispe",s+4,n)){let a={width:b(e,s+8),height:b(e,s+12)};(!t||a.width*a.height>t.width*t.height)&&(t=a)}return t}function ne(e,t){switch(v(e)){case"png":return Ae(t);case"jpg":case"jpeg":return _e(t);case"gif":return Se(t);case"webp":return ve(t);case"avif":return $e(t);default:return null}}function Te(e){let t=0;for(let n=12;n+8<=e.length;){let s=k(e,n,4),a=E(e,n+4);if(s==="fmt "&&n+20<=e.length&&(t=E(e,n+16)),s==="data")return t>0?a/t:null;n+=8+a+a%2}return null}var ee={v1:[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320],v2:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160]},Me=[44100,48e3,32e3];function Pe(e){let t=0;for(k(e,0,3)==="ID3"&&e.length>10&&(t=10+(e[6]<<21|e[7]<<14|e[8]<<7|e[9]));t+4<e.length&&t<512*1024;t++){if(e[t]!==255||(e[t+1]&224)!==224)continue;let n=e[t+1]>>3&3,s=e[t+1]>>1&3,a=e[t+2]>>2&3,o=e[t+2]>>4;if(n===1||s!==1||a===3||o===0||o===15)continue;let u=n===3,d=Me[a]/(u?1:n===2?2:4),r=(u?ee.v1:ee.v2)[o],p=e[t+3]>>6===3,i=u?1152:576,c=t+4+(u?p?17:32:p?9:17),l=k(e,c,4);return(l==="Xing"||l==="Info")&&c+12<=e.length&&b(e,c+4)&1?b(e,c+8)*i/d:(e.length-t)*8/(r*1e3)}return null}function Ee(e){if(k(e,0,4)!=="OggS"||e.length<28)return null;let t=27+e[26],n=0,s=0;if(k(e,t,7)==="vorbis"?n=E(e,t+12):k(e,t,8)==="OpusHead"&&(n=48e3,s=I(e,t+10)),!n)return null;for(let a=e.length-14;a>=0;a--){if(e[a]!==79||k(e,a,4)!=="OggS")continue;let o=E(e,a+6)+E(e,a+10)*2**32;return Math.max(0,o-s)/n}return null}function Ie(e){let t=B(e,"mvhd");if(t===-1||t+32>e.length)return null;if(e[t+4]===1){let a=b(e,t+24),o=b(e,t+28)*2**32+b(e,t+32);return a?o/a:null}let s=b(e,t+16);return s?b(e,t+20)/s:null}function Be(e,t){switch(v(e)){case"wav":return Te(t);case"mp3":return Pe(t);case"ogg":return Ee(t);case"m4a":return Ie(t);default:return null}}var ze=/<(?!a[\s>])[a-z][^>]*?\s(?:src|href|poster)\s*=\s*["']([^"']+)["']/gi,te=/url\(\s*["']?([^"')]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi,Re=/(?:^|[\s;}])(?:import|export)\s*(?:[\w$*{}\s,]+?\s*from\s*)?["']([^"'\n]+)["']/g;function Ue(e,t){let n=t.trim().split(/[?#]/)[0];if(!n||/^[a-z][a-z0-9+.-]*:/i.test(n)||n.startsWith("//"))return null;let s=n.startsWith("/")?[]:e.split("/").slice(0,-1);for(let a of n.split("/"))if(!(a===""||a==="."))if(a===".."){if(!s.length)return null;s.pop()}else s.push(a);return s.join("/")||null}function Ce(e){if(!e.has(x))return[];let t=new Set([x]),n=[x],s=a=>new TextDecoder().decode(e.get(a));for(;n.length;){let a=n.shift(),o=S(a)?.kind,u=[],d=(r,p)=>{r.lastIndex=0;for(let i=r.exec(p);i;i=r.exec(p))u.push(i[1]??i[2])};if(o==="html"){let r=s(a);d(ze,r),d(te,r)}else if(o==="style")d(te,s(a));else if(o==="script")d(Re,s(a));else continue;for(let r of u){let p=Ue(a,r);p&&e.has(p)&&!t.has(p)&&(t.add(p),n.push(p))}}return[...t].sort()}var _=e=>`${(e/(1024*1024)).toFixed(1)} MB`,P=e=>e<1024*1024?`${Math.round(e/1024)} KB`:_(e);function se(e){let t=e instanceof Map?e:new Map([...e].map(d=>[d.path,d.bytes])),n=be,s={images:[],sounds:[],decodedBytes:0,start:{files:[],bytes:0},issues:[],advice:[]},a=(d,r,p)=>s.issues.push({path:d,code:r,message:p}),o=(d,r,p)=>s.advice.push({path:d,code:r,message:p});for(let[d,r]of[...t].sort(([p],[i])=>p<i?-1:1)){let p=S(d),i=v(d);if(p?.kind==="image"&&i!=="svg"){let c=ne(d,r);if(!c||!c.width||!c.height){i==="avif"?o(d,"image_size_unknown","Seek could not read the size of this AVIF image, so it is not counted in the memory budget. WebP or PNG is safer."):a(d,"image_unreadable","Seek cannot read the size of this image: the file looks broken. Export it again.");continue}let l=c.width*c.height*4;s.images.push({path:d,width:c.width,height:c.height,bytes:r.byteLength,decoded:l,...c.alpha!==void 0?{alpha:c.alpha}:{}}),s.decodedBytes+=l;let f=Math.max(c.width,c.height);f>n.imageMaxSide?a(d,"image_too_large",`${c.width} x ${c.height} pixels: an image is at most ${n.imageMaxSide} pixels on its longest side. Phones cannot keep larger ones in memory. seek optimize scales it down.`):f>n.imageAdviseSide&&o(d,"image_large",`${c.width} x ${c.height} pixels takes ${_(l)} of memory once loaded. ${n.imageAdviseSide} pixels on the longest side is plenty for a phone; seek optimize scales it down.`),i==="png"&&c.alpha===!1&&r.byteLength>n.opaquePngAdviseBytes&&o(d,"opaque_png",`A ${P(r.byteLength)} PNG without transparency is probably a photo. As WebP or JPEG it is often 5 to 10 times smaller: seek optimize --webp.`),i==="gif"&&r.byteLength>n.gifAdviseBytes&&o(d,"heavy_gif",`A ${P(r.byteLength)} GIF. Animated GIFs are heavy to download and to play; an animated WebP or a sprite sheet is far lighter.`)}else if(p?.kind==="audio"){let c=Be(d,r);s.sounds.push({path:d,seconds:c,bytes:r.byteLength}),c!==null&&c>n.audioMaxSeconds&&a(d,"audio_too_long",`This sound lasts ${Math.round(c)} seconds; one sound is at most ${n.audioMaxSeconds/60} minutes.`),i==="wav"&&(r.byteLength>n.wavMaxBytes?a(d,"wav_too_big",`A WAV of ${P(r.byteLength)} is uncompressed sound: at most ${P(n.wavMaxBytes)}. As M4A it is about a tenth of the size: seek optimize --audio.`):r.byteLength>n.wavAdviseBytes&&o(d,"wav_audio",`A WAV is uncompressed sound. As M4A this ${P(r.byteLength)} is about a tenth: seek optimize --audio.`))}else p?.kind==="font"&&(i==="ttf"||i==="otf")&&o(d,"font_format",`A .${i} font: as WOFF2 it is usually 30 to 50% smaller and loads the same everywhere.`)}s.decodedBytes>n.decodedMaxBytes?a("","memory_too_big",`All images together take ${_(s.decodedBytes)} of memory once loaded; a game may use at most ${_(n.decodedMaxBytes)}. Scale the largest ones down (seek optimize) or load them only when needed.`):s.decodedBytes>n.decodedAdviseBytes&&o("","memory_heavy",`All images together take ${_(s.decodedBytes)} of memory once loaded. Older phones start to struggle above ${_(n.decodedAdviseBytes)}.`);let u=Ce(t);return s.start={files:u,bytes:u.reduce((d,r)=>d+(t.get(r)?.byteLength??0),0)},s.start.bytes>n.startMaxBytes?a(x,"start_too_big",`index.html loads ${_(s.start.bytes)} before the game can start; at most ${_(n.startMaxBytes)}. On a phone outside that takes far too long. Load the rest after ready(), with import() or fetch().`):s.start.bytes>n.startAdviseBytes&&o(x,"start_heavy",`index.html loads ${_(s.start.bytes)} before the game can start. Outside, on 4G, every MB is about a second of waiting; keep it under ${_(n.startAdviseBytes)} and load the rest after ready().`),s}function $t(e){let t=ke;if(e.byteLength>t.maxBytes)return{code:"too_big",message:"A cover is at most 1 MB."};let n=e,s=n[0]===137&&n[1]===80?"png":n[0]===255&&n[1]===216?"jpg":k(n,0,4)==="RIFF"&&k(n,8,4)==="WEBP"?"webp":null;if(!s)return{code:"bad_cover",message:"A cover is a PNG, JPEG or WebP image."};let a=ne(`cover.${s}`,e);if(!a)return{code:"bad_cover",message:"Seek cannot read the size of this image: export it again."};let{width:o,height:u}=a;return o<t.minWidth||u<t.minHeight?{code:"cover_too_small",message:`A cover is at least ${t.minWidth} x ${t.minHeight} pixels; this one is ${o} x ${u}.`}:o>t.maxWidth||u>t.maxHeight?{code:"cover_too_large",message:`A cover is at most ${t.maxWidth} x ${t.maxHeight} pixels; this one is ${o} x ${u}.`}:Math.abs(o/u/t.ratio-1)>t.ratioTolerance?{code:"cover_ratio",message:`A cover is 16:9, like 1920 x 1080; this one is ${o} x ${u}.`}:null}var ae=1,It=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}$/,Ge="play.seekprotocol.ai",ie="https://api.play.seekprotocol.ai",A={files:200,fileBytes:5*1024*1024,totalBytes:15*1024*1024,pathLength:120,depth:5,segment:/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/,storageBytes:64*1024},Le={html:{type:"text/html; charset=utf-8",kind:"html",text:!0},js:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},mjs:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},css:{type:"text/css; charset=utf-8",kind:"style",text:!0},json:{type:"application/json; charset=utf-8",kind:"data",text:!0},svg:{type:"image/svg+xml",kind:"image",text:!0},png:{type:"image/png",kind:"image",text:!1},jpg:{type:"image/jpeg",kind:"image",text:!1},jpeg:{type:"image/jpeg",kind:"image",text:!1},gif:{type:"image/gif",kind:"image",text:!1},webp:{type:"image/webp",kind:"image",text:!1},avif:{type:"image/avif",kind:"image",text:!1},mp3:{type:"audio/mpeg",kind:"audio",text:!1},ogg:{type:"audio/ogg",kind:"audio",text:!1},wav:{type:"audio/wav",kind:"audio",text:!1},m4a:{type:"audio/mp4",kind:"audio",text:!1},woff:{type:"font/woff",kind:"font",text:!1},woff2:{type:"font/woff2",kind:"font",text:!1},ttf:{type:"font/ttf",kind:"font",text:!1},otf:{type:"font/otf",kind:"font",text:!1}},x="index.html",Oe=/^[0-9a-f]{64}$/,je=e=>[...new Uint8Array(e)].map(t=>t.toString(16).padStart(2,"0")).join(""),R=async e=>je(await crypto.subtle.digest("SHA-256",typeof e=="string"?new TextEncoder().encode(e):e)),v=e=>/\.([a-z0-9]+)$/i.exec(e)?.[1]?.toLowerCase()??"",S=e=>Le[v(e)];function N(e){let t=(s,a)=>({path:e,code:s,message:a});if(typeof e!="string"||!e)return t("bad_path","A file needs a path.");if(e.length>A.pathLength)return t("path_too_long",`A path is at most ${A.pathLength} characters.`);let n=e.split("/");return n.length>A.depth?t("path_too_deep",`Folders go at most ${A.depth-1} deep.`):n.every(s=>A.segment.test(s))?S(e)?null:t("file_type",`".${v(e)||"(none)"}" files are not allowed. A game is html, js, css, json, images, audio and fonts.`):t("bad_path",'Use letters, digits, dots, hyphens and underscores; no hidden files, spaces or "..".')}var y=(e,t,n=0)=>t.every((s,a)=>e[n+a]===s),Ne=[["a Windows executable",[77,90]],["an ELF executable",[127,69,76,70]],["a Mach-O executable",[207,250,237,254]],["a Mach-O executable",[206,250,237,254]],["a Mach-O executable",[254,237,250,207]],["a Mach-O or Java binary",[202,254,186,190]],["a script with an interpreter line",[35,33]],["a zip, jar or apk archive",[80,75,3,4]],["WebAssembly",[0,97,115,109]],["a gzip archive",[31,139]],["a PDF",[37,80,68,70]]],De={png:e=>y(e,[137,80,78,71,13,10,26,10]),jpg:e=>y(e,[255,216,255]),jpeg:e=>y(e,[255,216,255]),gif:e=>y(e,[71,73,70,56]),webp:e=>y(e,[82,73,70,70])&&y(e,[87,69,66,80],8),wav:e=>y(e,[82,73,70,70])&&y(e,[87,65,86,69],8),ogg:e=>y(e,[79,103,103,83]),woff:e=>y(e,[119,79,70,70]),woff2:e=>y(e,[119,79,70,50])};function re(e,t){let n=S(e);if(!n)return N(e);let s=v(e);for(let[o,u]of Ne)if((!n.text||u[0]===35)&&y(t,u))return{path:e,code:"executable",message:`This file is ${o}, which a game cannot carry.`};if(n.text){if(t.includes(0))return{path:e,code:"not_text",message:"A text file must not contain NUL bytes."};try{new TextDecoder("utf-8",{fatal:!0}).decode(t)}catch{return{path:e,code:"not_text",message:"A text file must be UTF-8."}}return null}let a=De[s];return a&&!a(t)?{path:e,code:"type_mismatch",message:`This file is not really a .${s} file.`}:null}function Fe(e){let t=[],n=(d,r,p)=>t.push({path:d,code:r,message:p}),s=e;if(!s||typeof s!="object"||Array.isArray(s))return[{path:"",code:"bad_game",message:"Not a game manifest."}];let a=new Set(["format","id","entry","files","size"]);for(let d of Object.keys(s))a.has(d)||n(d,"bad_game",`Unknown field "${d}".`);if(s.format!==ae&&n("format","bad_format",`Unknown game format ${String(s.format)}.`),(typeof s.id!="string"||!$.test(s.id))&&n("id","bad_game_id","A game id is 3 to 30 lowercase letters, digits and hyphens."),s.entry!==x&&n("entry","bad_entry",`A game starts at ${x}.`),!Array.isArray(s.files))return[...t,{path:"files",code:"bad_game",message:"A game lists its files."}];s.files.length>A.files&&n("files","too_many_files",`A game has at most ${A.files} files; this one has ${s.files.length}.`);let o=0,u="";for(let[d,r]of s.files.entries()){let p=`files[${d}]`;if(!r||typeof r!="object"||Object.keys(r).sort().join()!=="path,sha256,size,type"){n(p,"bad_game","A file is { path, size, sha256, type }.");continue}let i=N(r.path);i?n(r.path||p,i.code,i.message):r.type!==S(r.path).type&&n(r.path,"bad_type",`A .${v(r.path)} file is served as ${S(r.path).type}.`),typeof r.path=="string"&&r.path<=u&&n(r.path,"not_sorted","Files are sorted by path, each once."),typeof r.path=="string"&&(u=r.path),!Number.isSafeInteger(r.size)||r.size<0?n(r.path||p,"bad_size","A size is a whole number of bytes."):r.size>A.fileBytes?n(r.path,"file_too_big",`A file is at most ${A.fileBytes} bytes; this one is ${r.size}.`):o+=r.size,(typeof r.sha256!="string"||!Oe.test(r.sha256))&&n(r.path||p,"bad_sha256","sha256 is 64 lowercase hex characters.")}return o>A.totalBytes&&n("files","game_too_big",`A game is at most ${A.totalBytes} bytes; this one is ${o}.`),s.size!==o&&n("size","bad_size","size is the sum of the file sizes."),s.files.some(d=>d?.path===x)||n(x,"entry_missing",`A game needs ${x} at the top of its folder.`),t}var We=e=>R(M(e));async function Bt(e,t){return t.byteLength!==e.size?{path:e.path,code:"size_mismatch",message:`The manifest says ${e.size} bytes; the file has ${t.byteLength}.`}:await R(t)!==e.sha256?{path:e.path,code:"sha256_mismatch",message:"The file does not match its sha256."}:re(e.path,t)}async function zt(e,t){let n=[],s=[...t].sort((r,p)=>r.path<p.path?-1:r.path>p.path?1:0),a=[];for(let r of s){let p=N(r.path)??re(r.path,r.bytes);p&&n.push(p),a.push({path:r.path,size:r.bytes.byteLength,sha256:await R(r.bytes),type:S(r.path)?.type??"application/octet-stream"})}let o={format:ae,id:e,entry:x,files:a,size:a.reduce((r,p)=>r+p.size,0)},u=new Set(n.map(r=>`${r.path}:${r.code}`));for(let r of Fe(o))!u.has(`${r.path}:${r.code}`)&&r.code!=="bad_type"&&n.push(r);let d=se(s.filter(r=>!n.some(p=>p.path===r.path)));return n.push(...d.issues),{manifest:o,hash:await We(o),issues:n,assets:d}}async function Rt(e,t){return`${t}-${(await R(e.toLowerCase())).slice(0,16)}`}var Ut=e=>`https://${e}.${Ge}`,Qe=/\b(?:https?|wss?):\/\/[^\s"'`<>()\\]+/gi,Ke=/^(?:https?:\/\/www\.w3\.org\/|https:\/\/storeconnect\.seekprotocol\.ai(?:$|\/)|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?(?:$|\/))/i,Je=/\s(src|href|action|formaction|poster|data|srcset|background|xlink:href|manifest)\s*=\s*["']?\s*((?:(?:https?|wss?):)?\/\/[^"'\s>]+)/gi,He=/(?:url\(\s*["']?|@import\s+["'])\s*((?:(?:https?):)?\/\/[^"')\s]+)/gi,Ve=[["eval",/\beval\s*\(/g,"eval() runs text as code."],["new_function",/\bnew\s+Function\s*\(/g,"new Function() runs text as code."],["string_timer",/\bset(?:Timeout|Interval)\s*\(\s*["'`]/g,"A timer with a string runs text as code."],["document_write",/\bdocument\.write(?:ln)?\s*\(/g,"document.write() writes markup at run time."],["window_open",/\bwindow\.open\s*\(/g,"Opening windows is not allowed."],["navigation",/\b(?:(?:top|parent|window|document)\.location|location\.href)\s*=(?!=)/g,"A game does not navigate away."],["javascript_url",/javascript\s*:/gi,"javascript: URLs run code."]],Xe=/<[a-z][^>]*?\son[a-z]+\s*=/gi,Ye=/<script\b(?![^>]*\bsrc\s*=)[^>]*>\s*[^<\s]/gi,Ze=/<meta[^>]+http-equiv\s*=\s*["']?refresh/gi,qe=/<base\b/gi,j=e=>e===ie||e.startsWith(`${ie}/`);function Ct(e,t,n=20){let s=S(e);if(!s?.text||s.kind==="data")return[];let a=[],o=[];for(let i=t.indexOf(`
|
|
4
|
-
`);
|
|
5
|
-
`,
|
|
1
|
+
var ne=1,se=1,U=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/,ie=["location.markers","spawns.place","catch","steps","arena","rewards.cards","rewards.cosmetics","rewards.boosts","rewards.crypto","gamekit","location.beacon","rewards.outcomes","ar.models","map.marker"],H={places:["public","private_with_permission"],effort:["light","moderate","hard"],ai:["none","assisted","generated"]};function wt(e){return{en:e}}function kt(e){return e}var _t={anywhere(){return{kind:"anywhere"}},circle(e,n,t){return{kind:"circle",name:e,center:n,radiusM:t}},city(e,n,t=10){return{kind:"circle",name:e,center:n,radiusM:Math.round(t*1e3)}},place(e,n){return{name:e,at:n}}};function At(e,n){return{game:e,file:n}}function St(e,n){return n?{image:e,model:n}:{image:e}}function $t(e,n,t=1){return{id:e,title:n,weight:t}}function vt(e,n,t){return{id:e,title:n,minScore:t}}var Tt={talk(e){return{kind:"talk",...e}},arrive(e){return{kind:"arrive",radiusM:50,...e}},beacon(e){return{kind:"beacon",minM:150,maxM:300,radiusM:25,...e}},catch(e){return{kind:"catch",coin:"any",...e}},walk(e){return{kind:"walk",verified:!1,...e}},arena(e){return{kind:"arena",wins:1,...e}},choice(e){return{kind:"choice",...e}},play(e){return{kind:"play",...e}}},Mt={xp(e){return{kind:"xp",amount:e}},card(e){return{kind:"card",card:e}},cosmetic(e){return{kind:"cosmetic",item:e}},boost(e,n=1){return{kind:"boost",boost:e,count:n}},pool(e,n,t){return{kind:"pool",coin:e,perPlayer:n,maxPlayers:t}},draw(e,n,t){return{kind:"draw",coins:e,perPlayer:n,maxPlayers:t}},when(e,n){return{...n,when:e}}};var h={steps:{min:1,max:30},idPattern:/^[a-z0-9][a-z0-9-]{2,39}$/,stepIdPattern:/^[a-z0-9][a-z0-9-]{0,39}$/,title:60,summary:160,story:600,areaRadiusM:{min:100,max:5e4},arriveRadiusM:{min:25,max:500},catchCount:{min:1,max:50},catchWithinM:{min:50,max:2e3},walkSteps:{min:100,max:5e4},arenaWins:{min:1,max:20},choiceOptions:{min:2,max:4},xp:{max:5e3},boostCount:{max:3},rewards:{max:10},beaconM:{min:50,max:2e3},beaconRadiusM:{min:10,max:100},outcomes:{max:3},outcomeIdPattern:/^[a-z0-9][a-z0-9-]{0,29}$/,variants:{min:2,max:6},variantWeight:{min:1,max:100},drawCoins:{min:2,max:10},outcomeBonusShare:.5,variantBonusShare:1,windowDays:{max:92}},_e={talk:["location.markers"],arrive:["location.markers"],beacon:["location.beacon"],catch:["catch"],walk:["steps"],arena:["arena"],choice:[],play:["gamekit"]};function Ae(e,n){let s=(n.lat-e.lat)*Math.PI/180,a=(n.lng-e.lng)*Math.PI/180,o=Math.sin(s/2)**2+Math.cos(e.lat*Math.PI/180)*Math.cos(n.lat*Math.PI/180)*Math.sin(a/2)**2;return 2*6371e3*Math.asin(Math.min(1,Math.sqrt(o)))}var Q=e=>!!e&&Number.isFinite(e.lat)&&Number.isFinite(e.lng)&&Math.abs(e.lat)<=85&&Math.abs(e.lng)<=180;function Se(e){let n=new Set;for(let t of e.steps){for(let s of _e[t.kind]??[])n.add(s);t.kind==="catch"&&t.near&&n.add("spawns.place"),(t.kind==="talk"||t.kind==="beacon")&&t.model&&n.add("ar.models")}for(let t of e.rewards)t.kind==="card"&&n.add("rewards.cards"),t.kind==="cosmetic"&&n.add("rewards.cosmetics"),t.kind==="boost"&&n.add("rewards.boosts"),(t.kind==="pool"||t.kind==="draw")&&n.add("rewards.crypto"),t.when&&n.add("rewards.outcomes");return e.variants?.length&&n.add("rewards.outcomes"),e.marker&&n.add("map.marker"),n}function ae(e){let n=[];return(e.steps??[]).forEach((t,s)=>{(t.kind==="talk"||t.kind==="beacon")&&t.model&&n.push({path:`steps[${s}].model`,asset:t.model,kind:"model"})}),e.marker?.image&&n.push({path:"marker.image",asset:e.marker.image,kind:"image"}),e.marker?.model&&n.push({path:"marker.model",asset:e.marker.model,kind:"model"}),n}function V(e){return[...new Set([...(e.steps??[]).flatMap(n=>n.kind==="play"?[n.game]:[]),...ae(e).map(n=>n.asset.game)])]}var D=e=>e.kind==="pool"||e.kind==="draw",J=e=>e.kind==="pool"?e.coin:e.kind==="draw"?`draw:${[...e.coins].sort().join(",")}`:"",$e=/^(?:[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}\/){0,3}[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;function F(e,n){let t=e[n];return t.kind==="choice"?t.options.map(s=>s.next):t.next!==void 0?[t.next]:[e[n+1]?.id??null]}function re(e){let n=[],t=(i,c,d)=>n.push({path:i,code:c,message:d}),s=(i,c,d,g)=>{(!Number.isFinite(c)||c<(d.min??0)||c>d.max)&&t(i,"out_of_range",`${g} must be between ${d.min??0} and ${d.max}.`)},a=(i,c,d,g)=>{if(!c||typeof c.en!="string"||!c.en.trim()){g&&t(i,"text_missing","Text is required.");return}c.en.length>d&&t(i,"text_too_long",`At most ${d} characters (${c.en.length}).`)};h.idPattern.test(e.id??"")||t("id","bad_id","Use 3 to 40 lowercase letters, digits and hyphens."),a("title",e.title,h.title,!0),a("summary",e.summary,h.summary,!1),e.area?.kind==="circle"?(Q(e.area.center)||t("area.center","bad_point","Not a valid coordinate."),s("area.radiusM",e.area.radiusM,h.areaRadiusM,"The area radius")):e.area?.kind!=="anywhere"&&t("area","bad_area","Use zone.anywhere(), zone.city() or zone.circle().");let o=(i,c)=>{if(!Q(c))return t(i,"bad_point","Not a valid coordinate.");e.area?.kind==="circle"&&Q(e.area.center)&&Ae(e.area.center,c)>e.area.radiusM&&t(i,"outside_area",`This place is outside the quest area (${e.area.name}).`)};if(e.window){let i=Date.parse(e.window.from),c=Date.parse(e.window.to);!Number.isFinite(i)||!Number.isFinite(c)||c<=i?t("window","bad_window","The window needs a start before its end."):c-i>h.windowDays.max*864e5&&t("window","window_too_long",`A window lasts at most ${h.windowDays.max} days.`)}for(let[i,c]of(e.capabilities??[]).entries())ie.includes(c)||t(`capabilities[${i}]`,"unknown_capability",`Unknown capability "${c}".`);for(let i of Se(e))e.capabilities?.includes(i)||t("capabilities","capability_missing",`The quest uses "${i}" but does not declare it.`);let l=e.steps??[];(l.length<h.steps.min||l.length>h.steps.max)&&t("steps","step_count",`A quest has ${h.steps.min} to ${h.steps.max} steps.`);let u=new Set;if(l.forEach((i,c)=>{let d=`steps[${c}]`;switch(h.stepIdPattern.test(i.id??"")||t(`${d}.id`,"bad_id","Use lowercase letters, digits and hyphens."),u.has(i.id)&&t(`${d}.id`,"duplicate_id",`Step id "${i.id}" is used twice.`),u.add(i.id),a(`${d}.title`,i.title,h.title,!0),a(`${d}.story`,i.story,h.story,!1),i.kind){case"talk":i.giver||t(`${d}.giver`,"giver_missing","Name the quest-giver."),i.at?o(`${d}.at`,i.at.at):e.area?.kind!=="circle"&&t(`${d}.at`,"place_missing","A quest playable anywhere needs a place for its giver.");break;case"beacon":s(`${d}.minM`,i.minM,h.beaconM,"The nearest a beacon is placed, in metres,"),s(`${d}.maxM`,i.maxM,h.beaconM,"The farthest a beacon is placed, in metres,"),i.minM>=i.maxM?t(`${d}.maxM`,"bad_beacon","maxM is more than minM: Seek needs room to pick a walkable spot."):i.maxM-i.minM<50&&t(`${d}.maxM`,"bad_beacon","Leave at least 50 metres between minM and maxM, so Seek can find a walkable spot."),s(`${d}.radiusM`,i.radiusM,h.beaconRadiusM,"The beacon radius");break;case"arrive":o(`${d}.at`,i.at?.at),s(`${d}.radiusM`,i.radiusM,h.arriveRadiusM,"The arrival radius");break;case"catch":s(`${d}.count`,i.count,h.catchCount,"The number of catches"),i.near&&(o(`${d}.near`,i.near.at),s(`${d}.withinM`,i.withinM??500,h.catchWithinM,"The catch radius"));break;case"walk":s(`${d}.steps`,i.steps,h.walkSteps,"The number of steps");break;case"arena":s(`${d}.wins`,i.wins,h.arenaWins,"The number of wins");break;case"choice":{let g=i.options?.length??0;(g<h.choiceOptions.min||g>h.choiceOptions.max)&&t(`${d}.options`,"option_count","A choice has 2 to 4 options.");let b=new Set;i.options?.forEach((y,_)=>{b.has(y.id)&&t(`${d}.options[${_}].id`,"duplicate_id",`Option "${y.id}" is used twice.`),b.add(y.id),a(`${d}.options[${_}].label`,y.label,40,!0)});break}case"play":if(i.game?U.test(i.game)||t(`${d}.game`,"bad_game_id","A game id is its folder under games/: 3 to 30 lowercase letters, digits and hyphens."):t(`${d}.game`,"game_missing","Name the minigame."),(!(i.maxScore>0)||!(i.minScore>0)||i.minScore>i.maxScore)&&t(`${d}.minScore`,"bad_score","Scores are positive and minScore is at most maxScore."),i.outcomes!==void 0){(!Array.isArray(i.outcomes)||i.outcomes.length>h.outcomes.max)&&t(`${d}.outcomes`,"outcome_count",`A play step has at most ${h.outcomes.max} outcomes.`);let g=i.minScore,b=new Set;(Array.isArray(i.outcomes)?i.outcomes:[]).forEach((y,_)=>{let R=`${d}.outcomes[${_}]`;h.outcomeIdPattern.test(y?.id??"")||t(`${R}.id`,"bad_id","Use lowercase letters, digits and hyphens."),b.has(y?.id)&&t(`${R}.id`,"duplicate_id",`Outcome "${y.id}" is used twice.`),b.add(y?.id),a(`${R}.title`,y?.title,40,!0),!(y?.minScore>g)||y.minScore>i.maxScore?t(`${R}.minScore`,"bad_outcome_score","Each outcome asks for more than the pass mark and the outcome before it, and at most maxScore."):g=y.minScore})}break;default:t(`${d}.kind`,"unknown_step",`Unknown step kind "${i.kind}".`)}}),l.forEach((i,c)=>{F(l,c).forEach(d=>{d!==null&&!u.has(d)&&t(`steps[${c}].next`,"unknown_next",`No step "${d}".`)})}),l.length&&!n.some(i=>i.code==="unknown_next"||i.code==="duplicate_id")){let i=new Map(l.map((b,y)=>[b.id,y])),c=new Map,d=!1,g=b=>{if(c.get(b)!=="done"){if(c.get(b)==="open"){d=!0;return}c.set(b,"open");for(let y of F(l,i.get(b)))y!==null&&g(y);c.set(b,"done")}};g(l[0].id),d&&t("steps","loop","Steps lead back to an earlier step. A quest has to end."),l.forEach((b,y)=>{c.has(b.id)||t(`steps[${y}]`,"unreachable",`Step "${b.id}" can never be reached.`)})}let r=new Set;if(e.variants!==void 0){let i=Array.isArray(e.variants)?e.variants.length:0;(i<h.variants.min||i>h.variants.max)&&t("variants","variant_count",`A quest has ${h.variants.min} to ${h.variants.max} variants, or none.`),(Array.isArray(e.variants)?e.variants:[]).forEach((c,d)=>{h.outcomeIdPattern.test(c?.id??"")||t(`variants[${d}].id`,"bad_id","Use lowercase letters, digits and hyphens."),r.has(c?.id)&&t(`variants[${d}].id`,"duplicate_id",`Variant "${c.id}" is used twice.`),r.add(c?.id),a(`variants[${d}].title`,c?.title,40,!0),(!Number.isInteger(c?.weight)||c.weight<h.variantWeight.min||c.weight>h.variantWeight.max)&&t(`variants[${d}].weight`,"bad_weight",`A weight is a whole number from ${h.variantWeight.min} to ${h.variantWeight.max}.`)})}for(let i of ae(e))(!i.asset||typeof i.asset.game!="string"||!U.test(i.asset.game))&&t(`${i.path}.game`,"bad_game_id","Name the game folder the file is in, with asset(game, file)."),!i.asset||typeof i.asset.file!="string"||!$e.test(i.asset.file)?t(`${i.path}.file`,"bad_asset","A file path inside the game folder, such as models/pepe.glb."):i.kind==="model"&&!/\.glb$/i.test(i.asset.file)?t(`${i.path}.file`,"bad_asset","A model the app shows is one .glb file."):i.kind==="image"&&!/\.(?:png|webp)$/i.test(i.asset.file)&&t(`${i.path}.file`,"bad_asset","A marker image is a PNG or WebP, with transparency.");let p=e.rewards??[];p.length>h.rewards.max&&t("rewards","reward_count",`At most ${h.rewards.max} rewards.`);let m=new Map(l.map(i=>[i.id,i]));p.forEach((i,c)=>{let d=`rewards[${c}]`;if(i.kind==="xp"&&s(`${d}.amount`,i.amount,{min:1,max:h.xp.max},"XP"),i.kind==="boost"&&s(`${d}.count`,i.count,{min:1,max:h.boostCount.max},"The boost count"),D(i)&&((!(i.perPlayer>0)||!(i.maxPlayers>0)||!Number.isInteger(i.maxPlayers))&&t(d,"bad_pool","A pool pays a positive amount to a whole number of players."),(e.minAge??18)<18&&t("minAge","pool_needs_adults","A quest that pays coins is for players of 18 and over.")),i.kind==="draw"){let b=Array.isArray(i.coins)?i.coins.length:0;(b<h.drawCoins.min||b>h.drawCoins.max||new Set(i.coins).size!==b||i.coins.some(y=>typeof y!="string"||!y))&&t(`${d}.coins`,"bad_draw",`A draw is between ${h.drawCoins.min} and ${h.drawCoins.max} different coins.`),i.when&&t(`${d}.when`,"conditional_draw","A draw is paid to everyone who finishes; make a pool conditional instead.")}let g=i.when;if(g!==void 0)if("step"in g){let b=m.get(g.step);!b||b.kind!=="play"?t(`${d}.when.step`,"bad_condition",`"${g.step}" is not a play step of this quest.`):b.outcomes?.some(y=>y.id===g.outcome)||t(`${d}.when.outcome`,"bad_condition",`The play step "${g.step}" has no outcome "${g.outcome}".`)}else"variant"in g?r.has(g.variant)||t(`${d}.when.variant`,"bad_condition",`The quest has no variant "${g.variant}".`):t(`${d}.when`,"bad_condition","A condition names a play step and its outcome, or a variant.")});let w=new Map;for(let i of p)D(i)&&!i.when&&w.set(J(i),i);let f=new Map;p.forEach((i,c)=>{if(!D(i)||!i.when)return;let d=w.get(J(i));if(!d){t(`rewards[${c}]`,"bonus_without_base","Coins on a condition come on top of the same coin paid to everyone who finishes; add that first.");return}if(i.maxPlayers!==d.maxPlayers&&t(`rewards[${c}].maxPlayers`,"bonus_players",`A bonus pays the same players as the pool it tops up: maxPlayers ${d.maxPlayers}.`),"variant"in i.when)i.perPlayer>d.perPlayer*h.variantBonusShare&&t(`rewards[${c}].perPlayer`,"bonus_too_big",`A variant adds at most ${h.variantBonusShare*100}% of the base (${d.perPlayer*h.variantBonusShare}).`);else{let g=J(i);f.set(g,(f.get(g)??0)+i.perPlayer),(f.get(g)??0)>d.perPlayer*h.outcomeBonusShare&&t(`rewards[${c}].perPlayer`,"bonus_too_big",`Outcomes add at most ${h.outcomeBonusShare*100}% of the base in coins (${d.perPlayer*h.outcomeBonusShare} ${i.kind==="pool"?i.coin:""}). A game reports its own score; XP and cosmetics can reward a perfect run without limit.`)}}),p.some(D)&&e.repeat&&e.repeat!=="never"&&t("repeat","pool_repeats","A quest that pays coins cannot be repeated.");let x=e.declarations;if(!x||typeof x!="object")t("declarations","declarations_missing","Say what the quest asks of players: places, effort, hazards, AI, sponsoring and game events.");else{let i=d=>{H[d].includes(x[d])||t(`declarations.${d}`,"bad_declaration",`One of ${H[d].join(", ")}.`)};i("places"),i("effort"),i("ai");for(let d of["hazards","sponsored","gameEvents"])typeof x[d]!="boolean"&&t(`declarations.${d}`,"bad_declaration","true or false.");x.rights!==!0&&t("declarations.rights","rights_not_declared","Declare rights: true. Only use texts, images, sounds, fonts, characters, logos and code that are yours or that you have a licence for.");let c=(e.steps??[]).reduce((d,g)=>d+(g.kind==="walk"?g.steps:0),0);x.effort==="light"&&c>1e4&&t("declarations.effort","effort_understated",`The walk steps add up to ${c} steps; that is not light.`)}return n}var ve=/^(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})\.(0|[1-9]\d{0,3})$/,G=class extends Error{constructor(t){super(`The quest has ${t.length} problem(s):
|
|
2
|
+
${t.map(s=>` ${s.path}: ${s.message}`).join(`
|
|
3
|
+
`)}`);this.issues=t}},z={keys:300,chars:600,totalChars:15e3,key:/^[A-Za-z0-9_][A-Za-z0-9_.-]{0,59}$/};function Te(e,n){let t=`games.${e}.texts`;if(!n||typeof n!="object"||Array.isArray(n))return[{path:t,code:"bad_game_texts",message:"texts/en.json is one JSON object of key to English text."}];let s=[],a=Object.entries(n);a.length>z.keys&&s.push({path:t,code:"too_many_texts",message:`A game has at most ${z.keys} texts; this one has ${a.length}.`});let o=0;for(let[l,u]of a){if(z.key.test(l)||s.push({path:`${t}.${l}`,code:"bad_text_key",message:"A key is letters, digits, dots, hyphens and underscores, at most 60."}),typeof u!="string"||!u.trim()){s.push({path:`${t}.${l}`,code:"text_missing",message:"Every key needs its English text."});continue}u.length>z.chars&&s.push({path:`${t}.${l}`,code:"text_too_long",message:`At most ${z.chars} characters (${u.length}).`}),/[\u2013\u2014]/.test(u)&&s.push({path:`${t}.${l}`,code:"dash",message:"Use a comma, a colon or a new sentence instead of an em or en dash."}),o+=u.length}return o>z.totalChars&&s.push({path:t,code:"texts_too_long",message:`A game's texts are at most ${z.totalChars} characters together; these are ${o}.`}),s}function Me(e,n){if(n===void 0)return[];let t=[];if(!n||typeof n!="object"||Array.isArray(n))return[{path:"games",code:"bad_games",message:"games maps a game id to its hash."}];let s=new Set(V(e));for(let[a,o]of Object.entries(n))U.test(a)||t.push({path:`games.${a}`,code:"bad_game_id",message:"A game id is 3 to 30 lowercase letters, digits and hyphens."}),(typeof o!="string"||!/^[0-9a-f]{64}$/.test(o))&&t.push({path:`games.${a}`,code:"bad_game_hash",message:"A game hash is 64 lowercase hex characters."}),s.has(a)||t.push({path:`games.${a}`,code:"game_unused",message:`Nothing in the quest uses the game "${a}".`});for(let a of s)a in n||t.push({path:"games",code:"game_missing",message:`The game "${a}" is not in the quest. Put it in games/${a}/.`});return t}function oe(e,n){let t=re(e);ve.test(n.version)||t.push({path:"version",code:"bad_version",message:"Use a version like 1.2.0."}),(!Number.isInteger(n.build)||n.build<1)&&t.push({path:"build",code:"bad_build",message:"The build number is a whole number from 1."}),t.push(...Me(e,n.games));let s=new Set(V(e));for(let[u,r]of Object.entries(n.gameTexts??{}))s.has(u)||t.push({path:`games.${u}.texts`,code:"game_unused",message:`Nothing in the quest uses the game "${u}".`}),t.push(...Te(u,r));if(t.length)throw new G(t);let a={},o=(u,r)=>{if(r)return a[u]=r.en.trim(),u};for(let[u,r]of Object.entries(n.gameTexts??{}).sort(([p],[m])=>p<m?-1:1))for(let[p,m]of Object.entries(r))o(`games.${u}.${p}`,{en:m});let l=e.steps.map((u,r)=>{let p=`steps.${u.id}`,[m]=F(e.steps,r),{title:w,story:f,next:x,...i}=u,c={...i,title:o(`${p}.title`,w),...f?{story:o(`${p}.story`,f)}:{},next:u.kind==="choice"?null:m??null};return u.kind==="play"&&u.outcomes?.length?c.outcomes=u.outcomes.map(d=>({id:d.id,title:o(`${p}.outcomes.${d.id}`,d.title),minScore:d.minScore})):u.kind==="play"&&delete c.outcomes,u.kind==="choice"&&(c.options=u.options.map(d=>({id:d.id,label:o(`${p}.options.${d.id}`,d.label),next:d.next}))),c});return{manifest:{format:1,runtime:1,id:e.id,version:n.version,build:n.build,title:o("title",e.title),...e.summary?{summary:o("summary",e.summary)}:{},area:e.area,...e.window?{window:e.window}:{},repeat:e.repeat??"never",...e.variants?.length?{variants:e.variants.map(u=>({id:u.id,title:o(`variants.${u.id}`,u.title),weight:u.weight}))}:{},...e.marker?{marker:e.marker}:{},minAge:e.minAge??18,declarations:{...e.declarations},capabilities:[...e.capabilities].sort(),rewards:e.rewards,start:e.steps[0].id,...n.games&&Object.keys(n.games).length?{games:{...n.games}}:{}},steps:l,texts:{en:a}}}function L(e){if(e===null||typeof e!="object"){if(typeof e=="number"&&!Number.isFinite(e))throw new Error("Not JSON: a number that is not finite.");return JSON.stringify(e)}return Array.isArray(e)?`[${e.map(L).join(",")}]`:`{${Object.entries(e).filter(([,t])=>t!==void 0).sort(([t],[s])=>t<s?-1:t>s?1:0).map(([t,s])=>`${JSON.stringify(t)}:${L(s)}`).join(",")}}`}var Pe=e=>[...new Uint8Array(e)].map(n=>n.toString(16).padStart(2,"0")).join("");async function X(e){let n=new TextEncoder().encode(L(e));return Pe(await crypto.subtle.digest("SHA-256",n))}var de=(e,n)=>`seek-bundle-v1:${n}:${e}`,Ee=e=>btoa(String.fromCharCode(...e)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),ce=e=>{let n=atob(e.replace(/-/g,"+").replace(/_/g,"/")+"=".repeat((4-e.length%4)%4));return Uint8Array.from(n,t=>t.charCodeAt(0))};async function zt(){let e=await crypto.subtle.generateKey({name:"Ed25519"},!0,["sign","verify"]),n=await crypto.subtle.exportKey("jwk",e.publicKey),t=await crypto.subtle.exportKey("jwk",e.privateKey);return{keyId:await Re(n),publicJwk:n,privateJwk:t}}async function Re(e){let n=await crypto.subtle.digest("SHA-256",ce(e.x));return[...new Uint8Array(n)].slice(0,8).map(t=>t.toString(16).padStart(2,"0")).join("")}async function Ot(e,n,t,s){let a=await crypto.subtle.importKey("jwk",s,{name:"Ed25519"},!1,["sign"]),o=await crypto.subtle.sign({name:"Ed25519"},a,new TextEncoder().encode(de(e,n)));return{by:n,keyId:t,sig:Ee(new Uint8Array(o))}}async function Ie(e,n,t){try{let s=await crypto.subtle.importKey("jwk",t,{name:"Ed25519"},!1,["verify"]);return await crypto.subtle.verify({name:"Ed25519"},s,ce(n.sig),new TextEncoder().encode(de(e,n.by)))}catch{return!1}}async function Gt(e,n,t){return n.by!=="seek"||!Object.hasOwn(t,n.keyId)?!1:Ie(e,n,t[n.keyId])}var Be="0".repeat(64);async function Ue(e){let n=["seek-log-v1",e.seq,e.prev_entry,e.event,e.quest_id,e.version_id,e.hash,e.seek_key_id,e.seek_sig].join(":"),t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(n));return[...new Uint8Array(t)].map(s=>s.toString(16).padStart(2,"0")).join("")}async function Ct(e,n={size:0,head:Be}){let{size:t,head:s}=n;for(let a of e){if(a.seq!==t+1)return{ok:!1,seq:a.seq,reason:"bad_sequence"};if(a.prev_entry!==s)return{ok:!1,seq:a.seq,reason:"bad_link"};if(await Ue(a)!==a.entry_hash)return{ok:!1,seq:a.seq,reason:"bad_hash"};t=a.seq,s=a.entry_hash}return{ok:!0,size:t,head:s}}function ze(e){let n=e.texts?.en??{},t=o=>o===void 0?void 0:{en:n[o]??""},s=e.manifest,a=e.steps.map(o=>{let{title:l,story:u,next:r,...p}=o,m={...p,title:t(l),...u?{story:t(u)}:{},next:r};return m.kind==="choice"&&o.kind==="choice"&&(m.options=o.options.map(w=>({id:w.id,label:t(w.label),next:w.next}))),m});return{id:s.id,title:t(s.title),...s.summary?{summary:t(s.summary)}:{},area:s.area,...s.window?{window:s.window}:{},repeat:s.repeat,minAge:s.minAge,declarations:s.declarations,capabilities:s.capabilities,steps:a,rewards:s.rewards}}async function le(e,n){let t=(r,p)=>({ok:!1,issues:[{path:"",code:r,message:p}]}),s=e;if(!s||typeof s!="object"||!s.manifest||!Array.isArray(s.steps)||!s.texts?.en)return t("bad_bundle","Not a quest bundle.");if(s.manifest.format!==1)return t("bad_format",`Unknown bundle format ${String(s.manifest.format)}.`);let a,o;try{a=ze(s),o=oe(a,{version:s.manifest.version,build:s.manifest.build,...s.manifest.games!==void 0?{games:s.manifest.games}:{}})}catch(r){return r instanceof G?{ok:!1,issues:r.issues}:t("bad_bundle","The bundle could not be read.")}let[l,u]=await Promise.all([X(s),X(o)]);return l!==u?t("not_compiled","The bundle is not what seek build makes. Build it again with the CLI."):n!==void 0&&n!==l?t("hash_mismatch","The bundle does not match its hash."):{ok:!0,hash:l,definition:a}}var Oe=".seek",Z={fileBytes:64*1024*1024,unpackedBytes:128*1024*1024},v=class extends Error{},Kt=e=>`${e.manifest.id}-${e.manifest.version}-${e.manifest.build}${Oe}`;async function me(e,n){let t=e.getReader(),s=[],a=0;for(;;){let{done:u,value:r}=await t.read();if(u)break;if(a+=r.byteLength,a>n)throw await t.cancel(),new v("This .seek file unpacks to more than a build can be.");s.push(r)}let o=new Uint8Array(a),l=0;for(let u of s)o.set(u,l),l+=u.byteLength;return o}var ue=e=>new Blob([e]).stream();async function Wt(e){let n=new TextEncoder().encode(JSON.stringify({format:1,bundle:e.bundle,hash:e.hash,signature:e.signature,games:e.games})),t=ue(n).pipeThrough(new CompressionStream("gzip"));return await me(t,Z.fileBytes)}async function Ht(e){if(e.byteLength>Z.fileBytes)throw new v("This .seek file is too big.");if(e[0]!==31||e[1]!==139)throw new v("This is not a .seek file. Make one with seek build.");let n;try{let o=ue(e).pipeThrough(new DecompressionStream("gzip"));n=JSON.parse(new TextDecoder().decode(await me(o,Z.unpackedBytes)))}catch(o){throw o instanceof v?o:new v("This .seek file is damaged. Build it again.")}if(!n||n.format!==1)throw new v("This .seek file was made by another version of seek. Build it again.");let t=await le(n.bundle,n.hash);if(!t.ok)throw new v(`This .seek file is damaged: ${t.issues[0].message}`);let s=n.bundle.manifest.games??{},a=n.games??{};for(let[o,l]of Object.entries(s))if(!Object.hasOwn(a,o)||a[o].hash!==l)throw new v(`This .seek file is damaged: the game "${o}" is not the one the quest names.`);if(n.signature&&(n.signature.by!=="maker"||typeof n.signature.keyId!="string"||typeof n.signature.sig!="string"))throw new v("This .seek file has a signature that is not a maker's.");return{format:1,bundle:n.bundle,hash:n.hash,signature:n.signature??null,games:a}}var P={trianglesMax:5e5,trianglesAdvise:1e5,gameTrianglesMax:15e5,gameTrianglesAdvise:3e5,textureShareAdvise:.5},Ge=new Set(["KHR_materials_emissive_strength","KHR_materials_clearcoat","KHR_materials_ior","KHR_materials_iridescence","KHR_materials_sheen","KHR_materials_specular","KHR_materials_transmission","KHR_materials_unlit","KHR_materials_volume","KHR_materials_anisotropy","KHR_texture_transform","KHR_mesh_quantization","KHR_lights_punctual","EXT_texture_webp","EXT_mesh_gpu_instancing","KHR_animation_pointer"]),Y={KHR_draco_mesh_compression:"Draco",EXT_meshopt_compression:"Meshopt",KHR_meshopt_compression:"Meshopt",KHR_texture_basisu:"Basis Universal (KTX2)"},B=(e,n)=>(e[n]|e[n+1]<<8|e[n+2]<<16|e[n+3]<<24)>>>0;function fe(e){if(e.length<20||B(e,0)!==1179937895)return{problem:'This is not a .glb file: it does not start with "glTF".'};if(B(e,4)!==2)return{problem:`This .glb is glTF version ${B(e,4)}; Seek reads version 2.`};if(B(e,8)!==e.length)return{problem:"This .glb says it is a different size than it is: the file is cut off or broken."};let n=B(e,12);if(B(e,16)!==1313821514||20+n>e.length)return{problem:"The first chunk of this .glb is not its JSON."};let t=new TextDecoder().decode(e.subarray(20,20+n)),s;try{s=JSON.parse(t)}catch{return{problem:"The JSON inside this .glb does not parse."}}let a=null,o=20+n;if(o+8<=e.length&&B(e,o+4)===5130562){let l=B(e,o);if(o+8+l>e.length)return{problem:"The binary chunk of this .glb is cut off."};a=e.subarray(o+8,o+8+l)}return{json:s,bin:a,jsonText:t}}var Le=new Set([4,5,6]);function pe(e,n,t){let s={path:e,triangles:0,meshes:0,animations:0,textures:[],needs:[],extensions:[],problems:[]},a=(f,x)=>s.problems.push({code:f,message:x}),o=E(e);if(o==="bin")return s;let l,u=null;if(o==="glb"){let f=fe(n);if("problem"in f)return a("model_unreadable",f.problem),s;({json:l,bin:u}=f)}else try{l=JSON.parse(new TextDecoder().decode(n))}catch{return a("model_unreadable","This .gltf is not valid JSON."),s}l.asset?.version?.startsWith("2.")||a("model_version","Seek reads glTF 2.0 models; export again as glTF 2.0."),s.extensions=[...new Set([...l.extensionsUsed??[],...l.extensionsRequired??[]])].sort();for(let f of s.extensions)Y[f]&&a("model_compression",`${Y[f]} compression (${f}) needs a WebAssembly decoder, which a game cannot run. Export without it; seek optimize makes the model smaller in a way three.js reads as it is.`);for(let f of l.extensionsRequired??[])!Y[f]&&!Ge.has(f)&&a("model_extension",`The model requires the extension ${f}, which three.js does not load by itself.`);let r=e.includes("/")?e.slice(0,e.lastIndexOf("/")+1):"",p=f=>{if(f.startsWith("data:"))return null;if(/^[a-z][a-z0-9+.-]*:|^\/\//i.test(f))return a("model_remote",`The model loads "${f.slice(0,80)}" from outside the game. Everything a game uses ships inside it.`),null;let x=r+decodeURIComponent(f.split(/[?#]/)[0]);return s.needs.push(x),t&&!t.has(x)&&a("model_missing_file",`The model needs ${x}, which is not in the game.`),x};for(let f of l.buffers??[])f.uri&&p(f.uri);let m=l.accessors??[];for(let f of l.meshes??[]){s.meshes++;for(let x of f.primitives??[]){let i=x.mode??4;if(!Le.has(i))continue;let c=x.indices!==void 0?m[x.indices]?.count??0:m[x.attributes?.POSITION??-1]?.count??0;s.triangles+=i===4?Math.floor(c/3):Math.max(0,c-2)}}s.animations=l.animations?.length??0;let w=l.bufferViews??[];for(let[f,x]of(l.images??[]).entries()){let i,c=`${e}#${f}`,d=x.mimeType??"";if(x.bufferView!==void 0){let y=w[x.bufferView],_=y&&y.buffer===0&&u?u:void 0;y&&_&&(i=_.subarray(y.byteOffset??0,(y.byteOffset??0)+(y.byteLength??0)))}else if(x.uri&&!x.uri.startsWith("data:")){let y=p(x.uri);y&&(c=y,i=t?.get(y))}if(!i)continue;let g=d.includes("png")||i[0]===137?"png":d.includes("jpeg")||i[0]===255?"jpg":d.includes("webp")||i[0]===82?"webp":d.includes("ktx2")?"ktx2":"",b=g&&g!=="ktx2"?K(`t.${g}`,i):null;b&&s.textures.push({ref:c,mimeType:d||`image/${g==="jpg"?"jpeg":g}`,bytes:i.byteLength,width:b.width,height:b.height,decoded:b.width*b.height*4})}return s.triangles>P.trianglesMax&&a("model_too_detailed",`${s.triangles.toLocaleString("en")} triangles: a model has at most ${P.trianglesMax.toLocaleString("en")}. Phones draw it at a crawl; decimate it in Blender.`),s}async function Xt(e,n){let t=fe(e);if("problem"in t||!t.bin)return null;let s=JSON.parse(t.jsonText);if((s.buffers??[]).length!==1||s.buffers[0].uri)return null;let a=s.bufferViews??[];if(a.some(g=>(g.buffer??0)!==0))return null;let o=new Map,l=new Set,u=0;for(let[g,b]of(s.images??[]).entries()){if(b.bufferView===void 0)continue;let y=a[b.bufferView],_=t.bin.subarray(y.byteOffset??0,(y.byteOffset??0)+(y.byteLength??0)),R=await n(_,b.mimeType??"");R&&(o.set(b.bufferView,R.bytes),b.mimeType=R.mimeType,R.mimeType==="image/webp"&&l.add(g),u++)}if(!u)return null;let r=[],p=0;for(let[g,b]of a.entries()){let y=o.get(g)??t.bin.subarray(b.byteOffset??0,(b.byteOffset??0)+(b.byteLength??0));b.byteOffset=p,b.byteLength=y.byteLength,r.push(y);let _=(4-y.byteLength%4)%4;_&&r.push(new Uint8Array(_)),p+=y.byteLength+_}if(s.buffers[0].byteLength=p,l.size){for(let g of s.textures??[])g.source!==void 0&&l.has(g.source)&&(g.extensions={...g.extensions,EXT_texture_webp:{source:g.source}},delete g.source);s.extensionsUsed=[...new Set([...s.extensionsUsed??[],"EXT_texture_webp"])],s.extensionsRequired=[...new Set([...s.extensionsRequired??[],"EXT_texture_webp"])]}let m=new TextEncoder().encode(JSON.stringify(s)),w=(4-m.byteLength%4)%4,f=m.byteLength+w,x=20+f+8+p,i=new Uint8Array(x),c=new DataView(i.buffer);c.setUint32(0,1179937895,!0),c.setUint32(4,2,!0),c.setUint32(8,x,!0),c.setUint32(12,f,!0),c.setUint32(16,1313821514,!0),i.set(m,20),i.fill(32,20+m.byteLength,20+f);let d=20+f;c.setUint32(d,p,!0),c.setUint32(d+4,5130562,!0),d+=8;for(let g of r)i.set(g,d),d+=g.byteLength;return{bytes:i,changed:u}}var Ce={imageMaxSide:4096,imageAdviseSide:2048,decodedMaxBytes:96*1024*1024,decodedAdviseBytes:48*1024*1024,startMaxBytes:8*1024*1024,startAdviseBytes:3*1024*1024,wavMaxBytes:2*1024*1024,wavAdviseBytes:256*1024,audioMaxSeconds:600,opaquePngAdviseBytes:300*1024,gifAdviseBytes:1024*1024},je={maxBytes:1024*1024,minWidth:1280,minHeight:720,maxWidth:3840,maxHeight:2160,ratio:16/9,ratioTolerance:.01,types:["image/png","image/jpeg","image/webp"]},q=(e,n)=>(e[n]<<8|e[n+1])>>>0,j=(e,n)=>(e[n]|e[n+1]<<8)>>>0,ge=(e,n)=>(e[n]|e[n+1]<<8|e[n+2]<<16)>>>0,T=(e,n)=>(e[n]<<24|e[n+1]<<16|e[n+2]<<8|e[n+3])>>>0,C=(e,n)=>(e[n]|e[n+1]<<8|e[n+2]<<16|e[n+3]<<24)>>>0,M=(e,n,t)=>String.fromCharCode(...e.subarray(n,n+t));function N(e,n,t=0,s=e.length){let a=[...n].map(o=>o.charCodeAt(0));e:for(let o=Math.max(0,t);o<=Math.min(e.length,s)-a.length;o++){for(let l=0;l<a.length;l++)if(e[o+l]!==a[l])continue e;return o}return-1}function Ne(e){if(e.length<33||M(e,12,4)!=="IHDR")return null;let n=e[25],t=N(e,"IDAT",33),s=N(e,"tRNS",33,t===-1?e.length:t);return{width:T(e,16),height:T(e,20),alpha:n===4||n===6||s!==-1}}function De(e){let n=2;for(;n+9<e.length;){if(e[n]!==255){n++;continue}let t=e[n+1];if(t===255){n++;continue}if(t===216||t===1||t>=208&&t<=215){n+=2;continue}if(t===217||t===218)return null;if(t>=192&&t<=207&&t!==196&&t!==200&&t!==204)return{height:q(e,n+5),width:q(e,n+7)};n+=2+q(e,n+2)}return null}function Fe(e){return e.length>=10?{width:j(e,6),height:j(e,8)}:null}function Ke(e){if(e.length<30)return null;let n=M(e,12,4);if(n==="VP8 ")return e[23]!==157||e[24]!==1||e[25]!==42?null:{width:j(e,26)&16383,height:j(e,28)&16383};if(n==="VP8L"){if(e[20]!==47)return null;let t=e[21],s=e[22],a=e[23],o=e[24];return{width:1+((s&63)<<8|t),height:1+((o&15)<<10|a<<2|(s&192)>>6)}}return n==="VP8X"?{width:1+ge(e,24),height:1+ge(e,27)}:null}function We(e){let n=null,t=Math.min(e.length,256*1024);for(let s=N(e,"ispe",0,t);s!==-1&&!(s+16>e.length);s=N(e,"ispe",s+4,t)){let a={width:T(e,s+8),height:T(e,s+12)};(!n||a.width*a.height>n.width*n.height)&&(n=a)}return n}function K(e,n){switch(E(e)){case"png":return Ne(n);case"jpg":case"jpeg":return De(n);case"gif":return Fe(n);case"webp":return Ke(n);case"avif":return We(n);default:return null}}function He(e){let n=0;for(let t=12;t+8<=e.length;){let s=M(e,t,4),a=C(e,t+4);if(s==="fmt "&&t+20<=e.length&&(n=C(e,t+16)),s==="data")return n>0?a/n:null;t+=8+a+a%2}return null}var he={v1:[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320],v2:[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160]},Qe=[44100,48e3,32e3];function Je(e){let n=0;for(M(e,0,3)==="ID3"&&e.length>10&&(n=10+(e[6]<<21|e[7]<<14|e[8]<<7|e[9]));n+4<e.length&&n<512*1024;n++){if(e[n]!==255||(e[n+1]&224)!==224)continue;let t=e[n+1]>>3&3,s=e[n+1]>>1&3,a=e[n+2]>>2&3,o=e[n+2]>>4;if(t===1||s!==1||a===3||o===0||o===15)continue;let l=t===3,u=Qe[a]/(l?1:t===2?2:4),r=(l?he.v1:he.v2)[o],p=e[n+3]>>6===3,m=l?1152:576,w=n+4+(l?p?17:32:p?9:17),f=M(e,w,4);return(f==="Xing"||f==="Info")&&w+12<=e.length&&T(e,w+4)&1?T(e,w+8)*m/u:(e.length-n)*8/(r*1e3)}return null}function Ve(e){if(M(e,0,4)!=="OggS"||e.length<28)return null;let n=27+e[26],t=0,s=0;if(M(e,n,7)==="vorbis"?t=C(e,n+12):M(e,n,8)==="OpusHead"&&(t=48e3,s=j(e,n+10)),!t)return null;for(let a=e.length-14;a>=0;a--){if(e[a]!==79||M(e,a,4)!=="OggS")continue;let o=C(e,a+6)+C(e,a+10)*2**32;return Math.max(0,o-s)/t}return null}function Xe(e){let n=N(e,"mvhd");if(n===-1||n+32>e.length)return null;if(e[n+4]===1){let a=T(e,n+24),o=T(e,n+28)*2**32+T(e,n+32);return a?o/a:null}let s=T(e,n+16);return s?T(e,n+20)/s:null}function Ze(e,n){switch(E(e)){case"wav":return He(n);case"mp3":return Je(n);case"ogg":return Ve(n);case"m4a":return Xe(n);default:return null}}var Ye=/<(?!a[\s>])[a-z][^>]*?\s(?:src|href|poster)\s*=\s*["']([^"']+)["']/gi,xe=/url\(\s*["']?([^"')]+)["']?\s*\)|@import\s+["']([^"']+)["']/gi,qe=/(?:^|[\s;}])(?:import|export)\s*(?:[\w$*{}\s,]+?\s*from\s*)?["']([^"'\n]+)["']/g;function et(e,n){let t=n.trim().split(/[?#]/)[0];if(!t||/^[a-z][a-z0-9+.-]*:/i.test(t)||t.startsWith("//"))return null;let s=t.startsWith("/")?[]:e.split("/").slice(0,-1);for(let a of t.split("/"))if(!(a===""||a==="."))if(a===".."){if(!s.length)return null;s.pop()}else s.push(a);return s.join("/")||null}function tt(e){if(!e.has($))return[];let n=new Set([$]),t=[$],s=a=>new TextDecoder().decode(e.get(a));for(;t.length;){let a=t.shift(),o=I(a)?.kind,l=[],u=(r,p)=>{r.lastIndex=0;for(let m=r.exec(p);m;m=r.exec(p))l.push(m[1]??m[2])};if(o==="html"){let r=s(a);u(Ye,r),u(xe,r)}else if(o==="style")u(xe,s(a));else if(o==="script")u(qe,s(a));else continue;for(let r of l){let p=et(a,r);p&&e.has(p)&&!n.has(p)&&(n.add(p),t.push(p))}}return[...n].sort()}var S=e=>`${(e/(1024*1024)).toFixed(1)} MB`,O=e=>e<1024*1024?`${Math.round(e/1024)} KB`:S(e);function ye(e){let n=e instanceof Map?e:new Map([...e].map(r=>[r.path,r.bytes])),t=Ce,s={images:[],sounds:[],models:[],triangles:0,decodedBytes:0,start:{files:[],bytes:0},issues:[],advice:[]},a=(r,p,m)=>s.issues.push({path:r,code:p,message:m}),o=(r,p,m)=>s.advice.push({path:r,code:p,message:m});for(let[r,p]of[...n].sort(([m],[w])=>m<w?-1:1)){let m=I(r),w=E(r);if(m?.kind==="image"&&w!=="svg"){let f=K(r,p);if(!f||!f.width||!f.height){w==="avif"?o(r,"image_size_unknown","Seek could not read the size of this AVIF image, so it is not counted in the memory budget. WebP or PNG is safer."):a(r,"image_unreadable","Seek cannot read the size of this image: the file looks broken. Export it again.");continue}let x=f.width*f.height*4;s.images.push({path:r,width:f.width,height:f.height,bytes:p.byteLength,decoded:x,...f.alpha!==void 0?{alpha:f.alpha}:{}}),s.decodedBytes+=x;let i=Math.max(f.width,f.height);i>t.imageMaxSide?a(r,"image_too_large",`${f.width} x ${f.height} pixels: an image is at most ${t.imageMaxSide} pixels on its longest side. Phones cannot keep larger ones in memory. seek optimize scales it down.`):i>t.imageAdviseSide&&o(r,"image_large",`${f.width} x ${f.height} pixels takes ${S(x)} of memory once loaded. ${t.imageAdviseSide} pixels on the longest side is plenty for a phone; seek optimize scales it down.`),w==="png"&&f.alpha===!1&&p.byteLength>t.opaquePngAdviseBytes&&o(r,"opaque_png",`A ${O(p.byteLength)} PNG without transparency is probably a photo. As WebP or JPEG it is often 5 to 10 times smaller: seek optimize --webp.`),w==="gif"&&p.byteLength>t.gifAdviseBytes&&o(r,"heavy_gif",`A ${O(p.byteLength)} GIF. Animated GIFs are heavy to download and to play; an animated WebP or a sprite sheet is far lighter.`)}else if(m?.kind==="audio"){let f=Ze(r,p);s.sounds.push({path:r,seconds:f,bytes:p.byteLength}),f!==null&&f>t.audioMaxSeconds&&a(r,"audio_too_long",`This sound lasts ${Math.round(f)} seconds; one sound is at most ${t.audioMaxSeconds/60} minutes.`),w==="wav"&&(p.byteLength>t.wavMaxBytes?a(r,"wav_too_big",`A WAV of ${O(p.byteLength)} is uncompressed sound: at most ${O(t.wavMaxBytes)}. As M4A it is about a tenth of the size: seek optimize --audio.`):p.byteLength>t.wavAdviseBytes&&o(r,"wav_audio",`A WAV is uncompressed sound. As M4A this ${O(p.byteLength)} is about a tenth: seek optimize --audio.`))}else if(m?.kind==="model"&&w!=="bin"){let f=pe(r,p,n);s.models.push(f),s.triangles+=f.triangles;for(let c of f.problems)a(r,c.code,c.message);for(let c of f.textures)s.decodedBytes+=c.decoded,Math.max(c.width,c.height)>t.imageMaxSide&&a(r,"texture_too_large",`A texture of ${c.width} x ${c.height} pixels: at most ${t.imageMaxSide} on its longest side. seek optimize scales it down.`);!f.problems.length&&f.triangles>P.trianglesAdvise&&o(r,"model_detailed",`${f.triangles.toLocaleString("en")} triangles. Above ${P.trianglesAdvise.toLocaleString("en")} a phone starts to slow down; decimate it in Blender where nobody looks closely.`);let x=f.textures.filter(c=>c.ref.startsWith(`${r}#`)).reduce((c,d)=>c+d.bytes,0);f.textures.some(c=>c.mimeType!=="image/webp"||Math.max(c.width,c.height)>t.imageAdviseSide)&&x>p.byteLength*P.textureShareAdvise&&x>256*1024&&o(r,"model_textures",`${O(x)} of this model is textures. As WebP and at most ${t.imageAdviseSide} pixels they are far smaller: seek optimize --webp.`)}else m?.kind==="font"&&(w==="ttf"||w==="otf")&&o(r,"font_format",`A .${w} font: as WOFF2 it is usually 30 to 50% smaller and loads the same everywhere.`)}s.triangles>P.gameTrianglesMax?a("","triangles_too_many",`All models together draw ${s.triangles.toLocaleString("en")} triangles; a game may draw at most ${P.gameTrianglesMax.toLocaleString("en")}.`):s.triangles>P.gameTrianglesAdvise&&o("","triangles_many",`All models together draw ${s.triangles.toLocaleString("en")} triangles. Older phones struggle above ${P.gameTrianglesAdvise.toLocaleString("en")}; load models per phase and drop the ones you no longer show.`);let l=[...n.values()].reduce((r,p)=>r+p.byteLength,0);l>k.totalAdviseBytes&&l<=k.totalBytes&&o("","game_heavy",`The game is ${S(l)}. Players download it outside, often on 4G; under ${S(k.totalAdviseBytes)} it starts quickly. seek optimize --all makes images, sound and models lighter.`),s.decodedBytes>t.decodedMaxBytes?a("","memory_too_big",`All images together take ${S(s.decodedBytes)} of memory once loaded; a game may use at most ${S(t.decodedMaxBytes)}. Scale the largest ones down (seek optimize) or load them only when needed.`):s.decodedBytes>t.decodedAdviseBytes&&o("","memory_heavy",`All images together take ${S(s.decodedBytes)} of memory once loaded. Older phones start to struggle above ${S(t.decodedAdviseBytes)}.`);let u=tt(n);return s.start={files:u,bytes:u.reduce((r,p)=>r+(n.get(p)?.byteLength??0),0)},s.start.bytes>t.startMaxBytes?a($,"start_too_big",`index.html loads ${S(s.start.bytes)} before the game can start; at most ${S(t.startMaxBytes)}. On a phone outside that takes far too long. Load the rest after ready(), with import() or fetch().`):s.start.bytes>t.startAdviseBytes&&o($,"start_heavy",`index.html loads ${S(s.start.bytes)} before the game can start. Outside, on 4G, every MB is about a second of waiting; keep it under ${S(t.startAdviseBytes)} and load the rest after ready().`),s}function en(e){let n=je;if(e.byteLength>n.maxBytes)return{code:"too_big",message:"A cover is at most 1 MB."};let t=e,s=t[0]===137&&t[1]===80?"png":t[0]===255&&t[1]===216?"jpg":M(t,0,4)==="RIFF"&&M(t,8,4)==="WEBP"?"webp":null;if(!s)return{code:"bad_cover",message:"A cover is a PNG, JPEG or WebP image."};let a=K(`cover.${s}`,e);if(!a)return{code:"bad_cover",message:"Seek cannot read the size of this image: export it again."};let{width:o,height:l}=a;return o<n.minWidth||l<n.minHeight?{code:"cover_too_small",message:`A cover is at least ${n.minWidth} x ${n.minHeight} pixels; this one is ${o} x ${l}.`}:o>n.maxWidth||l>n.maxHeight?{code:"cover_too_large",message:`A cover is at most ${n.maxWidth} x ${n.maxHeight} pixels; this one is ${o} x ${l}.`}:Math.abs(o/l/n.ratio-1)>n.ratioTolerance?{code:"cover_ratio",message:`A cover is 16:9, like 1920 x 1080; this one is ${o} x ${l}.`}:null}var we=1,rn=/^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]-[0-9a-f]{16}$/,nt="play.seekprotocol.ai",be="https://api.play.seekprotocol.ai",k={files:300,fileBytes:8*1024*1024,totalBytes:30*1024*1024,totalAdviseBytes:15*1024*1024,pathLength:120,depth:5,segment:/^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/,storageBytes:64*1024},st={html:{type:"text/html; charset=utf-8",kind:"html",text:!0},js:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},mjs:{type:"text/javascript; charset=utf-8",kind:"script",text:!0},css:{type:"text/css; charset=utf-8",kind:"style",text:!0},json:{type:"application/json; charset=utf-8",kind:"data",text:!0},svg:{type:"image/svg+xml",kind:"image",text:!0},png:{type:"image/png",kind:"image",text:!1},jpg:{type:"image/jpeg",kind:"image",text:!1},jpeg:{type:"image/jpeg",kind:"image",text:!1},gif:{type:"image/gif",kind:"image",text:!1},webp:{type:"image/webp",kind:"image",text:!1},avif:{type:"image/avif",kind:"image",text:!1},mp3:{type:"audio/mpeg",kind:"audio",text:!1},ogg:{type:"audio/ogg",kind:"audio",text:!1},wav:{type:"audio/wav",kind:"audio",text:!1},m4a:{type:"audio/mp4",kind:"audio",text:!1},woff:{type:"font/woff",kind:"font",text:!1},woff2:{type:"font/woff2",kind:"font",text:!1},ttf:{type:"font/ttf",kind:"font",text:!1},otf:{type:"font/otf",kind:"font",text:!1},glb:{type:"model/gltf-binary",kind:"model",text:!1},gltf:{type:"model/gltf+json",kind:"model",text:!0},bin:{type:"application/octet-stream",kind:"model",text:!1}},$="index.html",it=/^[0-9a-f]{64}$/,at=e=>[...new Uint8Array(e)].map(n=>n.toString(16).padStart(2,"0")).join(""),W=async e=>at(await crypto.subtle.digest("SHA-256",typeof e=="string"?new TextEncoder().encode(e):e)),E=e=>/\.([a-z0-9]+)$/i.exec(e)?.[1]?.toLowerCase()??"",I=e=>st[E(e)];function te(e){let n=(s,a)=>({path:e,code:s,message:a});if(typeof e!="string"||!e)return n("bad_path","A file needs a path.");if(e.length>k.pathLength)return n("path_too_long",`A path is at most ${k.pathLength} characters.`);let t=e.split("/");return t.length>k.depth?n("path_too_deep",`Folders go at most ${k.depth-1} deep.`):t.every(s=>k.segment.test(s))?I(e)?null:n("file_type",`".${E(e)||"(none)"}" files are not allowed. A game is html, js, css, json, images, audio, fonts and 3D models (glb, gltf with its bin).`):n("bad_path",'Use letters, digits, dots, hyphens and underscores; no hidden files, spaces or "..".')}var A=(e,n,t=0)=>n.every((s,a)=>e[t+a]===s),rt=[["a Windows executable",[77,90]],["an ELF executable",[127,69,76,70]],["a Mach-O executable",[207,250,237,254]],["a Mach-O executable",[206,250,237,254]],["a Mach-O executable",[254,237,250,207]],["a Mach-O or Java binary",[202,254,186,190]],["a script with an interpreter line",[35,33]],["a zip, jar or apk archive",[80,75,3,4]],["WebAssembly",[0,97,115,109]],["a gzip archive",[31,139]],["a PDF",[37,80,68,70]]],ot={png:e=>A(e,[137,80,78,71,13,10,26,10]),jpg:e=>A(e,[255,216,255]),jpeg:e=>A(e,[255,216,255]),gif:e=>A(e,[71,73,70,56]),webp:e=>A(e,[82,73,70,70])&&A(e,[87,69,66,80],8),wav:e=>A(e,[82,73,70,70])&&A(e,[87,65,86,69],8),ogg:e=>A(e,[79,103,103,83]),woff:e=>A(e,[119,79,70,70]),woff2:e=>A(e,[119,79,70,50]),glb:e=>A(e,[103,108,84,70])};function ke(e,n){let t=I(e);if(!t)return te(e);let s=E(e);for(let[o,l]of rt)if((!t.text||l[0]===35)&&A(n,l))return{path:e,code:"executable",message:`This file is ${o}, which a game cannot carry.`};if(t.text){if(n.includes(0))return{path:e,code:"not_text",message:"A text file must not contain NUL bytes."};try{new TextDecoder("utf-8",{fatal:!0}).decode(n)}catch{return{path:e,code:"not_text",message:"A text file must be UTF-8."}}return null}let a=ot[s];return a&&!a(n)?{path:e,code:"type_mismatch",message:`This file is not really a .${s} file.`}:null}function dt(e){let n=[],t=(u,r,p)=>n.push({path:u,code:r,message:p}),s=e;if(!s||typeof s!="object"||Array.isArray(s))return[{path:"",code:"bad_game",message:"Not a game manifest."}];let a=new Set(["format","id","entry","files","size"]);for(let u of Object.keys(s))a.has(u)||t(u,"bad_game",`Unknown field "${u}".`);if(s.format!==we&&t("format","bad_format",`Unknown game format ${String(s.format)}.`),(typeof s.id!="string"||!U.test(s.id))&&t("id","bad_game_id","A game id is 3 to 30 lowercase letters, digits and hyphens."),s.entry!==$&&t("entry","bad_entry",`A game starts at ${$}.`),!Array.isArray(s.files))return[...n,{path:"files",code:"bad_game",message:"A game lists its files."}];s.files.length>k.files&&t("files","too_many_files",`A game has at most ${k.files} files; this one has ${s.files.length}.`);let o=0,l="";for(let[u,r]of s.files.entries()){let p=`files[${u}]`;if(!r||typeof r!="object"||Object.keys(r).sort().join()!=="path,sha256,size,type"){t(p,"bad_game","A file is { path, size, sha256, type }.");continue}let m=te(r.path);m?t(r.path||p,m.code,m.message):r.type!==I(r.path).type&&t(r.path,"bad_type",`A .${E(r.path)} file is served as ${I(r.path).type}.`),typeof r.path=="string"&&r.path<=l&&t(r.path,"not_sorted","Files are sorted by path, each once."),typeof r.path=="string"&&(l=r.path),!Number.isSafeInteger(r.size)||r.size<0?t(r.path||p,"bad_size","A size is a whole number of bytes."):r.size>k.fileBytes?t(r.path,"file_too_big",`A file is at most ${k.fileBytes} bytes; this one is ${r.size}.`):o+=r.size,(typeof r.sha256!="string"||!it.test(r.sha256))&&t(r.path||p,"bad_sha256","sha256 is 64 lowercase hex characters.")}return o>k.totalBytes&&t("files","game_too_big",`A game is at most ${k.totalBytes} bytes; this one is ${o}.`),s.size!==o&&t("size","bad_size","size is the sum of the file sizes."),s.files.some(u=>u?.path===$)||t($,"entry_missing",`A game needs ${$} at the top of its folder.`),n}var ct=e=>W(L(e));async function on(e,n){return n.byteLength!==e.size?{path:e.path,code:"size_mismatch",message:`The manifest says ${e.size} bytes; the file has ${n.byteLength}.`}:await W(n)!==e.sha256?{path:e.path,code:"sha256_mismatch",message:"The file does not match its sha256."}:ke(e.path,n)}async function dn(e,n){let t=[],s=[...n].sort((r,p)=>r.path<p.path?-1:r.path>p.path?1:0),a=[];for(let r of s){let p=te(r.path)??ke(r.path,r.bytes);p&&t.push(p),a.push({path:r.path,size:r.bytes.byteLength,sha256:await W(r.bytes),type:I(r.path)?.type??"application/octet-stream"})}let o={format:we,id:e,entry:$,files:a,size:a.reduce((r,p)=>r+p.size,0)},l=new Set(t.map(r=>`${r.path}:${r.code}`));for(let r of dt(o))!l.has(`${r.path}:${r.code}`)&&r.code!=="bad_type"&&t.push(r);let u=ye(s.filter(r=>!t.some(p=>p.path===r.path)));return t.push(...u.issues),{manifest:o,hash:await ct(o),issues:t,assets:u}}async function cn(e,n){return`${n}-${(await W(e.toLowerCase())).slice(0,16)}`}var ln=e=>`https://${e}.${nt}`,lt=/\b(?:https?|wss?):\/\/[^\s"'`<>()\\]+/gi,mt=/^(?:https?:\/\/www\.w3\.org\/|https:\/\/storeconnect\.seekprotocol\.ai(?:$|\/)|http:\/\/(?:localhost|127\.0\.0\.1)(?::\d{1,5})?(?:$|\/))/i,ut=/\s(src|href|action|formaction|poster|data|srcset|background|xlink:href|manifest)\s*=\s*["']?\s*((?:(?:https?|wss?):)?\/\/[^"'\s>]+)/gi,ft=/(?:url\(\s*["']?|@import\s+["'])\s*((?:(?:https?):)?\/\/[^"')\s]+)/gi,pt=[["eval",/\beval\s*\(/g,"eval() runs text as code."],["new_function",/\bnew\s+Function\s*\(/g,"new Function() runs text as code."],["string_timer",/\bset(?:Timeout|Interval)\s*\(\s*["'`]/g,"A timer with a string runs text as code."],["document_write",/\bdocument\.write(?:ln)?\s*\(/g,"document.write() writes markup at run time."],["window_open",/\bwindow\.open\s*\(/g,"Opening windows is not allowed."],["navigation",/\b(?:(?:top|parent|window|document)\.location|location\.href)\s*=(?!=)/g,"A game does not navigate away."],["javascript_url",/javascript\s*:/gi,"javascript: URLs run code."]],gt=/<[a-z][^>]*?\son[a-z]+\s*=/gi,ht=/<script\b(?![^>]*\bsrc\s*=)[^>]*>\s*[^<\s]/gi,xt=/<meta[^>]+http-equiv\s*=\s*["']?refresh/gi,yt=/<base\b/gi,ee=e=>e===be||e.startsWith(`${be}/`);function mn(e,n,t=20){let s=I(e);if(!s?.text||s.kind==="data")return[];let a=[],o=[];for(let m=n.indexOf(`
|
|
4
|
+
`);m!==-1;m=n.indexOf(`
|
|
5
|
+
`,m+1))o.push(m);let l=m=>{let w=0,f=o.length;for(;w<f;){let x=w+f>>1;o[x]<m?w=x+1:f=x}return w+1},u=(m,w,f,x,i)=>{a.length<t&&!a.some(c=>c.code===w&&c.line===l(m))&&a.push({path:e,line:l(m),code:w,severity:f,message:x,match:i.slice(0,120)})},r=(m,w)=>{m.lastIndex=0;for(let f=m.exec(n);f;f=m.exec(n))w(f)},p=new Set;if((s.kind==="html"||s.kind==="image")&&(r(ut,m=>{ee(m[2])||(p.add(l(m.index)),u(m.index,"remote_url","fail",`"${m[1]}" points outside the game. Everything a game loads ships inside it.`,m[2]))}),r(gt,m=>u(m.index,"inline_handler","flag","Inline event handlers do not run under the CSP; use addEventListener in a .js file.",m[0])),r(ht,m=>u(m.index,"inline_script","flag","Inline scripts do not run under the CSP; put the code in a .js file.",m[0])),r(xt,m=>u(m.index,"meta_refresh","fail","A game does not redirect.",m[0])),r(yt,m=>u(m.index,"base_tag","flag","A <base> tag changes where relative URLs point.",m[0]))),(s.kind==="style"||s.kind==="html")&&r(ft,m=>{ee(m[1])||(p.add(l(m.index)),u(m.index,"remote_url","fail","CSS that loads from outside the game.",m[1]))}),s.kind==="script"||s.kind==="html")for(let[m,w,f]of pt)r(w,x=>u(x.index,m,"flag",f,x[0]));return r(lt,m=>{ee(m[0])||mt.test(m[0])||p.has(l(m.index))||u(m.index,"remote_url_text","flag","A URL outside the game. The CSP blocks it; a reviewer checks why it is there.",m[0])}),a}export{Ce as ASSET_LIMITS,ne as BUNDLE_FORMAT,ie as CAPABILITIES,je as COVER_SPEC,H as DECLARATION_VALUES,rn as GAME_APP_ID_PATTERN,$ as GAME_ENTRY,st as GAME_FILE_TYPES,we as GAME_FORMAT,nt as GAME_HOST_SUFFIX,U as GAME_ID_PATTERN,k as GAME_LIMITS,be as GAME_PROXY_ORIGIN,z as GAME_TEXT_LIMITS,h as LIMITS,Be as LOG_GENESIS,Ge as MODEL_EXTENSIONS_OK,P as MODEL_LIMITS,Oe as PACKAGE_EXTENSION,Z as PACKAGE_LIMITS,v as PackageError,G as QuestInvalidError,se as SDK_RUNTIME,ve as VERSION_PATTERN,ye as analyzeGameAssets,pe as analyzeModel,At as asset,Ze as audioSeconds,X as bundleHash,L as canonicalJson,oe as compile,en as coverProblem,ze as decompile,Ae as distanceM,E as extensionOf,cn as gameAppId,I as gameFileType,ct as gameHash,ln as gameOrigin,te as gamePathProblem,Te as gameTextIssues,zt as generateSigningKey,K as imageSize,Re as keyIdOf,mn as lintGameText,Ue as logEntryHash,St as marker,vt as outcome,Kt as packageFileName,dn as packageGame,kt as quest,ae as questAssets,V as questGames,fe as readGlb,Ht as readPackage,Mt as reward,Xt as rewriteGlbTextures,Ot as signHash,de as signedMessage,ke as sniffGameFile,tt as startFiles,Tt as step,F as successors,wt as t,Se as usedCapabilities,re as validate,dt as validateGameManifest,$t as variant,le as verifyBundle,on as verifyGameFile,Ie as verifyHash,Ct as verifyLogChain,Gt as verifySeekSignature,Wt as writePackage,_t as zone};
|
package/models.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export declare const MODEL_LIMITS: {
|
|
2
|
+
readonly trianglesMax: 500000;
|
|
3
|
+
readonly trianglesAdvise: 100000;
|
|
4
|
+
readonly gameTrianglesMax: 1500000;
|
|
5
|
+
readonly gameTrianglesAdvise: 300000;
|
|
6
|
+
readonly textureShareAdvise: 0.5;
|
|
7
|
+
};
|
|
8
|
+
export declare const MODEL_EXTENSIONS_OK: Set<string>;
|
|
9
|
+
export interface ModelTexture {
|
|
10
|
+
ref: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
bytes: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
decoded: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ModelReport {
|
|
18
|
+
path: string;
|
|
19
|
+
triangles: number;
|
|
20
|
+
meshes: number;
|
|
21
|
+
animations: number;
|
|
22
|
+
textures: ModelTexture[];
|
|
23
|
+
needs: string[];
|
|
24
|
+
extensions: string[];
|
|
25
|
+
problems: {
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
interface Gltf {
|
|
31
|
+
asset?: {
|
|
32
|
+
version?: string;
|
|
33
|
+
};
|
|
34
|
+
extensionsUsed?: string[];
|
|
35
|
+
extensionsRequired?: string[];
|
|
36
|
+
buffers?: {
|
|
37
|
+
uri?: string;
|
|
38
|
+
byteLength?: number;
|
|
39
|
+
}[];
|
|
40
|
+
bufferViews?: {
|
|
41
|
+
buffer?: number;
|
|
42
|
+
byteOffset?: number;
|
|
43
|
+
byteLength?: number;
|
|
44
|
+
}[];
|
|
45
|
+
accessors?: {
|
|
46
|
+
count?: number;
|
|
47
|
+
}[];
|
|
48
|
+
meshes?: {
|
|
49
|
+
primitives?: {
|
|
50
|
+
mode?: number;
|
|
51
|
+
indices?: number;
|
|
52
|
+
attributes?: Record<string, number>;
|
|
53
|
+
}[];
|
|
54
|
+
}[];
|
|
55
|
+
images?: {
|
|
56
|
+
uri?: string;
|
|
57
|
+
bufferView?: number;
|
|
58
|
+
mimeType?: string;
|
|
59
|
+
}[];
|
|
60
|
+
animations?: unknown[];
|
|
61
|
+
}
|
|
62
|
+
export declare function readGlb(bytes: Uint8Array): {
|
|
63
|
+
json: Gltf;
|
|
64
|
+
bin: Uint8Array | null;
|
|
65
|
+
jsonText: string;
|
|
66
|
+
} | {
|
|
67
|
+
problem: string;
|
|
68
|
+
};
|
|
69
|
+
export declare function analyzeModel(path: string, bytes: Uint8Array, files?: ReadonlyMap<string, Uint8Array>): ModelReport;
|
|
70
|
+
export type TextureRewrite = (bytes: Uint8Array, mimeType: string) => Promise<{
|
|
71
|
+
bytes: Uint8Array;
|
|
72
|
+
mimeType: string;
|
|
73
|
+
} | null>;
|
|
74
|
+
export declare function rewriteGlbTextures(bytes: Uint8Array, rewrite: TextureRewrite): Promise<{
|
|
75
|
+
bytes: Uint8Array;
|
|
76
|
+
changed: number;
|
|
77
|
+
} | null>;
|
|
78
|
+
export {};
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -30,7 +30,11 @@ export declare const CAPABILITIES: readonly [
|
|
|
30
30
|
"rewards.cosmetics",
|
|
31
31
|
"rewards.boosts",
|
|
32
32
|
"rewards.crypto",
|
|
33
|
-
"gamekit"
|
|
33
|
+
"gamekit",
|
|
34
|
+
"location.beacon",
|
|
35
|
+
"rewards.outcomes",
|
|
36
|
+
"ar.models",
|
|
37
|
+
"map.marker"
|
|
34
38
|
];
|
|
35
39
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
36
40
|
interface StepBase {
|
|
@@ -39,10 +43,22 @@ interface StepBase {
|
|
|
39
43
|
story?: Text;
|
|
40
44
|
next?: string | null;
|
|
41
45
|
}
|
|
46
|
+
export interface GameAsset {
|
|
47
|
+
game: string;
|
|
48
|
+
file: string;
|
|
49
|
+
}
|
|
42
50
|
export interface TalkStep extends StepBase {
|
|
43
51
|
kind: "talk";
|
|
44
52
|
giver: string;
|
|
45
53
|
at?: Place;
|
|
54
|
+
model?: GameAsset;
|
|
55
|
+
}
|
|
56
|
+
export interface BeaconStep extends StepBase {
|
|
57
|
+
kind: "beacon";
|
|
58
|
+
minM: number;
|
|
59
|
+
maxM: number;
|
|
60
|
+
radiusM: number;
|
|
61
|
+
model?: GameAsset;
|
|
46
62
|
}
|
|
47
63
|
export interface ArriveStep extends StepBase {
|
|
48
64
|
kind: "arrive";
|
|
@@ -73,15 +89,21 @@ export interface ChoiceStep extends StepBase {
|
|
|
73
89
|
next: string | null;
|
|
74
90
|
}[];
|
|
75
91
|
}
|
|
92
|
+
export interface Outcome {
|
|
93
|
+
id: string;
|
|
94
|
+
title: Text;
|
|
95
|
+
minScore: number;
|
|
96
|
+
}
|
|
76
97
|
export interface PlayStep extends StepBase {
|
|
77
98
|
kind: "play";
|
|
78
99
|
game: string;
|
|
79
100
|
minScore: number;
|
|
80
101
|
maxScore: number;
|
|
102
|
+
outcomes?: Outcome[];
|
|
81
103
|
}
|
|
82
|
-
export type Step = TalkStep | ArriveStep | CatchStep | WalkStep | ArenaStep | ChoiceStep | PlayStep;
|
|
104
|
+
export type Step = TalkStep | ArriveStep | BeaconStep | CatchStep | WalkStep | ArenaStep | ChoiceStep | PlayStep;
|
|
83
105
|
export type StepKind = Step["kind"];
|
|
84
|
-
export type Reward = {
|
|
106
|
+
export type Reward = ({
|
|
85
107
|
kind: "xp";
|
|
86
108
|
amount: number;
|
|
87
109
|
} | {
|
|
@@ -99,7 +121,29 @@ export type Reward = {
|
|
|
99
121
|
coin: string;
|
|
100
122
|
perPlayer: number;
|
|
101
123
|
maxPlayers: number;
|
|
124
|
+
} | {
|
|
125
|
+
kind: "draw";
|
|
126
|
+
coins: string[];
|
|
127
|
+
perPlayer: number;
|
|
128
|
+
maxPlayers: number;
|
|
129
|
+
}) & {
|
|
130
|
+
when?: RewardCondition;
|
|
102
131
|
};
|
|
132
|
+
export type RewardCondition = {
|
|
133
|
+
step: string;
|
|
134
|
+
outcome: string;
|
|
135
|
+
} | {
|
|
136
|
+
variant: string;
|
|
137
|
+
};
|
|
138
|
+
export interface Variant {
|
|
139
|
+
id: string;
|
|
140
|
+
title: Text;
|
|
141
|
+
weight: number;
|
|
142
|
+
}
|
|
143
|
+
export interface Marker {
|
|
144
|
+
image: GameAsset;
|
|
145
|
+
model?: GameAsset;
|
|
146
|
+
}
|
|
103
147
|
export interface Declarations {
|
|
104
148
|
places: "public" | "private_with_permission";
|
|
105
149
|
effort: "light" | "moderate" | "hard";
|
|
@@ -135,6 +179,8 @@ export interface QuestDefinition {
|
|
|
135
179
|
to: string;
|
|
136
180
|
};
|
|
137
181
|
repeat?: "never" | "daily" | "weekly";
|
|
182
|
+
variants?: Variant[];
|
|
183
|
+
marker?: Marker;
|
|
138
184
|
capabilities: Capability[];
|
|
139
185
|
steps: Step[];
|
|
140
186
|
rewards: Reward[];
|
|
@@ -142,7 +188,7 @@ export interface QuestDefinition {
|
|
|
142
188
|
declarations: Declarations;
|
|
143
189
|
}
|
|
144
190
|
export type TextTable = Record<string, string>;
|
|
145
|
-
type CompileOne<S extends Step> = Omit<S, "title" | "story" | "next" | "options"> & {
|
|
191
|
+
type CompileOne<S extends Step> = Omit<S, "title" | "story" | "next" | "options" | "outcomes"> & {
|
|
146
192
|
title: string;
|
|
147
193
|
story?: string;
|
|
148
194
|
next: string | null;
|
|
@@ -152,6 +198,12 @@ type CompileOne<S extends Step> = Omit<S, "title" | "story" | "next" | "options"
|
|
|
152
198
|
label: string;
|
|
153
199
|
next: string | null;
|
|
154
200
|
}[];
|
|
201
|
+
} : unknown) & (S extends PlayStep ? {
|
|
202
|
+
outcomes?: {
|
|
203
|
+
id: string;
|
|
204
|
+
title: string;
|
|
205
|
+
minScore: number;
|
|
206
|
+
}[];
|
|
155
207
|
} : unknown);
|
|
156
208
|
export type CompiledStep = {
|
|
157
209
|
[K in StepKind]: CompileOne<Extract<Step, {
|
|
@@ -172,6 +224,12 @@ export interface QuestManifest {
|
|
|
172
224
|
to: string;
|
|
173
225
|
};
|
|
174
226
|
repeat: "never" | "daily" | "weekly";
|
|
227
|
+
variants?: {
|
|
228
|
+
id: string;
|
|
229
|
+
title: string;
|
|
230
|
+
weight: number;
|
|
231
|
+
}[];
|
|
232
|
+
marker?: Marker;
|
|
175
233
|
minAge: 13 | 16 | 18;
|
|
176
234
|
declarations: Declarations;
|
|
177
235
|
capabilities: Capability[];
|
package/validate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Capability, type Point, type QuestDefinition, type Step } from "./types.js";
|
|
1
|
+
import { type Capability, type GameAsset, type Point, type QuestDefinition, type Step } from "./types.js";
|
|
2
2
|
export declare const LIMITS: {
|
|
3
3
|
readonly steps: {
|
|
4
4
|
readonly min: 1;
|
|
@@ -44,8 +44,34 @@ export declare const LIMITS: {
|
|
|
44
44
|
readonly max: 3;
|
|
45
45
|
};
|
|
46
46
|
readonly rewards: {
|
|
47
|
+
readonly max: 10;
|
|
48
|
+
};
|
|
49
|
+
readonly beaconM: {
|
|
50
|
+
readonly min: 50;
|
|
51
|
+
readonly max: 2000;
|
|
52
|
+
};
|
|
53
|
+
readonly beaconRadiusM: {
|
|
54
|
+
readonly min: 10;
|
|
55
|
+
readonly max: 100;
|
|
56
|
+
};
|
|
57
|
+
readonly outcomes: {
|
|
58
|
+
readonly max: 3;
|
|
59
|
+
};
|
|
60
|
+
readonly outcomeIdPattern: RegExp;
|
|
61
|
+
readonly variants: {
|
|
62
|
+
readonly min: 2;
|
|
47
63
|
readonly max: 6;
|
|
48
64
|
};
|
|
65
|
+
readonly variantWeight: {
|
|
66
|
+
readonly min: 1;
|
|
67
|
+
readonly max: 100;
|
|
68
|
+
};
|
|
69
|
+
readonly drawCoins: {
|
|
70
|
+
readonly min: 2;
|
|
71
|
+
readonly max: 10;
|
|
72
|
+
};
|
|
73
|
+
readonly outcomeBonusShare: 0.5;
|
|
74
|
+
readonly variantBonusShare: 1;
|
|
49
75
|
readonly windowDays: {
|
|
50
76
|
readonly max: 92;
|
|
51
77
|
};
|
|
@@ -57,5 +83,11 @@ export interface Issue {
|
|
|
57
83
|
}
|
|
58
84
|
export declare function distanceM(a: Point, b: Point): number;
|
|
59
85
|
export declare function usedCapabilities(q: QuestDefinition): Set<Capability>;
|
|
86
|
+
export declare function questAssets(q: QuestDefinition): {
|
|
87
|
+
path: string;
|
|
88
|
+
asset: GameAsset;
|
|
89
|
+
kind: "model" | "image";
|
|
90
|
+
}[];
|
|
91
|
+
export declare function questGames(q: QuestDefinition): string[];
|
|
60
92
|
export declare function successors(steps: readonly Step[], index: number): (string | null)[];
|
|
61
93
|
export declare function validate(q: QuestDefinition): Issue[];
|