@ -3,7 +3,7 @@ import { TILE_WIDTH, TILE_HEIGHT, MAP_ZOOM } from '../shared/constants';
import { getViewport } from '../shared/telegram' ;
import type { Camera } from './camera' ;
import type { EnemyType } from './types' ;
import type { TownData , NPCData } from './types' ;
import type { TownData , NPCData , BuildingData } from './types' ;
import { drawEnemyByType } from './enemyVisuals' ;
/ * *
@ -211,6 +211,31 @@ export class GameRenderer {
gfx . fill ( { color : 0x6a3a8e , alpha : 0.92 } ) ;
}
private _drawBarrel ( gfx : Graphics , x : number , y : number , variant : number ) : void {
const s = ( 0.9 + variant * 0.15 ) * 2.8 ;
gfx . ellipse ( x , y , 5 * s , 3 * s ) ;
gfx . fill ( { color : 0x5a4228 , alpha : 0.9 } ) ;
gfx . rect ( x - 5 * s , y - 7 * s , 10 * s , 7 * s ) ;
gfx . fill ( { color : 0x6b4a30 , alpha : 0.92 } ) ;
gfx . ellipse ( x , y - 7 * s , 5 * s , 3 * s ) ;
gfx . fill ( { color : 0x7a5a3a , alpha : 0.9 } ) ;
gfx . rect ( x - 5 * s , y - 5 * s , 10 * s , 1 * s ) ;
gfx . fill ( { color : 0x4a3218 , alpha : 0.6 } ) ;
gfx . rect ( x - 5 * s , y - 2 * s , 10 * s , 1 * s ) ;
gfx . fill ( { color : 0x4a3218 , alpha : 0.6 } ) ;
}
private _drawLeafPile ( gfx : Graphics , x : number , y : number , variant : number ) : void {
const s = ( 0.85 + variant * 0.25 ) * 2.5 ;
const colors = [ 0x6a8a2a , 0x8a7a22 , 0x5a7a28 , 0x9a8a30 ] ;
for ( let i = 0 ; i < 4 ; i ++ ) {
const ox = ( ( ( variant * 100 + i * 37 ) | 0 ) % 7 - 3 ) * s ;
const oy = ( ( ( variant * 100 + i * 53 ) | 0 ) % 5 - 2 ) * s * 0.5 ;
gfx . ellipse ( x + ox , y + oy , 4 * s , 2.5 * s ) ;
gfx . fill ( { color : colors [ i % colors . length ] ! , alpha : 0.7 } ) ;
}
}
private _terrainColors ( terrain : string , dark : boolean ) : number {
if ( terrain === 'plaza' ) return dark ? 0x5a5a62 : 0x6c6c75 ;
if ( terrain === 'road' ) return dark ? 0x7b6545 : 0x8e7550 ;
@ -459,6 +484,8 @@ export class GameRenderer {
else if ( obj === 'stall' ) this . _drawMarketStall ( gfx , iso . x , iso . y , variant ) ;
else if ( obj === 'well' ) this . _drawWell ( gfx , iso . x , iso . y , variant ) ;
else if ( obj === 'banner' ) this . _drawBanner ( gfx , iso . x , iso . y , variant ) ;
else if ( obj === 'barrel' ) this . _drawBarrel ( gfx , iso . x , iso . y , variant ) ;
else if ( obj === 'leaves' ) this . _drawLeafPile ( gfx , iso . x , iso . y , variant ) ;
}
}
}
@ -775,6 +802,140 @@ export class GameRenderer {
gfx . fill ( { color : 0x44aa88 , alpha : 0.7 } ) ;
}
/ * *
* Draw server - defined buildings for a town . Each building type gets a distinct
* visual style so players can identify NPC houses at a glance .
* /
private _drawServerBuildings (
gfx : Graphics ,
buildings : BuildingData [ ] ,
_townScreenX : number ,
_townScreenY : number ,
scale : number ,
) : void {
for ( let i = 0 ; i < buildings . length ; i ++ ) {
const b = buildings [ i ] ! ;
const bScreen = worldToScreen ( b . worldX , b . worldY ) ;
const bx = bScreen . x ;
const by = bScreen . y ;
const w = 60 * scale * ( b . footprintW / 2.5 ) ;
const h = 48 * scale * ( b . footprintH / 2.0 ) ;
const rh = 32 * scale ;
const bt = b . buildingType ;
if ( bt === 'house.quest_giver' ) {
this . _drawHouse ( gfx , bx , by , w , h , rh , 0xb89040 , 0x6a3a22 , 0 ) ;
this . _drawFence ( gfx , bx , by , w , 'left' ) ;
this . _drawBuildingIcon ( gfx , bx , by - h - rh * 0.5 , '!' , 0xffd700 , scale ) ;
} else if ( bt === 'house.merchant' ) {
this . _drawHouse ( gfx , bx , by , w * 1.1 , h , rh * 0.8 , 0x44aa55 , 0x2a5a30 , 1 ) ;
this . _drawTownStall ( gfx , bx + w * 0.7 , by + 4 , scale * 0.6 ) ;
this . _drawBuildingIcon ( gfx , bx , by - h - rh * 0.3 , '$' , 0x88dd88 , scale ) ;
} else if ( bt === 'house.healer' ) {
this . _drawHouse ( gfx , bx , by , w , h , rh , 0xccccdd , 0x5555aa , 2 ) ;
this . _drawBuildingIcon ( gfx , bx , by - h - rh * 0.5 , '+' , 0xff6666 , scale ) ;
} else if ( bt === 'decoration.well' ) {
this . _drawTownWell ( gfx , bx , by , scale ) ;
} else if ( bt === 'decoration.stall' ) {
this . _drawTownStall ( gfx , bx , by , scale * 0.9 ) ;
} else if ( bt === 'decoration.signpost' ) {
this . _drawSignpost ( gfx , bx , by , scale ) ;
}
}
}
/** Draw a small icon circle above a building to indicate its purpose. */
private _drawBuildingIcon (
gfx : Graphics , cx : number , cy : number , _icon : string , color : number , scale : number ,
) : void {
const r = 6 * scale ;
gfx . circle ( cx , cy , r ) ;
gfx . fill ( { color , alpha : 0.6 } ) ;
gfx . stroke ( { color : 0x000000 , width : 1.2 , alpha : 0.4 } ) ;
}
/** Draw a town well decoration (server-driven building). */
private _drawTownWell ( gfx : Graphics , cx : number , cy : number , s : number ) : void {
gfx . ellipse ( cx , cy , 10 * s , 5 * s ) ;
gfx . fill ( { color : 0x6a6a7a , alpha : 0.8 } ) ;
gfx . stroke ( { color : 0x4a4a5a , width : 1.5 , alpha : 0.6 } ) ;
gfx . rect ( cx - 1 * s , cy - 12 * s , 2 * s , 12 * s ) ;
gfx . fill ( { color : 0x5a4a3a , alpha : 0.9 } ) ;
gfx . rect ( cx - 6 * s , cy - 13 * s , 12 * s , 2 * s ) ;
gfx . fill ( { color : 0x5a4a3a , alpha : 0.9 } ) ;
}
/** Draw a signpost decoration. */
private _drawSignpost ( gfx : Graphics , cx : number , cy : number , s : number ) : void {
gfx . rect ( cx - 1 * s , cy - 16 * s , 2 * s , 16 * s ) ;
gfx . fill ( { color : 0x6a5a3a , alpha : 0.9 } ) ;
gfx . poly ( [
cx + 2 * s , cy - 14 * s ,
cx + 12 * s , cy - 13 * s ,
cx + 12 * s , cy - 10 * s ,
cx + 2 * s , cy - 9 * s ,
] ) ;
gfx . fill ( { color : 0x8a7a5a , alpha : 0.85 } ) ;
}
/ * *
* Fallback procedural building placement when server buildings are unavailable .
* /
private _drawProceduralBuildings (
gfx : Graphics , tx : number , ty : number , s : number ,
spread : number , size : string , townSeed : number ,
) : void {
const houseCount = size === 'XS' ? 5 : size === 'S' ? 7 : size === 'M' ? 10 : 14 ;
const wallColors = [ 0x9a7e5a , 0x8b7252 , 0xa08860 , 0x7e6844 , 0x907656 , 0x9e8862 , 0x887050 ] ;
const roofColors = [ 0x6a3a22 , 0x5a3020 , 0x7a4028 , 0x5e3422 , 0x6e3a24 , 0x724030 , 0x603828 ] ;
const baseW = 60 ;
const baseH = 48 ;
const baseRH = 32 ;
for ( let i = 0 ; i < houseCount ; i ++ ) {
const hash = ( ( townSeed * 31 + i * 17 ) ^ ( i * 0x45d9f3b ) ) >>> 0 ;
const r1 = ( hash & 0xffff ) / 0xffff ;
const r2 = ( ( hash >> 16 ) & 0xffff ) / 0xffff ;
const r3 = ( ( hash * 7 + i * 13 ) & 0xff ) / 0xff ;
const angle = ( i / houseCount ) * Math . PI * 2 + r1 * 0.4 ;
const dist = spread * ( 0.2 + r2 * 0.65 ) ;
const dx = Math . cos ( angle ) * dist ;
const dy = Math . sin ( angle ) * dist * 0.5 ;
const sizeVar = 0.7 + r3 * 0.5 ;
const w = baseW * s * sizeVar ;
const h = baseH * s * sizeVar ;
const rh = baseRH * s * sizeVar ;
const roofStyle = i % 5 === 0 ? 1 : i % 3 === 0 ? 2 : 0 ;
this . _drawHouse (
gfx , tx + dx , ty + dy , w , h , rh ,
wallColors [ i % wallColors . length ] ! ,
roofColors [ i % roofColors . length ] ! ,
roofStyle ,
) ;
if ( i % 4 === 1 ) {
this . _drawFence ( gfx , tx + dx , ty + dy , w , i % 2 === 0 ? 'left' : 'right' ) ;
}
}
const stallCount = houseCount >= 10 ? 2 : 1 ;
for ( let si = 0 ; si < stallCount ; si ++ ) {
const stallAngle = ( si + 0.5 ) * Math . PI + ( townSeed & 0xf ) * 0.1 ;
const stallDist = spread * 0.35 ;
this . _drawTownStall (
gfx ,
tx + Math . cos ( stallAngle ) * stallDist ,
ty + Math . sin ( stallAngle ) * stallDist * 0.5 ,
s * 0.9 ,
) ;
}
}
/ * *
* Draw towns visible in the current viewport .
* Each town renders a ground plane , a large cluster of buildings with detail ,
@ -839,77 +1000,14 @@ export class GameRenderer {
gfx . circle ( tx , ty , borderRadius * 0.6 ) ;
gfx . fill ( { color : 0xdaa520 , alpha : 0.04 } ) ;
// --- Building cluster: many houses spread wide ---
const houseCount =
town . size === 'XS' ? 5 :
town . size === 'S' ? 7 :
town . size === 'M' ? 10 : 14 ;
// Generate house positions spread across a wider area
const housePositions : Array < { dx : number ; dy : number ; w : number ; h : number ; rh : number ; roofStyle : number ; fence : boolean ; stall : boolean } > = [ ] ;
const spread = 100 * s ;
const baseW = 60 ;
const baseH = 48 ;
const baseRH = 32 ;
// Seed pseudo-random from town id for deterministic layout
// --- Buildings: server-driven if available, fallback procedural ---
const townSeed = typeof town . id === 'number' ? town.id : 0 ;
for ( let i = 0 ; i < houseCount ; i ++ ) {
// Deterministic pseudo-random using a simple hash
const hash = ( ( townSeed * 31 + i * 17 ) ^ ( i * 0x45d9f3b ) ) >>> 0 ;
const r1 = ( ( hash & 0xffff ) / 0xffff ) ;
const r2 = ( ( ( hash >> 16 ) & 0xffff ) / 0xffff ) ;
const r3 = ( ( hash * 7 + i * 13 ) & 0xff ) / 0xff ;
// Angle-based layout to fill the town area
const angle = ( i / houseCount ) * Math . PI * 2 + r1 * 0.4 ;
const dist = spread * ( 0.2 + r2 * 0.65 ) ;
const dx = Math . cos ( angle ) * dist ;
const dy = Math . sin ( angle ) * dist * 0.5 ; // isometric compression
const sizeVar = 0.7 + r3 * 0.5 ;
const w = baseW * s * sizeVar ;
const h = baseH * s * sizeVar ;
const rh = baseRH * s * sizeVar ;
const roofStyle = i % 5 === 0 ? 1 : i % 3 === 0 ? 2 : 0 ;
const fence = i % 4 === 1 ;
const stall = false ; // stalls are added separately
housePositions . push ( { dx , dy , w , h , rh , roofStyle , fence , stall } ) ;
}
const wallColors = [ 0x9a7e5a , 0x8b7252 , 0xa08860 , 0x7e6844 , 0x907656 , 0x9e8862 , 0x887050 ] ;
const roofColors = [ 0x6a3a22 , 0x5a3020 , 0x7a4028 , 0x5e3422 , 0x6e3a24 , 0x724030 , 0x603828 ] ;
for ( let i = 0 ; i < housePositions . length ; i ++ ) {
const hp = housePositions [ i ] ! ;
this . _drawHouse (
gfx ,
tx + hp . dx ,
ty + hp . dy ,
hp . w ,
hp . h ,
hp . rh ,
wallColors [ i % wallColors . length ] ! ,
roofColors [ i % roofColors . length ] ! ,
hp . roofStyle ,
) ;
if ( hp . fence ) {
this . _drawFence ( gfx , tx + hp . dx , ty + hp . dy , hp . w , i % 2 === 0 ? 'left' : 'right' ) ;
}
}
const spread = 100 * s ;
// Add 1-2 market stalls per town (larger towns get 2)
const stallCount = houseCount >= 10 ? 2 : 1 ;
for ( let si = 0 ; si < stallCount ; si ++ ) {
const stallAngle = ( si + 0.5 ) * Math . PI + ( townSeed & 0xf ) * 0.1 ;
const stallDist = spread * 0.35 ;
this . _drawTownStall (
gfx ,
tx + Math . cos ( stallAngle ) * stallDist ,
ty + Math . sin ( stallAngle ) * stallDist * 0.5 ,
s * 0.9 ,
) ;
if ( town . buildings && town . buildings . length > 0 ) {
this . _drawServerBuildings ( gfx , town . buildings , tx , ty , s ) ;
} else {
this . _drawProceduralBuildings ( gfx , tx , ty , s , spread , town . size , townSeed ) ;
}
// --- Town name label (larger font, positioned higher) ---