Back to Outbreak: Soycoon City
SOURCE / PINNED RELEASE

Made of little things.

Outbreak: Soycoon City

Release
db7750b1d009…
Author-recorded commit
3ee3f5fd27a6…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9

Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/renderer.ts
import { AmbientLight, BoxGeometry, CanvasTexture, CircleGeometry, Color, CylinderGeometry, DirectionalLight, EdgesGeometry, Group, HemisphereLight, LineBasicMaterial, LineSegments, Mesh, MeshBasicMaterial, MeshLambertMaterial, NearestFilter, PerspectiveCamera, PlaneGeometry, PointLight, Scene, SphereGeometry, SRGBColorSpace, WebGLRenderer, type Material, type Object3D } from 'three';
import { signature, cutWall, type Door, type Room, type Camera, type Prop } from './world.js';
import soyModel from './models/soybert.glb?url';
import zombieModel from './models/zombie.glb?url';
import { CharacterAsset, decodeEmbeddedModel, type CharacterActor } from './character-model.js';
import type { Game } from './game.js';
import { addDressing } from './dressing.js';
const palette = { wall: 0x817969, panel: 0x304d47, iron: 0x404a45, wood: 0x69513a, box: 0x97866a, floor: 0x777467, cream: 0xffe8b2 };
const matte = (c: number) => new MeshLambertMaterial({ color: c, flatShading: true });
function cube(parent: Object3D, size: number[], pos: number[], material: Material): Mesh {
  const m = new Mesh(new BoxGeometry(...size as [number, number, number]), material);
  m.position.set(...pos as [number, number, number]); parent.add(m); return m;
}
function texture(canvas: HTMLCanvasElement): CanvasTexture {
  const t = new CanvasTexture(canvas); t.colorSpace = SRGBColorSpace; t.minFilter = NearestFilter; t.magFilter = NearestFilter; return t;
}
function canvas(w: number, h: number): [HTMLCanvasElement, CanvasRenderingContext2D] {
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  return [c, c.getContext('2d')!];
}
function floorTexture(room: Room): CanvasTexture {
  const [c, ctx] = canvas(512, 512);
  let seed = 31337;
  const rand = () => { seed = (seed * 16807) % 2147483647; return seed / 2147483647; };
  const tiles = Math.round(Math.max(room.w, room.d));
  const s = 512 / tiles;
  for (let z = 0; z < tiles; z++) for (let x = 0; x < tiles; x++) {
    ctx.fillStyle = (x + z) % 2 ? '#686858' : '#8d8975'; ctx.fillRect(x * s, z * s, s, s);
    ctx.strokeStyle = '#383d35'; ctx.lineWidth = 2; ctx.strokeRect(x * s, z * s, s, s);
    ctx.strokeStyle = '#b2a68a'; ctx.lineWidth = .7; ctx.strokeRect(x * s + 3, z * s + 3, s - 6, s - 6);
  }
  for (let i = 0; i < 8500; i++) { ctx.fillStyle = rand() > .5 ? '#171b1920' : '#dbcd9a14'; ctx.fillRect(rand() * 512, rand() * 512, rand() * 5, rand() * 3); }
  for (let i = 0; i < 30; i++) { const x = rand() * 512, y = rand() * 512; ctx.strokeStyle = '#24292380'; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + rand() * 25, y + 14); ctx.lineTo(x + 7, y + rand() * 45); ctx.stroke(); }
  return texture(c);
}
function wallTexture(): CanvasTexture {
  const [c, ctx] = canvas(128, 128); ctx.fillStyle = '#b6ad8c'; ctx.fillRect(0, 0, 128, 128);
  ctx.fillStyle = '#46675b'; ctx.fillRect(0, 70, 128, 58); ctx.fillStyle = '#333b31'; ctx.fillRect(0, 69, 128, 4);
  for (let i = 0; i < 160; i++) { ctx.fillStyle = '#423b2920'; ctx.fillRect((i * 37) % 128, (i * 53) % 128, 3 + i % 13, 1 + i % 12); }
  ctx.strokeStyle = '#e4d5a340'; for (let i = 1; i < 7; i++) { ctx.beginPath(); ctx.moveTo(i * 20, 0); ctx.lineTo(i * 20, 69); ctx.stroke(); }
  return texture(c);
}
function blob(): Mesh {
  const m = new Mesh(new CircleGeometry(.44, 12), new MeshBasicMaterial({ color: 0x080d0c, transparent: true, opacity: .46, depthWrite: false })); m.rotation.x = -Math.PI / 2; m.position.y = .012; return m;
}
function furnish(parent: Group, p: Prop, holding: boolean): void {
  const g = new Group(); g.position.set(p.x, 0, p.z); parent.add(g);
  const metal = matte(palette.iron), wood = matte(palette.wood);
  if (p.kind === 'shelf') {
    for (const a of [-1, 1]) for (const b of [-1, 1]) cube(g, [.06, p.h, .06], [a * (p.w / 2 - .04), p.h / 2, b * (p.d / 2 - .04)], metal);
    for (let j = 0; j < 4; j++) {
      const y = .13 + j * (p.h - .2) / 3; cube(g, [p.w, .055, p.d], [0, y, 0], metal);
      const longX = p.w > p.d; const n = Math.floor(Math.max(p.w, p.d) / .6);
      for (let k = 0; k < n; k++) {
        const along = (k + .5) * Math.max(p.w, p.d) / n - Math.max(p.w, p.d) / 2;
        const box = cube(g, [longX ? .48 : p.w * .8, .35, longX ? p.d * .8 : .48], [longX ? along : 0, y + .2, longX ? 0 : along], matte(k % 3 ? palette.box : 0x776e58));
        cube(box, [longX ? .18 : .01, .09, longX ? .01 : .18], [longX ? 0 : p.w * .4 + .002, 0, longX ? p.d * .4 + .002 : 0], matte(0xc4b799));
      }
    }
  } else if (p.kind === 'desk' || p.kind === 'bench') {
    cube(g, [p.w, .1, p.d], [0, p.h, 0], wood);
    for (const a of [-1, 1]) for (const b of [-1, 1]) cube(g, [.1, p.h, .1], [a * (p.w / 2 - .12), p.h / 2, b * (p.d / 2 - .12)], wood);
    if (p.kind === 'desk') { cube(g, [.55, .025, .4], [-.2, p.h + .065, 0], matte(0xd0c2a0)); cube(g, [.28, .2, .25], [.55, p.h + .15, 0], matte(0x333c34)); }
  } else if (p.kind === 'wall' && holding) {
    const n = Math.max(1, Math.floor(p.w / .28)); for (let i = 0; i <= n; i++) cube(g, [.045, p.h, .045], [-p.w / 2 + i * p.w / n, p.h / 2, 0], metal);
    for (const y of [.2, 1.2, p.h]) cube(g, [p.w, .055, .09], [0, y, 0], metal);
  } else { cube(g, [p.w, p.h, p.d], [0, p.h / 2, 0], p.kind === 'wall' ? matte(palette.wall) : wood); }
}
export class StageRenderer {
  renderer: WebGLRenderer;
  scene = new Scene();
  camera = new PerspectiveCamera(55, 4 / 3, .1, 80);
  environment = new Group(); actors = new Group();
  soyAsset=new CharacterAsset(decodeEmbeddedModel(soyModel)); undeadAsset=new CharacterAsset(decodeEmbeddedModel(zombieModel));
  playerActor=this.soyAsset.instantiate(); player=this.playerActor.root; playerShadow=blob(); lastTime=0;
  herbModels=new Map<string,Group>(); bursts=new Map<number,Group>(); shells=new Map<number,Mesh>(); lastShot=0;
  muzzleFlash=new Mesh(new SphereGeometry(.09,5,3),new MeshBasicMaterial({color:0xffdc82}));
  muzzle = new PointLight(0xffb45a, 0, 5);
  zombieModels = new Map<string, CharacterActor>();
  roomId = ''; geometryKey = ''; backgroundKey = ''; background: CanvasTexture | null = null;
  room?: Room; current?: Camera; originalMaterials = new Map<Mesh, Material | Material[]>();
  resolution = 480; useArt = true;
  art = new Map<string, { url: string; signature: string }>();
  private images = new Map<string, HTMLImageElement>();
  clearImages(): void { this.images.clear(); this.backgroundKey = ''; }
  constructor(element: HTMLElement) {
    this.renderer = new WebGLRenderer({ antialias: false, alpha: false, preserveDrawingBuffer: true });
    this.renderer.setPixelRatio(1); this.renderer.setSize(480, 360, false); this.renderer.setClearColor(0x111613);
    this.renderer.domElement.setAttribute('aria-label', 'Fixed camera game scene'); element.append(this.renderer.domElement);
    this.scene.add(this.environment, this.actors, new AmbientLight(0xc7bfa2, .65), new HemisphereLight(0xb8ccb2, 0x27271c, 1.45));
    const key = new DirectionalLight(0xffeac3, 2.4); key.position.set(-2, 6, 4); this.scene.add(key);
    const socket=this.player.getObjectByName('Muzzle')!;socket.add(this.muzzle,this.muzzleFlash);this.muzzleFlash.visible=false;
    this.player.add(this.playerShadow); this.actors.add(this.player);
  }
  loadRoom(room: Room): void {
    const key = JSON.stringify([room.id, room.w, room.d, room.props, room.doors, room.dressing]);
    if (key === this.geometryKey) return;
    this.geometryKey = key; this.roomId = room.id; this.room = room; this.backgroundKey = '';
    this.disposeGroup(this.environment); this.environment.clear(); this.originalMaterials.clear();
    const floor = new Mesh(new PlaneGeometry(room.w, room.d), new MeshLambertMaterial({ map: floorTexture(room) })); floor.rotation.x = -Math.PI / 2; this.environment.add(floor);
    const ceiling = new Mesh(new PlaneGeometry(room.w, room.d), matte(0x656756)); ceiling.rotation.x = Math.PI / 2; ceiling.position.y = 3.2; ceiling.name = 'ceiling'; this.environment.add(ceiling);
    const wallMat = new MeshLambertMaterial({ map: wallTexture(), color: 0xc6c1ad });
    const addWall = (side: string, length: number, coord: number, horizontal: boolean) => {
      const group = new Group(); group.name = side;
      group.position.set(horizontal ? 0 : coord, 0, horizontal ? coord : 0);
      if (!horizontal) group.rotation.y = Math.PI / 2;
      const doors = room.doors.filter(d => d.side === side).sort((a, b) => (horizontal ? a.x - b.x : b.z - a.z));
      let start = -length / 2;
      for (const door of doors) {
        const along = horizontal ? door.x : -door.z;
        if (along - .65 > start) cube(group, [along - .65 - start, 3.2, .12], [(start + along - .65) / 2, 1.6, 0], wallMat);
        cube(group, [1.3, 1, .12], [along, 2.7, 0], wallMat);
        cube(group, [1.2, 2.15, .1], [along, 1.075, 0], matte(0x414a3e));
        cube(group, [.1, 2.3, .18], [along - .65, 1.15, 0], matte(0x827253)); cube(group, [.1, 2.3, .18], [along + .65, 1.15, 0], matte(0x827253));
        const [c, x] = canvas(128, 32); x.fillStyle = '#182d27'; x.fillRect(0, 0, 128, 32); x.fillStyle = '#e9d5a4'; x.font = 'bold 13px monospace'; x.textAlign = 'center'; x.fillText('S.C.P.D.', 64, 21);
        for (const sign of [-1, 1]) { const m = new Mesh(new PlaneGeometry(.9, .225), new MeshBasicMaterial({ map: texture(c) })); m.position.set(along, 2.45, sign * .1); if (sign < 0) m.rotation.y = Math.PI; group.add(m); }
        start = along + .65;
      }
      if (length / 2 > start) cube(group, [length / 2 - start, 3.2, .12], [(start + length / 2) / 2, 1.6, 0], wallMat);
      cube(group, [length, .12, .15], [0, .08, 0], matte(0x28392f)); this.environment.add(group);
    };
    addWall('north', room.w, -room.d / 2, true); addWall('south', room.w, room.d / 2, true);
    addWall('west', room.d, -room.w / 2, false); addWall('east', room.d, room.w / 2, false);
    for (const p of room.props) furnish(this.environment, p, room.id === 'holding');
    addDressing(this.environment, room);
    for (const x of [-room.w * .22, room.w * .22]) { const fixture = cube(this.environment, [1.3, .06, .15], [x, 3, -room.d * .25], new MeshBasicMaterial({ color: 0xe4e7c1 })); fixture.userData.lightFixture = true; }
    const lamp = new PointLight(0xd7dec2, 15, 10, 2); lamp.position.set(0, 2.8, -1); this.environment.add(lamp);
    const cool = new PointLight(0x7cabb4, 9, 8, 2); cool.position.set(-room.w * .35, 2.1, -room.d * .32); this.environment.add(cool);
    this.environment.traverse(o => {
      if (o instanceof Mesh) {
        // Occluders must write depth before any actor; writing depth after an
        // actor has drawn cannot erase that actor from the baked background.
        o.renderOrder = -1000;
        this.originalMaterials.set(o, o.material);
      }
    });
  }
  setCamera(c: Camera): void {
    this.current = c; this.camera.position.set(...c.position); this.camera.lookAt(...c.target); this.camera.fov = c.fov; this.camera.updateProjectionMatrix();
    const ceiling = this.environment.getObjectByName('ceiling'); if (ceiling) ceiling.visible = c.position[1] < 3.1;
    for (const side of ['north', 'south', 'west', 'east']) {
      const wall = this.environment.getObjectByName(side);
      if (wall) wall.visible = !cutWall(c,side as Door['side']);
    }
  }
  private materials(schema = false, depth = false): void {
    for (const [mesh, mat] of this.originalMaterials) {
      // Lighting is described by the art prompt. Floating rods in an overhead
      // schema look like floor planks and are not actor occluders.
      mesh.visible = !schema || !mesh.userData.lightFixture;
      const transient = mesh.userData.transient as Material | undefined;
      if (transient) { transient.dispose(); delete mesh.userData.transient; }
      if (schema || depth) {
        const material = schema ? new MeshBasicMaterial({ color: mesh.geometry instanceof PlaneGeometry ? 0xd8dedb : 0xb4c0bc, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 }) : new MeshBasicMaterial({ colorWrite: false, depthWrite: !mesh.userData.lightFixture });
        mesh.material = material; mesh.userData.transient = material;
      } else mesh.material = mat;
    }
  }
  private bake(room: Room, c: Camera): void {
    const key = signature(room, c) + String(this.useArt);
    if (this.backgroundKey === key) return;
    this.backgroundKey = key;
    this.background?.dispose(); this.background = null; this.scene.background = null;
    this.materials(); this.actors.visible = false;
    this.renderer.setSize(640, 480, false); this.renderer.render(this.scene, this.camera);
    const [b, x] = canvas(640, 480); x.drawImage(this.renderer.domElement, 0, 0);
    this.background = texture(b); this.scene.background = this.background;
    const built = this.art.get(`${room.id}/${c.id}`);
    const url = c.backdrop && c.artSignature === signature(room, c) ? c.backdrop : built?.signature === signature(room, c) ? built.url : undefined;
    if (this.useArt && url) {
      let img = this.images.get(url);
      if (!img) { img = new Image(); this.images.set(url, img); img.onload = () => { this.backgroundKey = ''; }; img.src = url; }
      if (img.complete && img.naturalWidth) {
        x.drawImage(img, 0, 0, 640, 480);
        // A mild ordered palette reduction gives the baked image the same
        // restrained 16-bit texture treatment as the low-poly actors.
        const pixels = x.getImageData(0, 0, 640, 480);
        const bayer = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
        for (let y = 0; y < 480; y++) for (let xx = 0; xx < 640; xx++) {
          const i = (y * 640 + xx) * 4, threshold = (bayer[(y % 4) * 4 + xx % 4] - 7.5) * .3;
          for (let channel = 0; channel < 3; channel++) pixels.data[i + channel] = Math.round((pixels.data[i + channel] + threshold) / 8) * 8;
        }
        x.putImageData(pixels, 0, 0); this.background.needsUpdate = true;
      }
    }
    this.materials(false, true); this.actors.visible = true;
    this.renderer.setSize(this.resolution, this.resolution * .75, false);
  }
  render(game: Game): void {
    this.loadRoom(game.room); this.setCamera(game.camera); this.bake(game.room, game.camera);
    const dt=Math.max(0,Math.min(.05,game.time-this.lastTime));this.lastTime=game.time;
    this.player.position.set(game.player.x,0,game.player.z); this.player.rotation.y=game.player.yaw;
    const shotAge=game.time-game.shotAt;
    if(game.player.hp<=0)this.playerActor.play('Death',true,game.deathAt,game.time-game.deathAt);
    else if(game.reloadTime>0)this.playerActor.play('Reload',true);
    else if(shotAge<.55)this.playerActor.play(game.shotUp?'ShootUp':'Shoot',true,game.shotSerial,shotAge);
    else if(game.hurt>0)this.playerActor.play('Hit',true);
    else this.playerActor.play(game.moving ? game.aiming ? game.aimUp?'WalkAimUp':'WalkAim' : game.running?'Run':'Walk' : game.aiming?game.aimUp?'AimUp':'Aim':'Idle');
    this.playerActor.update(dt);this.muzzle.intensity=game.flash>0?24:0;this.muzzleFlash.visible=game.flash>0;this.muzzleFlash.scale.setScalar(.8+game.flash*6);
    for(const z of game.zombies){
      let actor=this.zombieModels.get(z.id);
      if(!actor){actor=this.undeadAsset.instantiate();actor.root.add(blob());this.zombieModels.set(z.id,actor);this.actors.add(actor.root);}
      actor.root.visible=z.room===game.room.id;actor.root.position.set(z.x,0,z.z);actor.root.rotation.y=z.yaw;
      const head=actor.root.getObjectByName('HeadMesh');if(head)head.visible=!z.headGone;
      if(z.hp<=0)actor.play('Death',true,z.deathAt??-1,z.deathAt===undefined?2:game.time-z.deathAt);
      else if(z.hitAt!==undefined&&game.time-z.hitAt<.45)actor.play('Hit',true,z.hitAt,game.time-z.hitAt);
      else if(z.attackAt!==undefined&&game.time-z.attackAt<.85)actor.play('Attack',true,z.attackAt,game.time-z.attackAt);
      else actor.play(z.moving?'Walk':'Idle');actor.update(dt);
    }
    for(const h of game.herbs){let g=this.herbModels.get(h.id);if(!g){g=new Group();const pot=new Mesh(new CylinderGeometry(.11,.075,.13,7),matte(0x826044));pot.position.y=.07;g.add(pot);
        for(let i=0;i<7;i++){const leaf=new Mesh(new SphereGeometry(1,4,3),matte(i%2?0x5fa845:0x8ccc69));leaf.scale.set(.055,.14,.035);leaf.position.set(Math.sin(i*2.4)*.095,.19,Math.cos(i*2.4)*.095);leaf.rotation.set(Math.cos(i*2.4)*.6,0,-Math.sin(i*2.4)*.6);g.add(leaf);}g.add(blob());g.position.set(h.x,.015,h.z);this.herbModels.set(h.id,g);this.actors.add(g);}g.visible=!h.collected&&h.room===game.room.id;}
    for(const burst of game.bursts){let g=this.bursts.get(burst.id);if(!g){g=new Group();g.position.set(burst.x,1.78,burst.z);for(let i=0;i<22;i++){const fragment=new Mesh(new SphereGeometry(.02+i%3*.01,4,3),matte(i%4?0x8e1615:0x7a8565));const angle=i*2.399;fragment.userData.velocity=[Math.sin(angle)*(.7+i%4*.16),.6+i%5*.23,Math.cos(angle)*(.7+i%4*.16)];g.add(fragment);}this.bursts.set(burst.id,g);this.actors.add(g);}const age=game.time-burst.at;g.visible=burst.room===game.room.id;for(const fragment of g.children){const [x,y,z]=fragment.userData.velocity;fragment.position.set(x*age,Math.max(-1.75,y*age-3.5*age*age),z*age);fragment.rotation.set(age*9,age*4,age*7);fragment.scale.setScalar(Math.max(.05,1-age/1.8));}}
    for(const [id,g] of this.bursts)if(!game.bursts.some(b=>b.id===id)){this.disposeGroup(g);g.removeFromParent();this.bursts.delete(id);}
    if(game.shotSerial!==this.lastShot){this.lastShot=game.shotSerial;if(game.shotSerial){const shell=new Mesh(new CylinderGeometry(.018,.018,.055,5),matte(0xb39348));shell.position.set(game.player.x,1.13,game.player.z);shell.userData={at:game.shotAt,room:game.room.id,x:game.player.x,z:game.player.z,yaw:game.player.yaw};this.shells.set(game.shotSerial,shell);this.actors.add(shell);}}
    for(const [id,shell] of this.shells){const age=game.time-shell.userData.at;if(age>2){shell.geometry.dispose();(shell.material as Material).dispose();shell.removeFromParent();this.shells.delete(id);continue;}shell.visible=shell.userData.room===game.room.id;shell.position.set(shell.userData.x+Math.cos(shell.userData.yaw)*Math.min(age,.65)*1.3,Math.max(.04,1.13+age*.9-3.5*age*age),shell.userData.z-Math.sin(shell.userData.yaw)*Math.min(age,.65)*1.3);shell.rotation.set(age*10,age*5,age*7);}
    this.renderer.render(this.scene, this.camera);
  }
  preview(room: Room, c: Camera, schema: boolean): void {
    this.loadRoom(room); this.setCamera(c);
    this.actors.visible = false;
    if (!schema) { this.bake(room, c); this.actors.visible = false; this.renderer.render(this.scene, this.camera); return; }
    this.scene.background = new Color(0xf0f3f1); this.materials(true);
    const edges: LineSegments[] = [];
    for (const mesh of this.originalMaterials.keys()) { const e = new LineSegments(new EdgesGeometry(mesh.geometry), new LineBasicMaterial({ color: 0x273d38 })); mesh.add(e); edges.push(e); }
    this.renderer.setSize(640, 480, false); this.renderer.render(this.scene, this.camera);
    for (const e of edges) { e.removeFromParent(); e.geometry.dispose(); (e.material as Material).dispose(); }
    this.materials(false, true); this.backgroundKey = '';
  }
  schemaShot(room: Room, c: Camera): string {
    const old = this.resolution; this.resolution = 640; this.preview(room, c, true);
    const data = this.renderer.domElement.toDataURL('image/png'); this.resolution = old; this.backgroundKey = ''; return data;
  }
  private disposeGroup(group: Group): void {
    const mats = new Set<Material>();
    group.traverse(o => {
      if (o instanceof Mesh) {
        o.geometry.dispose();
        for (const m of (Array.isArray(o.material) ? o.material : [o.material])) mats.add(m);
        const original = this.originalMaterials.get(o);
        if (original) for (const m of (Array.isArray(original) ? original : [original])) mats.add(m);
      }
    });
    for (const m of mats) { if ('map' in m) (m.map as CanvasTexture | null)?.dispose(); m.dispose(); }
  }
  clearEnemies(): void {
    for(const actor of this.zombieModels.values()){const shadow=actor.root.children.find(c=>c instanceof Mesh);if(shadow instanceof Mesh){shadow.geometry.dispose();(shadow.material as Material).dispose();}actor.dispose();}this.zombieModels.clear();
    for(const g of [...this.herbModels.values(),...this.bursts.values()]){this.disposeGroup(g);g.removeFromParent();}this.herbModels.clear();this.bursts.clear();for(const m of this.shells.values()){m.geometry.dispose();(m.material as Material).dispose();m.removeFromParent();}this.shells.clear();this.lastTime=0;this.lastShot=0;this.playerActor.mixer.stopAllAction();this.playerActor.current='';this.playerActor.play('Idle');this.playerActor.update(0);
  }
  dispose(): void { this.clearEnemies();this.playerShadow.geometry.dispose();(this.playerShadow.material as Material).dispose();this.playerActor.dispose();this.soyAsset.dispose();this.undeadAsset.dispose();this.muzzleFlash.geometry.dispose();(this.muzzleFlash.material as Material).dispose();this.background?.dispose(); this.disposeGroup(this.environment); this.disposeGroup(this.actors); this.renderer.dispose(); }
}