@@ -163,6 +163,9 @@ function getDataBaseUrl(version: string): string {
163163 return `${ CBN_DATA_BASE_URL } /data/${ version } ` ;
164164}
165165
166+ /**
167+ * @internal
168+ */
166169export function resolveModChunkUrl (
167170 version : string ,
168171 modId : string ,
@@ -174,6 +177,9 @@ export function resolveModChunkUrl(
174177 ) ;
175178}
176179
180+ /**
181+ * @internal
182+ */
177183export function resolveExternalChunkUrl ( version : string , file : string ) : string {
178184 return resolvePath (
179185 getDataBaseUrl ( version ) ,
@@ -345,6 +351,9 @@ function getCachedBaseTileset(url: string): Promise<NonNullable<TilesetData>> {
345351 return created ;
346352}
347353
354+ /**
355+ * @internal
356+ */
348357export function collectActiveModTilesets (
349358 data : CBNData ,
350359) : ModTilesetContribution [ ] {
@@ -376,6 +385,9 @@ function isExternalTilesetChunk(file: string): boolean {
376385 return file . startsWith ( "external_tileset/" ) ;
377386}
378387
388+ /**
389+ * @internal
390+ */
379391export function collectExternalTilesets (
380392 data : Pick < CBNData , "all" > ,
381393) : ExternalTilesetContribution [ ] {
@@ -402,6 +414,9 @@ export function collectExternalTilesets(
402414 return result ;
403415}
404416
417+ /**
418+ * @internal
419+ */
405420export function isContributionCompatible (
406421 contribution : Pick < TilesetContribution , "compatibility" > ,
407422 selectedAliases : Set < string > ,
@@ -417,6 +432,9 @@ export function isContributionCompatible(
417432 return false ;
418433}
419434
435+ /**
436+ * @internal
437+ */
420438export function getTilesetCompatibilityIdentities (
421439 tilesetName : string ,
422440) : Set < string > {
@@ -444,6 +462,9 @@ function getMergeCacheKey(
444462 return `${ version } |${ tileset . path ?? "ascii" } |${ activeMods } |${ aliasSignature } |${ modsLoaded } ` ;
445463}
446464
465+ /**
466+ * @internal
467+ */
447468export async function loadMergedTileset (
448469 data : CBNData ,
449470 version : string ,
@@ -593,6 +614,10 @@ export type TilesetData = {
593614 baseUrl ?: string ;
594615} | null ;
595616
617+ type IndexedTilesetData = NonNullable < TilesetData > ;
618+ type TileLookupIndex = Map < string , TileInfo > ;
619+ const tileLookupIndexCache = new WeakMap < IndexedTilesetData , TileLookupIndex > ( ) ;
620+
596621export function resolveTileLayerUrl (
597622 tileset : TilesetData ,
598623 layer : TilePosition | undefined ,
@@ -610,72 +635,91 @@ export function findTile(
610635 id : string ,
611636) : TileInfo | undefined {
612637 if ( ! tileData || ! id ) return ;
613- //TODO: Cache tiles-new ranges and tile lookups per tileset to avoid per-cell scans.
638+ return getTileLookupIndex ( tileData ) . get ( id ) ;
639+ }
640+
641+ function getTileLookupIndex ( tileData : IndexedTilesetData ) : TileLookupIndex {
642+ const cached = tileLookupIndexCache . get ( tileData ) ;
643+ if ( cached ) return cached ;
644+
645+ const indexed = buildTileLookupIndex ( tileData ) ;
646+ tileLookupIndexCache . set ( tileData , indexed ) ;
647+ return indexed ;
648+ }
649+
650+ function buildTileLookupIndex ( tileData : IndexedTilesetData ) : TileLookupIndex {
614651 let offset = 0 ;
615- const ranges : { from : number ; to : number ; chunk : any } [ ] = [ ] ;
616- for ( const chunk of tileData [ "tiles-new" ] ) {
617- ranges . push ( {
652+ const ranges = tileData [ "tiles-new" ] . map ( ( chunk ) => {
653+ const range = {
618654 from : offset ,
619655 to : offset + chunk . nx * chunk . ny ,
620656 chunk,
621- } ) ;
622- offset += chunk . nx * chunk . ny ;
623- }
624- function findRange ( id : number ) {
625- for ( const range of ranges )
626- if ( id >= range . from && id < range . to ) return range ;
657+ } ;
658+ offset = range . to ;
659+ return range ;
660+ } ) ;
661+
662+ function findRange ( spriteId : number ) {
663+ for ( const range of ranges ) {
664+ if ( spriteId >= range . from && spriteId < range . to ) return range ;
665+ }
627666 }
628- function tileInfoForId ( id : number | undefined ) : TilePosition | undefined {
629- if ( id == null ) return ;
630- const range = findRange ( id ) ;
667+
668+ function tileInfoForSprite (
669+ spriteId : number | undefined ,
670+ ) : TilePosition | undefined {
671+ if ( spriteId == null ) return ;
672+ const range = findRange ( spriteId ) ;
631673 if ( ! range ) return ;
632- const offsetInFile = id - range . from ;
633- const fgTx = offsetInFile % range . chunk . nx ;
634- const fgTy = ( offsetInFile / range . chunk . nx ) | 0 ;
674+ const offsetInFile = spriteId - range . from ;
635675 return {
636676 file : range . chunk . file ,
637677 file_url : range . chunk . file_url ,
638678 source_base_url : range . chunk . source_base_url ,
639- // Safe to use ! because we check tileData at function entry
640- width : range . chunk . sprite_width ?? tileData ! . tile_info [ 0 ] . width ,
641- height : range . chunk . sprite_height ?? tileData ! . tile_info [ 0 ] . height ,
679+ width : range . chunk . sprite_width ?? tileData . tile_info [ 0 ] . width ,
680+ height : range . chunk . sprite_height ?? tileData . tile_info [ 0 ] . height ,
642681 offx : range . chunk . sprite_offset_x ?? 0 ,
643682 offy : range . chunk . sprite_offset_y ?? 0 ,
644- tx : fgTx ,
645- ty : fgTy ,
683+ tx : offsetInFile % range . chunk . nx ,
684+ ty : ( offsetInFile / range . chunk . nx ) | 0 ,
685+ } ;
686+ }
687+
688+ function firstSpriteRef ( value : unknown ) : number | undefined {
689+ const maybeArrayHead = Array . isArray ( value ) ? value [ 0 ] : value ;
690+ if ( typeof maybeArrayHead === "number" ) return maybeArrayHead ;
691+ if (
692+ typeof maybeArrayHead === "object" &&
693+ maybeArrayHead !== null &&
694+ typeof maybeArrayHead . sprite === "number"
695+ ) {
696+ return maybeArrayHead . sprite ;
697+ }
698+ }
699+
700+ function tileInfoForEntry ( entry : TileEntry ) : TileInfo {
701+ return {
702+ fg : tileInfoForSprite ( firstSpriteRef ( entry . fg ) ) ,
703+ bg : tileInfoForSprite ( firstSpriteRef ( entry . bg ) ) ,
646704 } ;
647705 }
648- const idMatches = ( testId : string ) =>
649- testId &&
650- ( testId === id ||
651- ( testId . startsWith ( id ) &&
652- / ^ _ s e a s o n _ ( a u t u m n | s p r i n g | s u m m e r | w i n t e r ) $ / . test (
653- testId . substring ( id . length ) ,
654- ) ) ) ;
655- for (
656- let chunkIdx = tileData [ "tiles-new" ] . length - 1 ;
657- chunkIdx >= 0 ;
658- chunkIdx --
659- ) {
660- const chunk = tileData [ "tiles-new" ] [ chunkIdx ] ;
661- for ( const info of chunk . tiles ) {
662- if (
663- Array . isArray ( info . id ) ? info . id . some ( idMatches ) : idMatches ( info . id )
664- ) {
665- let fg = Array . isArray ( info . fg ) ? info . fg [ 0 ] : info . fg ;
666- let bg = Array . isArray ( info . bg ) ? info . bg [ 0 ] : info . bg ;
667- if ( fg && typeof fg === "object" ) fg = fg . sprite ;
668- if ( bg && typeof bg === "object" ) bg = bg . sprite ;
669- return {
670- fg : tileInfoForId ( fg ) ,
671- bg : tileInfoForId ( bg ) ,
672- } ;
706+
707+ const exact = new Map < string , TileInfo > ( ) ;
708+
709+ for ( const chunk of tileData [ "tiles-new" ] ) {
710+ for ( let idx = chunk . tiles . length - 1 ; idx >= 0 ; idx -- ) {
711+ const entry = chunk . tiles [ idx ] ;
712+ const tileInfo = tileInfoForEntry ( entry ) ;
713+ for ( const entryId of Array . isArray ( entry . id ) ? entry . id : [ entry . id ] ) {
714+ exact . set ( entryId , tileInfo ) ;
673715 }
674716 }
675717 }
718+
719+ return exact ;
676720}
677721
678- export const MAX_INHERITANCE_DEPTH = 10 ;
722+ const MAX_INHERITANCE_DEPTH = 10 ;
679723
680724export function findTileOrLooksLike (
681725 data : CBNData ,
0 commit comments