Skip to content

Lighting ​

clientside

Mytharyn has a tile-based lighting system: a script darkens the world with an ambient light level, and light sources attached to NPCs brighten it again — torches, lamps, lighthouses, glowing pickups. Walls (blocking tiles) absorb light, so lights cast soft shadows.

Lighting is entirely clientside. Each client computes its own lightmap; nothing is sent over the network. Two pieces work together:

Ambient light ​

setambient(r, g, b) sets the ambient light level, 0-255 per channel. At the default 255,255,255 lighting is off — the lighting pass doesn't run at all, so levels without lighting look and cost exactly the same as before. Anything below full multiplies the world by that color and lets light sources show through. resetambient() restores full light.

ts
// example: weapons/lighttest.client.ts
export function onPlayerChats(who: ChatPlayer, chat: string): void {
    if (who.id !== player.id)
        return
    if (chat === '/night')
        setambient(25, 25, 55)       // dark, slightly blue moonlight
    else if (chat === '/day')
        resetambient()
}

The channels are independent, so ambient light also tints the world: a warm dusk (200,140,110), a cold cave (40,50,70). A day/night cycle is just setambient called periodically with an interpolated color:

ts
// weapons/daynight.client.ts — one full cycle every 10 minutes
const CYCLE = 600
const NIGHT = [30, 30, 60]

export function onCreated() {
    this.t = 0
    this.acc = 0
}

export function onUpdate(dt: number) {
    this.t = (this.t + dt) % CYCLE
    this.acc += dt
    if (this.acc < 0.5) return          // no need to update every frame
    this.acc = 0
    const day = (Math.cos(this.t / CYCLE * 2 * Math.PI) + 1) / 2   // 1 = noon, 0 = midnight
    setambient(
        NIGHT[0] + (255 - NIGHT[0]) * day,
        NIGHT[1] + (255 - NIGHT[1]) * day,
        NIGHT[2] + (255 - NIGHT[2]) * day)
}

Both calls are batched like other client commands. The ambient level is session state, not level state:

  • It persists across level changes (like setfocus), so set it once rather than on every level entry.
  • It resets to full light when a weapon is removed from the player, so a weapon that's taken away can't leave the world stuck in darkness. Values outside 0-255 are clamped.

Per-area darkness

Because ambient light is global, "this dungeon is dark" is a script decision: have a weapon watch player.level (or an event such as a level change) and call setambient / resetambient accordingly.

Light sources ​

Only NPCs can be light sources. In a clientside NPC script (or a class the NPC joins), call drawaslight() and configure the light with properties on this:

ts
// clientside half of a torch NPC
export function onCreated() {
    this.lightcolor = '255,170,80'   // warm orange
    this.lightradius = 6             // tiles
    this.lightintensity = 1
    this.drawaslight()
}

export function onUpdate() {
    // flicker
    this.lightintensity = 0.9 + Math.random() * 0.2
}
PropertyDefaultMeaning
lightcolor'255,255,255'Light color, 'r,g,b' 0-255.
lightintensity1Brightness at the source. Above 1 overdrives the core so full brightness reaches further out (max 10). 0 turns the light off.
lightradius8Distance in tiles at which the light fades out in open air (max 48).
lightshape'circle''circle', 'diamond', 'square', 'cone' or 'ring'.
lightdir0Cone aim in degrees, clockwise: 0 = up, 90 = right.
lightarc90Cone width in degrees (the full wedge, max 360).
lightmode00 = visible only in darkness; 1 = also an additive glow, visible by day.

Things to know:

  • drawaslight() is one-way, like showCharacter(). There's no "stop being a light" call — set lightintensity = 0 to switch it off, and back up to switch it on again.
  • The light follows the NPC. It is centered on the NPC's shape (or its image) and moves with this.x/this.y, so a moving NPC carries its light along.
  • Walls cast shadows. Blocking (type-22) tiles absorb light far faster than open air, for every shape. Areas outside the level count as solid, so light doesn't leak into the void.
  • Overlapping lights don't over-brighten: where two lights overlap, the brighter one wins per channel rather than adding up.
  • Light properties are per client. Each player's client runs the NPC's clientside script, so every player sees the same lights as long as the script sets them deterministically.

Shapes ​

  • 'circle' — a round glow (the default).
  • 'diamond' — pointed on the axes.
  • 'square' — even brightness out to a square edge.
  • 'cone' — a directional wedge aimed with lightdir, lightarc degrees wide. Animate lightdir for a lighthouse beam or a flashlight.
  • 'ring' — a glowing band at about two-thirds of the radius with a dark center.
ts
// a rotating lighthouse beam
export function onCreated() {
    this.lightshape = 'cone'
    this.lightarc = 40
    this.lightradius = 20
    this.lightcolor = '255,250,200'
    this.drawaslight()
}

export function onUpdate(dt: number) {
    this.lightdir = (this.lightdir + dt * 45) % 360    // 45 degrees per second
}

Light modes ​

By default (lightmode = 0) a light only shows where setambient has darkened the world — at full ambient it's invisible. That's what you want for torches and lamps that "turn on" at night.

With lightmode = 1 the light is also drawn as an additive glow on top of the scene, the same by day or night, so it stays visible at full ambient; at night it still lights the tiles under it like mode 0. Use it for magical glows, pickups and effects that should shine in daylight. Additive light saturates quickly — keep lightintensity low (about 0.3-0.6) for a subtle glow:

ts
// example: classes/flare.client.ts — an orange glow visible any time of day
export function onCreated() {
    this.lightshape = 'circle'
    this.lightmode = 1
    this.lightintensity = 0.25
    this.lightcolor = '255,100,0'
    this.drawaslight()
}

What gets lit ​

The lightmap is applied over the whole world pass: tiles, name tags, NPCs, players, projectiles, world-space script images and particles are all darkened by ambient light and brightened by light sources. Screen-space images (screen = true), chat bubbles and GUI controls draw in later passes and are never lit — which is what you want for HUDs.

Glowing effects at night

World-space images and particles are darkened like everything else. For a spark or spell effect that should glow in the dark, pair it with a lightmode = 1 NPC light, or use additive particles (particle.mode = 0) on top of a light source.

See the Day/night & torches tutorial for a complete setup.