<|start_filename|>client/src/blood-effect.js<|end_filename|>
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js';

import {particle_system} from "./particle-system.js";
import {entity} from "./entity.js";

export const blood_effect = (() => {

  class BloodEffectEmitter extends particle_system.ParticleEmitter {
    constructor(parent) {
      super();
      this.parent_ = parent;
      this.blend_ = 0.0;
    }

    OnUpdate_() {
      this.parent_.updateMatrixWorld(true);

      this.SetEmissionRate(300.0 * (this.emitterLife_ / 3.0));
    }

    CreateParticle_() {
      const origin = new THREE.Vector3(0, 0, 0);
      this.parent_.localToWorld(origin);

      const radius = 1.0;
      const life = (Math.random() * 0.75 + 0.25) * 0.5;
      const p = new THREE.Vector3(
          (Math.random() * 2 - 1) * radius,
          (Math.random() * 2 - 1) * radius,
          (Math.random() * 2 - 1) * radius);

      const d = p.clone().normalize();
      p.copy(d);
      p.multiplyScalar(radius);
      p.add(origin);
      d.multiplyScalar(0.0);

      return {
          position: p,
          size: (Math.random() * 0.5 + 0.5) * 1.0,
          colour: new THREE.Color(),
          alpha: 1.0,
          life: life,
          maxLife: life,
          rotation: Math.random() * 2.0 * Math.PI,
          velocity: d,
          blend: this.blend_,
      };
    }
  };


  class FireFXEmitter extends particle_system.ParticleEmitter {
    constructor(parent) {
      super();
      this.parent_ = parent;
      this.blend_ = 0.0;
    }

    OnUpdate_() {
      this.parent_.updateMatrixWorld(true);
    }

    AddParticles(num) {
      for (let i = 0; i < num; ++i) {
        this.particles_.push(this.CreateParticle_());
      }
    }

    CreateParticle_() {
      const origin = new THREE.Vector3(0, 0, 0);
      this.parent_.localToWorld(origin);

      const radius = 1.0;
      const life = (Math.random() * 0.75 + 0.25) * 1.5;
      const p = new THREE.Vector3(
          (Math.random() * 2 - 1) * radius,
          (Math.random() * 2 - 1) * radius,
          (Math.random() * 2 - 1) * radius);

      const d = p.clone().normalize();
      p.copy(d);
      p.multiplyScalar(radius);
      p.add(origin);
      d.multiplyScalar(3.0);

      return {
          position: p,
          size: (Math.random() * 0.5 + 0.5) * 1.0,
          colour: new THREE.Color(),
          alpha: 1.0,
          life: life,
          maxLife: life,
          rotation: Math.random() * 2.0 * Math.PI,
          velocity: d,
          blend: this.blend_,
      };
    }
  };


  class BloodEffect extends entity.Component {
    constructor(params) {
      super();
      this.params_ = params;

      this.bloodFX_ = new particle_system.ParticleSystem({
          camera: params.camera,
          parent: params.scene,
          texture: './resources/textures/whitePuff14.png',
      });
      this.fireFX_ = new particle_system.ParticleSystem({
          camera: params.camera,
          parent: params.scene,
          texture: './resources/textures/fire.png',
      });
    }

    Destroy() {
      this.bloodFX_.Destroy();
      this.fireFX_.Destroy();
    }

    InitComponent() {
      this._RegisterHandler('events.network', (m) => { this.OnEvents_(m); });
      this._RegisterHandler('load.character', (m) => this.OnCharacterLoaded_(m));
    }

    OnCharacterLoaded_(msg) {
      this.bones_ = msg.bones;
    }

    OnEvents_(msg) {
      if (!this.bones_) {
        return;
      }

      for (let e of msg.value) {
        if (e.type != 'attack') {
          continue;
        }

        // Another hack
        const hc = e.attacker.GetComponent('HealthComponent');

        if (hc.stats_.desc.character.class != 'sorceror') {
          this.EmitBloodFX_();
        } else {
          this.EmitFireFX_();
        }
      }
    }

    EmitFireFX_() {
      const targets = ['Head', 'Hips'];
      for (let t of targets) {
        const b = this.bones_[t];
        let emitter = new FireFXEmitter(b);
        emitter.alphaSpline_.AddPoint(0.0, 0.0);
        emitter.alphaSpline_.AddPoint(0.5, 1.0);
        emitter.alphaSpline_.AddPoint(1.0, 0.0);
        
        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x00FF00));
        emitter.colourSpline_.AddPoint(0.3, new THREE.Color(0x00FF00));
        emitter.colourSpline_.AddPoint(0.4, new THREE.Color(0xdeec42));
        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0xf4a776));
        
        emitter.sizeSpline_.AddPoint(0.0, 0.5);
        emitter.sizeSpline_.AddPoint(0.5, 3.0);
        emitter.sizeSpline_.AddPoint(1.0, 0.5);
        emitter.blend_ = 0.0;
        emitter.AddParticles(200);

        this.fireFX_.AddEmitter(emitter);

        emitter = new FireFXEmitter(b);
        emitter.alphaSpline_.AddPoint(0.0, 0.0);
        emitter.alphaSpline_.AddPoint(0.7, 1.0);
        emitter.alphaSpline_.AddPoint(1.0, 0.0);
        
        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0x000000));
        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x000000));
        
        emitter.sizeSpline_.AddPoint(0.0, 0.5);
        emitter.sizeSpline_.AddPoint(0.5, 4.0);
        emitter.sizeSpline_.AddPoint(1.0, 10.0);
        emitter.blend_ = 1.0;
        emitter.AddParticles(100);

        this.fireFX_.AddEmitter(emitter);
      }
    }

    EmitBloodFX_() {
      // hack shoudl check if this was legit anything else than damage
      const targets = ['Head', 'Hips'];
      for (let t of targets) {
        const b = this.bones_[t];
        const emitter = new BloodEffectEmitter(b);
        emitter.alphaSpline_.AddPoint(0.0, 0.0);
        emitter.alphaSpline_.AddPoint(0.7, 1.0);
        emitter.alphaSpline_.AddPoint(1.0, 0.0);
        
        emitter.colourSpline_.AddPoint(0.0, new THREE.Color(0xbb2909));
        emitter.colourSpline_.AddPoint(1.0, new THREE.Color(0x701a08));
        
        emitter.sizeSpline_.AddPoint(0.0, 0.5);
        emitter.sizeSpline_.AddPoint(0.5, 1.0);
        emitter.sizeSpline_.AddPoint(1.0, 0.5);
        emitter.SetLife(0.5);
        emitter.SetEmissionRate(500);
        emitter.blend_ = 1.0;

        this.bloodFX_.AddEmitter(emitter);
      }      
    }

    Update(timeElapsed) {
      this.bloodFX_.Update(timeElapsed);
      this.fireFX_.Update(timeElapsed);
    }
  }
  
  return {
    BloodEffect: BloodEffect,
  };
})();

<|start_filename|>client/src/threejs_component.js<|end_filename|>
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js';

import {entity} from "./entity.js";

export const threejs_component = (() => {


  const _VS = `
  varying vec3 vWorldPosition;
  
  void main() {
    vec4 worldPosition = modelMatrix * vec4( position, 1.0 );
    vWorldPosition = worldPosition.xyz;
  
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }`;
  
  
  const _FS = `
  uniform vec3 topColor;
  uniform vec3 bottomColor;
  uniform float offset;
  uniform float exponent;
  uniform samplerCube background;
  
  varying vec3 vWorldPosition;
  
  void main() {
    vec3 viewDirection = normalize(vWorldPosition - cameraPosition);
    vec3 stars = textureCube(background, viewDirection).xyz;
  
    float h = normalize(vWorldPosition + offset).y;
    float t = max(pow(max(h, 0.0), exponent), 0.0);
  
    float f = exp(min(0.0, -vWorldPosition.y * 0.00125));
  
    vec3 sky = mix(stars, bottomColor, f);
    gl_FragColor = vec4(sky, 1.0);
  }`;

  class ThreeJSController extends entity.Component {
    constructor() {
      super();
    }

    InitEntity() {
      THREE.ShaderChunk.fog_fragment = `
      #ifdef USE_FOG
        vec3 fogOrigin = cameraPosition;
        vec3 fogDirection = normalize(vWorldPosition - fogOrigin);
        float fogDepth = distance(vWorldPosition, fogOrigin);
  
        fogDepth *= fogDepth;
  
        float heightFactor = 0.05;
        float fogFactor = heightFactor * exp(-fogOrigin.y * fogDensity) * (
            1.0 - exp(-fogDepth * fogDirection.y * fogDensity)) / fogDirection.y;
        fogFactor = saturate(fogFactor);
  
        gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );
      #endif`;
      
      THREE.ShaderChunk.fog_pars_fragment = `
      #ifdef USE_FOG
        uniform float fogTime;
        uniform vec3 fogColor;
        varying vec3 vWorldPosition;
        #ifdef FOG_EXP2
          uniform float fogDensity;
        #else
          uniform float fogNear;
          uniform float fogFar;
        #endif
      #endif`;
      
      THREE.ShaderChunk.fog_vertex = `
      #ifdef USE_FOG
        vWorldPosition = (modelMatrix * vec4(transformed, 1.0 )).xyz;
      #endif`;
      
      THREE.ShaderChunk.fog_pars_vertex = `
      #ifdef USE_FOG
        varying vec3 vWorldPosition;
      #endif`;
  
      this.threejs_ = new THREE.WebGLRenderer({
        antialias: false,
      });
      this.threejs_.outputEncoding = THREE.sRGBEncoding;
      this.threejs_.gammaFactor = 2.2;
      this.threejs_.shadowMap.enabled = true;
      this.threejs_.shadowMap.type = THREE.PCFSoftShadowMap;
      this.threejs_.setPixelRatio(window.devicePixelRatio);
      this.threejs_.setSize(window.innerWidth, window.innerHeight);
      this.threejs_.domElement.id = 'threejs';
  
      document.getElementById('container').appendChild(this.threejs_.domElement);
  
      window.addEventListener('resize', () => {
        this._OnWindowResize();
      }, false);
  
      const fov = 60;
      const aspect = 1920 / 1080;
      const near = 1.0;
      const far = 10000.0;
      this.camera_ = new THREE.PerspectiveCamera(fov, aspect, near, far);
      this.camera_.position.set(25, 10, 25);
  
      this.scene_ = new THREE.Scene();
      this.scene_.fog = new THREE.FogExp2(0x89b2eb, 0.00002);
  
      let light = new THREE.DirectionalLight(0x8088b3, 0.7);
      light.position.set(-10, 500, 10);
      light.target.position.set(0, 0, 0);
      light.castShadow = true;
      light.shadow.bias = -0.001;
      light.shadow.mapSize.width = 4096;
      light.shadow.mapSize.height = 4096;
      light.shadow.camera.near = 0.1;
      light.shadow.camera.far = 1000.0;
      light.shadow.camera.left = 100;
      light.shadow.camera.right = -100;
      light.shadow.camera.top = 100;
      light.shadow.camera.bottom = -100;
      this.scene_.add(light);
  
      this.sun_ = light;

      this.LoadSky_();
    }

    LoadSky_() {
      const hemiLight = new THREE.HemisphereLight(0x424a75, 0x6a88b5, 0.7);
      // hemiLight.color.setHSL(0.6, 1, 0.4);
      // hemiLight.groundColor.setHSL(0.095, 1, 0.5);
      this.scene_.add(hemiLight);
  
  
      const loader = new THREE.CubeTextureLoader();
      const texture = loader.load([
          './resources/terrain/space-posx.jpg',
          './resources/terrain/space-negx.jpg',
          './resources/terrain/space-posy.jpg',
          './resources/terrain/space-negy.jpg',
          './resources/terrain/space-posz.jpg',
          './resources/terrain/space-negz.jpg',
      ]);
      texture.encoding = THREE.sRGBEncoding;
  
      const uniforms = {
        "topColor": { value: new THREE.Color(0x000000) },
        "bottomColor": { value: new THREE.Color(0x5d679e) },
        "offset": { value: -500 },
        "exponent": { value: 0.3 },
        "background": { value: texture },
      };
      // uniforms["topColor"].value.copy(hemiLight.color);
  
      this.scene_.fog.color.copy(uniforms["bottomColor"].value);
  
  
      const skyGeo = new THREE.SphereBufferGeometry(5000, 32, 15);
      const skyMat = new THREE.ShaderMaterial({
          uniforms: uniforms,
          vertexShader: _VS,
          fragmentShader: _FS,
          side: THREE.BackSide
      });
  
      const sky = new THREE.Mesh(skyGeo, skyMat);
      this.scene_.add(sky);
    }

    Update(_) {
      const player = this.FindEntity('player');
      if (!player) {
        return;
      }
      const pos = player._position;
  
      this.sun_.position.copy(pos);
      this.sun_.position.add(new THREE.Vector3(-50, 200, -10));
      this.sun_.target.position.copy(pos);
      this.sun_.updateMatrixWorld();
      this.sun_.target.updateMatrixWorld();
    }
  }

  return {
      ThreeJSController: ThreeJSController,
  };
})();

<|start_filename|>client/src/health-component.js<|end_filename|>
import {entity} from "./entity.js";


export const health_component = (() => {

  class HealthComponent extends entity.Component {
    constructor(params) {
      super();
      this.stats_ = params;
    }

    InitComponent() {
      this._RegisterHandler(
          'health.damage', (m) => this.OnDamage_(m));
      this._RegisterHandler(
          'stats.network', (m) => this.OnNetworkUpdate_(m));
      this._RegisterHandler(
          'health.add-experience', (m) => this.OnAddExperience_(m));

      this.UpdateUI_();
    }

    IsAlive() {
      return this.stats_.health > 0;
    }

    get Health() {
      return this.stats_.health;
    }

    UpdateUI_() {
      if (!this.stats_.updateUI) {
        return;
      }

      const bar = document.getElementById('health-bar');

      const healthAsPercentage = this.stats_.health / this.stats_.maxHealth;
      bar.style.width = Math.floor(200 * healthAsPercentage) + 'px';

      document.getElementById('stats-strength').innerText = this.stats_.strength;
      document.getElementById('stats-wisdomness').innerText = this.stats_.wisdomness;
      document.getElementById('stats-benchpress').innerText = this.stats_.benchpress;
      document.getElementById('stats-curl').innerText = this.stats_.curl;
      document.getElementById('stats-experience').innerText = this.stats_.experience;
    }

    _ComputeLevelXPRequirement() {
      const level = this.stats_.level;
      // Blah just something easy
      const xpRequired = Math.round(2 ** (level - 1) * 100);
      return xpRequired;
    }

    OnAddExperience_(msg) {
      this.stats_.experience += msg.value;
      const requiredExperience = this._ComputeLevelXPRequirement();
      if (this.stats_.experience < requiredExperience) {
        return;
      }

      this.stats_.level += 1;
      this.stats_.strength += 1;
      this.stats_.wisdomness += 1;
      this.stats_.benchpress += 1;
      this.stats_.curl += 2;

      const spawner = this.FindEntity(
          'level-up-spawner').GetComponent('LevelUpComponentSpawner');
      spawner.Spawn(this.Parent.Position);

      this.Broadcast({
          topic: 'health.levelGained',
          value: this.stats_.level,
      });

      this.UpdateUI_();
    }

    _OnDeath() {
      this.Broadcast({
          topic: 'health.death',
      });
    }

    OnHealthChanged_() {
      if (this.stats_.health == 0) {
        this._OnDeath();
      }

      this.Broadcast({
        topic: 'health.update',
        health: this.stats_.health,
        maxHealth: this.stats_.maxHealth,
      });

      this.UpdateUI_();
    }


    OnNetworkUpdate_(msg) {
      const newStats = msg.value[1];
      for (let k in newStats) {
        this.stats_[k] = newStats[k];
      }

      this.OnHealthChanged_();
    }

    OnDamage_(msg) {
      this.stats_.health = Math.max(0.0, this.stats_.health - msg.value);

      this.OnHealthChanged_();
    }
  };

  return {
    HealthComponent: HealthComponent,
  };

})();

<|start_filename|>client/src/equip-weapon-component.js<|end_filename|>
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js';

import {entity} from './entity.js';

import {defs} from '/shared/defs.mjs';

import {FBXLoader} from 'https://cdn.jsdelivr.net/npm/three@0.124/examples/jsm/loaders/FBXLoader.js';


export const equip_weapon_component = (() => {

  class EquipWeapon extends entity.Component {
    constructor(params) {
      super();
      this.params_ = params;
      this.target_ = null;
      this.name_ = null;

      const classType = this.params_.desc.character.class;
      const modelData = defs.CHARACTER_MODELS[classType];
      this.anchor_ = modelData.anchors.rightHand;
    }

    InitComponent() {
      this._RegisterHandler('load.character', (m) => this._OnCharacterLoaded(m));
      this._RegisterHandler('inventory.equip', (m) => this._OnEquip(m));
    }

    _OnCharacterLoaded(msg) {
      this._bones = msg.bones;
      this._AttachTarget();
    }

    _AttachTarget() {
      if (this._bones && this.target_) {
        this._bones[this.anchor_].add(this.target_);
      }
    }

    GetItemDefinition_(name) {
      const database = this.FindEntity('database').GetComponent(
          'InventoryDatabaseController');
      return database.Find(name);
    }

    _OnEquip(msg) {
      if (msg.value == this.name_) {
        return;
      }

      if (this.target_) {
        this._UnloadModels();
      }
      const inventory = this.GetComponent('InventoryController');
      const item = this.GetItemDefinition_(msg.value);

      this.name_ = msg.value;

      if (item) {
        this._LoadModels(item, () => {
          this._AttachTarget();
        });
      }
    }

    _UnloadModels() {
      if (this.target_) {
        this.target_.parent.remove(this.target_);
        // Probably need to free the memory properly, whatever
        this.target_ = null;
      }
    }

    _LoadModels(item, cb) {
      const loader = new FBXLoader();
      loader.setPath('./resources/weapons/FBX/');
      loader.load(item.renderParams.name + '.fbx', (fbx) => {
        this.target_ = fbx;
        this.target_.scale.setScalar(item.renderParams.scale);
        // this.target_.rotateY(Math.PI);
        this.target_.rotateX(Math.PI / 2);
        // this.target_.rotateY(-1);

        this.target_.traverse(c => {
          c.castShadow = true;
          c.receiveShadow = true;

          // Do this instead of something smart like re-exporting.
          let materials = c.material;
          let newMaterials = [];
          if (!(c.material instanceof Array)) {
            materials = [c.material];
          }

          for (let m of materials) {
            if (m) {
              const c = new THREE.Color().copy(m.color);
              c.multiplyScalar(0.75);
              newMaterials.push(new THREE.MeshStandardMaterial({
                  color: c,
                  name: m.name,
                  metalness: 1.0,
              }));
            }
          }

          if (!(c.material instanceof Array)) {
            c.material = newMaterials[0];
          } else {
            c.material = newMaterials;
          }

        });

        cb();

        this.Broadcast({
            topic: 'load.weapon',
            model: this.target_,
            bones: this._bones,
        });
      });
    }
  };

  return {
      EquipWeapon: EquipWeapon,
  };
})();

<|start_filename|>client/src/inventory-controller.js<|end_filename|>
import {entity} from './entity.js';


export const inventory_controller = (() => {

  class InventoryDatabaseController extends entity.Component {
    constructor() {
      super();

      this.items_ = {};
    }

    AddItem(name, item) {
      this.items_[name] = item;
    }

    Find(name) {
      return this.items_[name];
    }
  };

  class UIInventoryController extends entity.Component {
    constructor() {
      super();
    }

    InitEntity() {
      this._RegisterHandler('inventory.updated', () => this.OnInventoryUpdated_());

      this.inventory_ = this.GetComponent('InventoryController');

      const _SetupElement = (n) => {
        const element = document.getElementById(n);
        element.ondragstart = (ev) => {
          ev.dataTransfer.setData('text/plain', n);
        };
        element.ondragover = (ev) => {
          ev.preventDefault();
        };
        element.ondrop = (ev) => {
          ev.preventDefault();
          const data = ev.dataTransfer.getData('text/plain');
          const other = document.getElementById(data);
    
          this.OnItemDropped_(other, element);
        };
      }

      for (let k in this.inventory_.Inventory) {
        _SetupElement(k);
      }
    }

    OnInventoryUpdated_() {
      const items = this.inventory_.Inventory;
      for (let k in items) {
        this.SetItemAtSlot_(k, items[k].value);
      }
    }

    OnItemDropped_(oldElement, newElement) {
      const oldItem = this.inventory_.Inventory[oldElement.id];
      const newItem = this.inventory_.Inventory[newElement.id];

      const oldValue = oldItem.value;
      const newValue = newItem.value;

      this.SetItemAtSlot_(oldElement.id, newValue);
      this.SetItemAtSlot_(newElement.id, oldValue);

      this.inventory_.SetItemAtSlot_(oldElement.id, newValue);
      this.inventory_.SetItemAtSlot_(newElement.id, oldValue);
    }

    GetItemDefinition_(name) {
      const database = this.FindEntity('database').GetComponent(
          'InventoryDatabaseController');
      return database.Find(name);
    }

    SetItemAtSlot_(slot, itemName) {
      const div = document.getElementById(slot);
      const item = this.GetItemDefinition_(itemName);
      if (item) {
        const path = './resources/icons/weapons/' + item.renderParams.icon;
        div.style.backgroundImage = "url('" + path + "')";
      } else {
        div.style.backgroundImage = '';
      }
    }
  };


  class InventoryController extends entity.Component {
    constructor() {
      super();

      this.inventory_ = this.CreateEmpty_();
    }

    CreateEmpty_() {
      const inventory = {};
      for (let i = 1; i <= 24; ++i) {
        inventory['inventory-' + i] = {
          type: 'inventory',
          value: null,
        };
      }

      for (let i = 1; i <= 8; ++i) {
        inventory['inventory-equip-' + i] = {
          type: 'equip',
          value: null,
        };
      }
      return inventory;
    }

    get Inventory() {
      return this.inventory_;
    }

    InitComponent() {
      // Hack
      this._RegisterHandler('network.inventory', (m) => this.OnNetworkUpdate_(m));
    }

    OnNetworkUpdate_(msg) {
      const inventory = this.CreateEmpty_();
      for (let k in msg.inventory) {
        inventory[k].value = msg.inventory[k];
      }

      let changed = false;
      for (let k in inventory) {
        if (inventory[k].value != this.inventory_[k].value) {
          this.SetItemAtSlot_(k, inventory[k].value);
        }
      }

      if (inventory) {
        this.Broadcast({
            topic: 'inventory.updated'
        });
      }
    }

    CreatePacket() {
      // Meh
      const packet = {};
      for (let k in this.inventory_) {
        if (this.inventory_[k].value) {
          packet[k] = this.inventory_[k].value;
        }
      }
      return packet;
    }

    GetItemDefinition_(name) {
      const database = this.FindEntity('database').GetComponent(
          'InventoryDatabaseController');
      return database.Find(name);
    }

    SetItemAtSlot_(slot, itemName) {
      const oldValue = this.inventory_[slot].value;

      this.inventory_[slot].value = itemName;

      if (this.inventory_[slot].type == 'equip' && oldValue != itemName) {
        this.Broadcast({
            topic: 'inventory.equip',
            value: itemName,
        });
      }
    }

    GetItemByName(name) {
      for (let k in this.inventory_) {
        if (this.inventory_[k].value == name) {
          return this.FindEntity(name);
        }
      }
      return null;
    }
  };


  class InventoryItem extends entity.Component {
    constructor(params) {
      super();
      this._params = params;
    }

    InitComponent() {}

    get Params() {
      return this._params;
    }

    get RenderParams() {
      return this._params.renderParams;
    }
  };

  
  return {
      InventoryDatabaseController: InventoryDatabaseController,
      UIInventoryController: UIInventoryController,
      InventoryController: InventoryController,
      InventoryItem: InventoryItem,
  };
})();

<|start_filename|>client/src/test.js<|end_filename|>




// We got nothing.

<|start_filename|>client/src/floating-name.js<|end_filename|>
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js';

import {entity} from './entity.js';

import {defs} from '/shared/defs.mjs';


export const floating_name = (() => {

  class FloatingName extends entity.Component {
    constructor(params) {
      super();
      this.params_ = params;
      this.visible_ = true;
    }

    Destroy() {
      if (!this.sprite_) {
        this.visible_ = false;
        return;
      }

      this.sprite_.traverse(c => {
        if (c.material) {
          let materials = c.material;
          if (!(c.material instanceof Array)) {
            materials = [c.material];
          }
          for (let m of materials) {
            m.dispose();
          }
        }

        if (c.geometry) {
          c.geometry.dispose();
        }
      });
      if (this.sprite_.parent) {
        this.sprite_.parent.remove(this.sprite_);
      }
    }

    InitComponent() {
      this._RegisterHandler(
          'load.character', (m) => { this.CreateSprite_(m); });
      this._RegisterHandler(
          'health.death', (m) => { this.OnDeath_(m); });
    }

    OnDeath_() {
      this.Destroy();
    }

    CreateSprite_(msg) {
      if (!this.visible_) {
        return;
      }
      const modelData = defs.CHARACTER_MODELS[
          this.params_.desc.character.class];

      this.element_ = document.createElement('canvas');
      this.context2d_ = this.element_.getContext('2d');
      this.context2d_.canvas.width = 256;
      this.context2d_.canvas.height = 128;
      this.context2d_.fillStyle = '#FFF';
      this.context2d_.font = "18pt Helvetica";
      this.context2d_.shadowOffsetX = 3;
      this.context2d_.shadowOffsetY = 3;
      this.context2d_.shadowColor = "rgba(0,0,0,0.3)";
      this.context2d_.shadowBlur = 4;
      this.context2d_.textAlign = 'center';
      this.context2d_.fillText(this.params_.desc.account.name, 128, 64);

      const map = new THREE.CanvasTexture(this.context2d_.canvas);

      this.sprite_ = new THREE.Sprite(
          new THREE.SpriteMaterial({map: map, color: 0xffffff, fog: false}));
      this.sprite_.scale.set(10, 5, 1)
      this.sprite_.position.y += modelData.nameOffset;
      msg.model.add(this.sprite_);
    }
  };

  return {
      FloatingName: FloatingName,
  };
})();

<|start_filename|>client/src/particle-system.js<|end_filename|>
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js';

export const particle_system = (() => {

  const _VS = `
  uniform float pointMultiplier;
  
  attribute float size;
  attribute float angle;
  attribute float blend;
  attribute vec4 colour;
  
  varying vec4 vColour;
  varying vec2 vAngle;
  varying float vBlend;
  
  void main() {
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  
    gl_Position = projectionMatrix * mvPosition;
    gl_PointSize = size * pointMultiplier / gl_Position.w;
  
    vAngle = vec2(cos(angle), sin(angle));
    vColour = colour;
    vBlend = blend;
  }`;
  
  const _FS = `
  
  uniform sampler2D diffuseTexture;
  
  varying vec4 vColour;
  varying vec2 vAngle;
  varying float vBlend;
  
  void main() {
    vec2 coords = (gl_PointCoord - 0.5) * mat2(vAngle.x, vAngle.y, -vAngle.y, vAngle.x) + 0.5;
    gl_FragColor = texture2D(diffuseTexture, coords) * vColour;
    gl_FragColor.xyz *= gl_FragColor.w;
    gl_FragColor.w *= vBlend;
  }`;
  
   
  class LinearSpline {
    constructor(lerp) {
      this.points_ = [];
      this._lerp = lerp;
    }
  
    AddPoint(t, d) {
      this.points_.push([t, d]);
    }
  
    Get(t) {
      let p1 = 0;
  
      for (let i = 0; i < this.points_.length; i++) {
        if (this.points_[i][0] >= t) {
          break;
        }
        p1 = i;
      }
  
      const p2 = Math.min(this.points_.length - 1, p1 + 1);
  
      if (p1 == p2) {
        return this.points_[p1][1];
      }
  
      return this._lerp(
          (t - this.points_[p1][0]) / (
              this.points_[p2][0] - this.points_[p1][0]),
          this.points_[p1][1], this.points_[p2][1]);
    }
  }
  
  class ParticleEmitter {
    constructor() {
      this.alphaSpline_ = new LinearSpline((t, a, b) => {
        return a + t * (b - a);
      });
  
      this.colourSpline_ = new LinearSpline((t, a, b) => {
        const c = a.clone();
        return c.lerp(b, t);
      });
  
      this.sizeSpline_ = new LinearSpline((t, a, b) => {
        return a + t * (b - a);
      });

      this.emissionRate_ = 0.0;
      this.emissionAccumulator_ = 0.0;
      this.particles_ = [];
      this.emitterLife_ = null;
    }

    UpdateParticles_(timeElapsed) {
      for (let p of this.particles_) {
        p.life -= timeElapsed;
      }
  
      this.particles_ = this.particles_.filter(p => {
        return p.life > 0.0;
      });
  
      for (let i = 0; i < this.particles_.length; ++i) {
        const p = this.particles_[i];
        const t = 1.0 - p.life / p.maxLife;

        if (t < 0 || t > 1) {
          let a =  0;
        }
  
        p.rotation += timeElapsed * 0.5;
        p.alpha = this.alphaSpline_.Get(t);
        p.currentSize = p.size * this.sizeSpline_.Get(t);
        p.colour.copy(this.colourSpline_.Get(t));
  
        p.position.add(p.velocity.clone().multiplyScalar(timeElapsed));
  
        const drag = p.velocity.clone();
        drag.multiplyScalar(timeElapsed * 0.1);
        drag.x = Math.sign(p.velocity.x) * Math.min(Math.abs(drag.x), Math.abs(p.velocity.x));
        drag.y = Math.sign(p.velocity.y) * Math.min(Math.abs(drag.y), Math.abs(p.velocity.y));
        drag.z = Math.sign(p.velocity.z) * Math.min(Math.abs(drag.z), Math.abs(p.velocity.z));
        p.velocity.sub(drag);
      }
    }
    
    CreateParticle_() {
      const life = (Math.random() * 0.75 + 0.25) * 5.0;
      return {
          position: new THREE.Vector3(
              (Math.random() * 2 - 1) * 4.0 + -44,
              (Math.random() * 2 - 1) * 4.0 + 0,
              (Math.random() * 2 - 1) * 4.0 + 12),
          size: (Math.random() * 0.5 + 0.5) * 2.0,
          colour: new THREE.Color(),
          alpha: 1.0,
          life: life,
          maxLife: life,
          rotation: Math.random() * 2.0 * Math.PI,
          velocity: new THREE.Vector3(0, 1, 0),
          blend: 0.0,
      };
    }

    get IsAlive() {
      if (this.emitterLife_ === null) {
        return this.particles_.length > 0;
      } else {
        return this.emitterLife_ > 0.0 || this.particles_.length > 0;
      }
    }

    SetLife(life) {
      this.emitterLife_ = life;
    }

    SetEmissionRate(rate) {
      this.emissionRate_ = rate;
    }

    OnUpdate_(_) {
    }

    Update(timeElapsed) {
      this.OnUpdate_(timeElapsed);

      if (this.emitterLife_ !== null) {
        this.emitterLife_ -= timeElapsed;
      }

      if (this.emissionRate_ > 0.0) {
        this.emissionAccumulator_ += timeElapsed;
        const n = Math.floor(this.emissionAccumulator_ * this.emissionRate_);
        this.emissionAccumulator_ -= n / this.emissionRate_;
    
        for (let i = 0; i < n; i++) {
          const p = this.CreateParticle_();
          this.particles_.push(p);
        }
      }

      this.UpdateParticles_(timeElapsed);
    }
  };

  
  class ParticleSystem {
    constructor(params) {
      const uniforms = {
          diffuseTexture: {
              value: new THREE.TextureLoader().load(params.texture)
          },
          pointMultiplier: {
              value: window.innerHeight / (2.0 * Math.tan(0.5 * 60.0 * Math.PI / 180.0))
          }
      };
  
      this.material_ = new THREE.ShaderMaterial({
          uniforms: uniforms,
          vertexShader: _VS,
          fragmentShader: _FS,
          blending: THREE.CustomBlending,
          blendEquation: THREE.AddEquation,
          blendSrc: THREE.OneFactor,
          blendDst: THREE.OneMinusSrcAlphaFactor,
          depthTest: true,
          depthWrite: false,
          transparent: true,
          vertexColors: true
      });
  
      this.camera_ = params.camera;
      this.particles_ = [];
  
      this.geometry_ = new THREE.BufferGeometry();
      this.geometry_.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
      this.geometry_.setAttribute('size', new THREE.Float32BufferAttribute([], 1));
      this.geometry_.setAttribute('colour', new THREE.Float32BufferAttribute([], 4));
      this.geometry_.setAttribute('angle', new THREE.Float32BufferAttribute([], 1));
      this.geometry_.setAttribute('blend', new THREE.Float32BufferAttribute([], 1));
  
      this.points_ = new THREE.Points(this.geometry_, this.material_);
  
      params.parent.add(this.points_);
  
      this.emitters_ = [];
      this.particles_ = [];
  
      this.UpdateGeometry_();
    }
  
    Destroy() {
      this.material_.dispose();
      this.geometry_.dispose();
      if (this.points_.parent) {
        this.points_.parent.remove(this.points_);
      }
    }
  

    AddEmitter(e) {
      this.emitters_.push(e);
    }

    UpdateGeometry_() {
      const positions = [];
      const sizes = [];
      const colours = [];
      const angles = [];
      const blends = [];
  
      const box = new THREE.Box3();
      for (let p of this.particles_) {
        positions.push(p.position.x, p.position.y, p.position.z);
        colours.push(p.colour.r, p.colour.g, p.colour.b, p.alpha);
        sizes.push(p.currentSize);
        angles.push(p.rotation);
        blends.push(p.blend);
  
        box.expandByPoint(p.position);
      }
  
      this.geometry_.setAttribute(
          'position', new THREE.Float32BufferAttribute(positions, 3));
      this.geometry_.setAttribute(
          'size', new THREE.Float32BufferAttribute(sizes, 1));
      this.geometry_.setAttribute(
          'colour', new THREE.Float32BufferAttribute(colours, 4));
      this.geometry_.setAttribute(
          'angle', new THREE.Float32BufferAttribute(angles, 1));
      this.geometry_.setAttribute(
          'blend', new THREE.Float32BufferAttribute(blends, 1));
    
      this.geometry_.attributes.position.needsUpdate = true;
      this.geometry_.attributes.size.needsUpdate = true;
      this.geometry_.attributes.colour.needsUpdate = true;
      this.geometry_.attributes.angle.needsUpdate = true;
      this.geometry_.attributes.blend.needsUpdate = true;
      this.geometry_.boundingBox = box;
      this.geometry_.boundingSphere = new THREE.Sphere();
  
      box.getBoundingSphere(this.geometry_.boundingSphere);
    }
  
    UpdateParticles_(timeElapsed) {
      this.particles_ = this.emitters_.map(e => e.particles_);
      this.particles_ = this.particles_.flat();
      this.particles_.sort((a, b) => {
        const d1 = this.camera_.position.distanceTo(a.position);
        const d2 = this.camera_.position.distanceTo(b.position);
  
        if (d1 > d2) {
          return -1;
        }
  
        if (d1 < d2) {
          return 1;
        }
  
        return 0;
      });
    }

    UpdateEmitters_(timeElapsed) {
      for (let i = 0; i < this.emitters_.length; ++i) {
        this.emitters_[i].Update(timeElapsed);
      }

      this.emitters_= this.emitters_.filter(e => e.IsAlive);
    }
  
    Update(timeElapsed) {
      this.UpdateEmitters_(timeElapsed);
      this.UpdateParticles_(timeElapsed);
      this.UpdateGeometry_();
    }
  };

  return {
      ParticleEmitter: ParticleEmitter,
      ParticleSystem: ParticleSystem,
  };
})();