First release
@ -1,2 +1,6 @@
|
||||
/.idea/
|
||||
/public/tileset/mwmap/
|
||||
/public/blob/*
|
||||
!/public/blob/.gitkeep
|
||||
/node_modules
|
||||
|
||||
|
@ -1,2 +1,31 @@
|
||||
# nwahmap
|
||||
You N'wah really need a map to get around?
|
||||
|
||||
---
|
||||
|
||||
Nwahmap is a companion site for NwaHTTP the tes3mp plugin
|
||||
|
||||
## Setup
|
||||
|
||||
Please make sure [`git-lfs`](https://git-lfs.github.com/) is installed. this is used to store the tiles.
|
||||
|
||||
```bash
|
||||
git clone git@git.cijber.net:teamnwah/nwahmap.git nwahmap
|
||||
cd $_
|
||||
# Extract tiles from storage/mwmap-tiles.txz
|
||||
bin/extract-tiles.sh
|
||||
# Extract 3d files and textures head renders (Optional)
|
||||
script/export_nif.py <morrowind>/Data\ Files/Meshes/b <morrowind>/Data\ Files/Textures public/blob
|
||||
# Install dependencies
|
||||
yarn
|
||||
webpack
|
||||
```
|
||||
|
||||
Then place the files under `public` at `$TES3MP_HOME/www` and nwahttp will serve them for you if installed
|
||||
|
||||
---
|
||||
|
||||
Code licensed under AGPL-3.0-or-later
|
||||
Tiles for morrowind licensed under cc-by-sa, source: <a href="https://mwmap.uesp.net/">uesp.net</a>
|
||||
|
||||
Dependencies may be licensed seperately.
|
||||
|
@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$(realpath "$0")")/..";
|
||||
mkdir -p public/tileset/mwmap;
|
||||
cd $_;
|
||||
tar -xvf ../../../storage/mwmap-tiles.txz;
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.8.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-transform-react-jsx": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.6",
|
||||
"@babel/runtime": "^7.8.4",
|
||||
"babel-loader": "^8.0.6",
|
||||
"leaflet": "1.4.0",
|
||||
"preact": "^10.3.3",
|
||||
"three": "^0.114.0",
|
||||
"webpack": "^4.41.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"css-loader": "^2.1.1",
|
||||
"file-loader": "^3.0.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"style-loader": "^0.23.1",
|
||||
"webpack-cli": "^3.3.11"
|
||||
}
|
||||
}
|
@ -1,18 +1,142 @@
|
||||
@font-face {
|
||||
font-family: 'Pelagiad';
|
||||
src: url("../blob/font/Pelagiad.ttf") format('truetype');
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main {
|
||||
main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
grid-template-rows: min-content auto;
|
||||
background-color: #222222;
|
||||
color: rgb(223, 201, 159);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.nwah-entry-marker {
|
||||
width: 10px !important;
|
||||
height: 10px !important;
|
||||
}
|
||||
|
||||
.nwah-entry-marker.mark {
|
||||
border: 5px solid black;
|
||||
}
|
||||
|
||||
.nwah-player-marker {
|
||||
border-bottom: 20px solid;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.head {
|
||||
height: 20px;
|
||||
grid-row: 1;
|
||||
grid-column: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
.head > h2 {
|
||||
font-family: 'Pelagiad', sans-serif;
|
||||
margin: 0;
|
||||
padding: 0.3em;
|
||||
font-size: 3em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.player-list {
|
||||
padding: 0 1em 1em;
|
||||
}
|
||||
|
||||
.player {
|
||||
background-color: #111111;
|
||||
border: 1px solid rgb(223, 201, 159);
|
||||
padding: 0.2em 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.player > .name {
|
||||
font-family: 'Pelagiad', sans-serif;
|
||||
font-size: 2em;
|
||||
padding: 0.1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.player > .name > .dot {
|
||||
display: inline-block;
|
||||
border-radius: 100%;
|
||||
width: 0.3em;
|
||||
height: 0.3em;
|
||||
margin-right: 0.3em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.player > .name > .level {
|
||||
font-size: 0.5em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.map {
|
||||
height: calc(100% - 20px);
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
grid-row: 2;
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile, .leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
will-change: auto !important;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.player .location {
|
||||
text-align: center;
|
||||
font-size: 0.7em;
|
||||
min-height: 2.2em;
|
||||
}
|
||||
|
||||
.player .location > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin: 0.7em 0;
|
||||
position: relative;
|
||||
border: 1px solid rgb(223, 201, 159);
|
||||
}
|
||||
|
||||
.bar .inner-bar {
|
||||
overflow: visible;
|
||||
height: 1.4em;
|
||||
}
|
||||
|
||||
.bar .inner-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding: 0.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bar.health .inner-bar {
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
.bar.magicka .inner-bar {
|
||||
background-color: darkblue;
|
||||
}
|
||||
|
||||
.bar.fatigue .inner-bar {
|
||||
background-color: darkgreen;
|
||||
}
|
After Width: | Height: | Size: 910 B |
After Width: | Height: | Size: 508 B |
@ -1,21 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>N'wah map</title>
|
||||
<link rel="stylesheet" href="third-party/leaflet/leaflet.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<script src="third-party/leaflet/leaflet.js"></script>
|
||||
<script defer src="js/main.js"></script>
|
||||
<meta charset="UTF-8">
|
||||
<title>N'wah map</title>
|
||||
<link rel="stylesheet" href="css/leaflet.css">
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<script defer src="js/dist/bundle.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="head">
|
||||
<span>N'wah map</span>
|
||||
</div>
|
||||
<div class="map">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,81 +0,0 @@
|
||||
const SETTINGS = {
|
||||
cellSize: 2048,
|
||||
cellOffsetY: 20,
|
||||
cellOffsetX: 8,
|
||||
};
|
||||
|
||||
|
||||
const GameCRS = Object.assign({}, L.CRS, {
|
||||
scale(zoom) {
|
||||
return Math.pow(2, zoom);
|
||||
},
|
||||
|
||||
zoom(scale) {
|
||||
return Math.log(scale) / Math.LN2;
|
||||
},
|
||||
|
||||
distance(latlng1, latlng2) {
|
||||
let dx = latlng2.lng - latlng1.lng,
|
||||
dy = latlng2.lat - latlng1.lat;
|
||||
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
},
|
||||
projection: L.Projection.LonLat,
|
||||
|
||||
wrapLng: [-128, 128],
|
||||
wrapLat: [-128, 128],
|
||||
transformation: L.transformation(1, 128, 1, 128),
|
||||
infinite: false
|
||||
});
|
||||
|
||||
const map = document.querySelector('.map');
|
||||
const lMap = L.map(map, {
|
||||
zoom: 0,
|
||||
center: [0, 0],
|
||||
crs: GameCRS
|
||||
});
|
||||
|
||||
L.tileLayer('/tileset/mwmap/zoom{z}/vvardenfell-{x}-{y}-{z}.jpg', {
|
||||
attribution: 'uesp.net',
|
||||
minZoom: 0,
|
||||
maxZoom: 7,
|
||||
zoomOffset: 10,
|
||||
}).addTo(lMap);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => lMap.invalidateSize());
|
||||
document.addEventListener('resize', () => lMap.invalidateSize());
|
||||
|
||||
setMarkers();
|
||||
|
||||
async function setMarkers() {
|
||||
let markers = await fetch('http://yoko/tes3mp/assets/json/LiveMap.json');
|
||||
|
||||
for (let [name, details] of Object.entries(await markers.json())) {
|
||||
L.marker(translateLocationGameToMap(details.x, details.y)).addTo(lMap)
|
||||
}
|
||||
}
|
||||
|
||||
function translateLocationGameToMap(x, y) {
|
||||
const trans = [
|
||||
(-y / SETTINGS.cellSize) + SETTINGS.cellOffsetY,
|
||||
(x / SETTINGS.cellSize) + SETTINGS.cellOffsetX,
|
||||
];
|
||||
|
||||
console.log(trans);
|
||||
return trans;
|
||||
}
|
||||
|
||||
class NwahMap {
|
||||
constructor(settings) {
|
||||
this.markers = {};
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
init(map) {
|
||||
|
||||
}
|
||||
|
||||
translateGameLocToLatLng(x, y) {
|
||||
return L.LatLng
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 696 B |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 618 B |
@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from os import DirEntry
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
|
||||
from pyffi.formats.nif import NifFormat
|
||||
from PIL import Image
|
||||
import json
|
||||
|
||||
|
||||
class Texture:
|
||||
file: str
|
||||
wrap_s: bool
|
||||
wrap_t: bool
|
||||
uv_set: int
|
||||
|
||||
def __init__(self, file: str, wrap_s: bool = True, wrap_t: bool = True, uv_set: int = 0):
|
||||
self.file = file
|
||||
self.wrap_s = wrap_s
|
||||
self.wrap_t = wrap_t
|
||||
self.uv_set = uv_set
|
||||
|
||||
def __dict__(self):
|
||||
return {
|
||||
'file': self.file,
|
||||
'wrapS': self.wrap_s,
|
||||
'wrapT': self.wrap_t,
|
||||
'uvSet': self.uv_set,
|
||||
}
|
||||
|
||||
|
||||
class Shape:
|
||||
name: str
|
||||
vertices: List[Tuple[float, float, float]]
|
||||
normals: List[Tuple[float, float, float]]
|
||||
faces: List[Tuple[float, float, float]]
|
||||
uv_sets: List[List[float]]
|
||||
translation: Tuple[float, float, float]
|
||||
rotation: List[Tuple[float, float, float]]
|
||||
texture: Optional[Texture]
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
self.texture = None
|
||||
|
||||
def __dict__(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'vertices': self.vertices,
|
||||
'normals': self.normals,
|
||||
'faces': self.faces,
|
||||
'uvSets': self.uv_sets,
|
||||
'translation': self.translation,
|
||||
'rotation': self.rotation,
|
||||
'texture': self.texture.__dict__(),
|
||||
}
|
||||
|
||||
|
||||
class ShapeCollection:
|
||||
name: str
|
||||
shapes: List[Shape]
|
||||
|
||||
def __init__(self, name: str = ""):
|
||||
self.name = name
|
||||
self.shapes = []
|
||||
|
||||
def __dict__(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'shapes': [s.__dict__() for s in self.shapes],
|
||||
}
|
||||
|
||||
|
||||
def export(path) -> List[ShapeCollection]:
|
||||
data = NifFormat.Data()
|
||||
with open(path, 'rb') as f:
|
||||
data.inspect(f)
|
||||
data.read(f)
|
||||
|
||||
objects = []
|
||||
for root in data.roots:
|
||||
name = root.name.decode('utf-8').lower()
|
||||
# For this project, only hair and head matters
|
||||
if 'hair' in name or 'head' in name:
|
||||
sys.stderr.write("> " + name + "\n")
|
||||
else:
|
||||
sys.stderr.write(" " + name + "\n")
|
||||
continue
|
||||
|
||||
if isinstance(root, NifFormat.NiNode):
|
||||
objects.append(export_object(root))
|
||||
|
||||
# Some NiTriShape's are a root object
|
||||
if isinstance(root, NifFormat.NiTriShape):
|
||||
coll = ShapeCollection(name)
|
||||
coll.shapes = [export_shape(root)]
|
||||
|
||||
objects.append(coll)
|
||||
|
||||
return objects
|
||||
|
||||
|
||||
def export_object(root: NifFormat.NiNode) -> List[ShapeCollection]:
|
||||
obj = ShapeCollection(root.name.decode('utf-8'))
|
||||
|
||||
for shape in root.tree():
|
||||
if not isinstance(shape, NifFormat.NiTriShape):
|
||||
continue
|
||||
|
||||
obj.shapes.append(export_shape(shape))
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def export_shape(shape: NifFormat.NiTriShape) -> Shape:
|
||||
vertices = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.vertices)]
|
||||
faces = shape.data.get_triangles()
|
||||
uv_sets = [[(coord.u, coord.v) for coord in uv_set] for uv_set in shape.data.uv_sets]
|
||||
normals = [(vertice.x, vertice.y, vertice.z) for vertice in list(shape.data.normals)]
|
||||
|
||||
res_shape = Shape()
|
||||
for property in shape.get_properties():
|
||||
if isinstance(property, NifFormat.NiTexturingProperty):
|
||||
res_shape.texture = export_texture(property.base_texture)
|
||||
|
||||
res_shape.name = shape.name.decode('utf-8')
|
||||
res_shape.vertices = vertices
|
||||
res_shape.faces = faces
|
||||
res_shape.uv_sets = uv_sets
|
||||
res_shape.normals = normals
|
||||
res_shape.translation = (shape.translation.x, shape.translation.y, shape.translation.z)
|
||||
res_shape.rotation = shape.rotation.as_list()
|
||||
|
||||
return res_shape
|
||||
|
||||
|
||||
def export_texture(texture) -> Texture:
|
||||
wrap_s = texture.clamp_mode in [2, 3]
|
||||
wrap_t = texture.clamp_mode in [1, 3]
|
||||
source = texture.source
|
||||
|
||||
return Texture(source.file_name.decode('utf-8'), wrap_s, wrap_t, texture.uv_set)
|
||||
|
||||
|
||||
# Dumb file map allowing case-insensitive matching
|
||||
# And Morrowind's fucky file resolving
|
||||
class TextureMap:
|
||||
path: str
|
||||
files: Dict[str, DirEntry]
|
||||
dds_used: int
|
||||
original_dds: int
|
||||
original_used: int
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.files = {}
|
||||
self.dds_used = 0
|
||||
self.original_dds = 0
|
||||
self.original_used = 0
|
||||
|
||||
def build(self):
|
||||
for item in os.scandir(self.path):
|
||||
self.files[item.name.lower()] = item
|
||||
|
||||
def get(self, name):
|
||||
lower_name = name.lower()
|
||||
dds_instead = lower_name.rsplit('.', maxsplit=1)[0] + '.dds'
|
||||
|
||||
if dds_instead in self.files:
|
||||
if dds_instead == lower_name:
|
||||
self.original_dds += 1
|
||||
else:
|
||||
self.dds_used += 1
|
||||
return self.files[dds_instead].path
|
||||
|
||||
self.original_used += 1
|
||||
return self.files[lower_name].path
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <nif dir> <texture dir> <output dir>")
|
||||
exit(1)
|
||||
|
||||
nif_dir = sys.argv[1]
|
||||
texture_dir = sys.argv[2]
|
||||
texture_dir_map = TextureMap(texture_dir)
|
||||
texture_dir_map.build()
|
||||
|
||||
output_dir = sys.argv[3]
|
||||
|
||||
output_texture_dir = os.path.join(output_dir, 'texture')
|
||||
output_shape_dir = os.path.join(output_dir, 'shape')
|
||||
os.makedirs(output_shape_dir, exist_ok=True)
|
||||
os.makedirs(output_texture_dir, exist_ok=True)
|
||||
|
||||
for item in os.scandir(nif_dir):
|
||||
if not item.is_file():
|
||||
continue
|
||||
file_objects = export(item.path)
|
||||
|
||||
for file_object in file_objects:
|
||||
name = file_object.name = file_object.name.lower()
|
||||
|
||||
for shape in file_object.shapes:
|
||||
if shape.texture is not None:
|
||||
new_texture_name = shape.texture.file.rsplit('.', maxsplit=1)[0] + '.png'
|
||||
new_texture_name = new_texture_name.lower()
|
||||
new_file_name = os.path.join(output_texture_dir, new_texture_name)
|
||||
|
||||
texture_image = Image.open(texture_dir_map.get(shape.texture.file))
|
||||
texture_image.save(new_file_name, 'png')
|
||||
shape.texture.file = new_texture_name
|
||||
|
||||
shape_file_name = os.path.join(output_shape_dir, name + '.json')
|
||||
|
||||
with open(shape_file_name, 'w') as sf:
|
||||
json.dump(file_object.__dict__(), sf)
|
||||
|
||||
print(f"Textures: {texture_dir_map.original_dds} Orig. DDS, {texture_dir_map.dds_used} New DDS, {texture_dir_map.original_used} Orig. Non-DDS")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,24 @@
|
||||
import {CRS, Projection, transformation} from 'leaflet'
|
||||
|
||||
export const GameCRS = Object.assign({}, CRS, {
|
||||
scale(zoom) {
|
||||
return Math.pow(2, zoom);
|
||||
},
|
||||
|
||||
zoom(scale) {
|
||||
return Math.log(scale) / Math.LN2;
|
||||
},
|
||||
|
||||
distance(latlng1, latlng2) {
|
||||
let dx = latlng2.lng - latlng1.lng,
|
||||
dy = latlng2.lat - latlng1.lat;
|
||||
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
},
|
||||
projection: Projection.LonLat,
|
||||
|
||||
// wrapLng: [-128, 128],
|
||||
// wrapLat: [-128, 128],
|
||||
transformation: transformation(1, 128, -1, 128),
|
||||
infinite: false
|
||||
});
|
@ -0,0 +1,26 @@
|
||||
import {Marker} from "leaflet";
|
||||
import {divIcon} from "leaflet/dist/leaflet-src.esm";
|
||||
|
||||
export const NwahEntryMarker = Marker.extend({
|
||||
options: {
|
||||
color: '#fff',
|
||||
icon: divIcon({
|
||||
className: 'nwah-entry-marker',
|
||||
iconSize: ['', '']
|
||||
})
|
||||
},
|
||||
|
||||
_initIcon() {
|
||||
Marker.prototype._initIcon.call(this);
|
||||
this._icon.src = "";
|
||||
this._icon.style.backgroundColor = this.options.color;
|
||||
},
|
||||
|
||||
mark() {
|
||||
this._icon.classList.add('mark');
|
||||
},
|
||||
|
||||
unmark() {
|
||||
this._icon.classList.remove('mark');
|
||||
},
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
import {icon} from "leaflet";
|
||||
|
||||
export const NwahIcons = {
|
||||
playerIcon: icon({
|
||||
iconUrl: '/img/arrow.png',
|
||||
iconSize: [15, 25],
|
||||
iconAnchor: [7, 13],
|
||||
className: 'nwah-player'
|
||||
})
|
||||
};
|
@ -0,0 +1,135 @@
|
||||
import {CRS, LatLng, LatLngBounds, map, tileLayer} from 'leaflet'
|
||||
import {NwahIcons} from "./NwahIcons";
|
||||
import {NwahPlayer} from "./NwahPlayer";
|
||||
import {Component} from "preact";
|
||||
import PlayerList from "./components/PlayerList";
|
||||
|
||||
export class NwahMap {
|
||||
constructor({mapUrl, settings, icons, websocketUrl, playerList} = {}) {
|
||||
|
||||
this.players = {};
|
||||
this.mapUrl = mapUrl;
|
||||
this.playerList = playerList;
|
||||
this.settings = settings || {
|
||||
cellSize: 2048,
|
||||
cellOffsetY: -20,
|
||||
cellOffsetX: 8,
|
||||
};
|
||||
this.map = null;
|
||||
this.follow = false;
|
||||
this.icons = icons || NwahIcons;
|
||||
this.websocketUrl = websocketUrl;
|
||||
}
|
||||
|
||||
setFollow(follow) {
|
||||
this.follow = follow;
|
||||
}
|
||||
|
||||
init(element, component) {
|
||||
this.map = map(element, {
|
||||
zoom: 3,
|
||||
center: this.gameCoordsToLatLng(0, 0),
|
||||
crs: CRS.Simple
|
||||
});
|
||||
|
||||
window.nwahMap = this;
|
||||
|
||||
this.map.setMaxBounds(new LatLngBounds(new LatLng(-255, 255), new LatLng(0, 0)));
|
||||
|
||||
this.map.invalidateSize();
|
||||
|
||||
this.tileLayer = tileLayer('/tileset/mwmap/zoom{z}/vvardenfell-{x}-{y}-{z}.jpg', {
|
||||
attribution: 'uesp.net',
|
||||
minZoom: 0,
|
||||
maxZoom: 7,
|
||||
zoomOffset: 10,
|
||||
});
|
||||
|
||||
this.tileLayer.addTo(this.map);
|
||||
|
||||
if (this.websocketUrl) {
|
||||
this.websocket = new WebSocket(this.websocketUrl);
|
||||
this.websocket.onmessage = (message) => {
|
||||
let event = JSON.parse(message.data);
|
||||
|
||||
if (event.type === "playerPosition") {
|
||||
this.updatePositions(event.positions);
|
||||
}
|
||||
|
||||
if (event.type === "fullPlayer") {
|
||||
this.updatePlayers(event.players);
|
||||
component.setState({players: event.players});
|
||||
window.players = event.players;
|
||||
}
|
||||
|
||||
if (this.follow) {
|
||||
this.zoomOnPlayers();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zoomOnPlayers() {
|
||||
let locations = Object.values(this.players)
|
||||
.filter(x => x.isOutside)
|
||||
.map(x => this.gameCoordsToLatLng(x.x, x.y));
|
||||
|
||||
if (locations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let bounds = new LatLngBounds(locations);
|
||||
this.map.fitBounds(bounds);
|
||||
}
|
||||
|
||||
updatePositions(positions) {
|
||||
let knownPlayers = new Set(Object.keys(this.players));
|
||||
for (let pos of positions) {
|
||||
const name = pos.name;
|
||||
knownPlayers.delete(name);
|
||||
if (!(name in this.players)) {
|
||||
this.players[name] = new NwahPlayer(name);
|
||||
this.players[name].addTo(this);
|
||||
}
|
||||
|
||||
let [x, y] = pos.position;
|
||||
this.players[name].update(x, y, pos.rotation, false, pos.isOutside, pos.cell);
|
||||
}
|
||||
|
||||
for (let name of knownPlayers) {
|
||||
this.players[name].remove();
|
||||
delete this.players[name];
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayers(players) {
|
||||
let knownPlayers = new Set(Object.keys(this.players));
|
||||
for (let player of players) {
|
||||
const name = player.name;
|
||||
knownPlayers.delete(name);
|
||||
if (!(name in this.players)) {
|
||||
this.players[name] = new NwahPlayer(name);
|
||||
this.players[name].addTo(this);
|
||||
}
|
||||
|
||||
let {x, y} = player.position;
|
||||
this.players[name].update(x, y, player.rotation.z, true, player.isOutside, player.cell);
|
||||
}
|
||||
|
||||
for (let name of knownPlayers) {
|
||||
this.players[name].remove();
|
||||
delete this.players[name];
|
||||
}
|
||||
}
|
||||
|
||||
gameCoordsToLatLng(x, y) {
|
||||
return new LatLng(
|
||||
(y / this.settings.cellSize) - 128 + this.settings.cellOffsetY,
|
||||
128 + ((x / this.settings.cellSize) + this.settings.cellOffsetX),
|
||||
);
|
||||
}
|
||||
|
||||
show({x, y}) {
|
||||
this.map.panTo(this.gameCoordsToLatLng(x, y));
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import {Marker} from "leaflet";
|
||||
import {divIcon} from "leaflet/dist/leaflet-src.esm";
|
||||
|
||||
export const NwahMarker = Marker.extend({
|
||||
options: {
|
||||
rotation: '0deg',
|
||||
color: '#fff',
|
||||
icon: divIcon({
|
||||
className: 'nwah-player-marker',
|
||||
iconSize: [0, 0],
|
||||
iconAnchor: [5, 10],
|
||||
})
|
||||
},
|
||||
|
||||
setRotation(deg) {
|
||||
this.options.rotation = deg;
|
||||
},
|
||||
|
||||
_initIcon() {
|
||||
Marker.prototype._initIcon.call(this);
|
||||
this._icon.style.borderBottomColor = this.options.color || '#fff';
|
||||
},
|
||||
|
||||
_setPos(pos) {
|
||||
Marker.prototype._setPos.call(this, pos);
|
||||
if (this.options.icon && this.options.icon.options && this.options.icon.options.iconAnchor) {
|
||||
let iconAnchor = this.options.icon.options.iconAnchor;
|
||||
this._icon.style.transformOrigin = `${iconAnchor[0]}px ${iconAnchor[1]}px`;
|
||||
} else {
|
||||
this._icon.style.transformOrigin = 'center bottom';
|
||||
}
|
||||
this._icon.style.transform += ` rotateZ(${this.options.rotation})`
|
||||
}
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import {NwahMarker} from "./NwahMarker";
|
||||
import {colorFromName, normalizeRadians} from "./utils";
|
||||
import {NwahPlayerTrail} from "./NwahPlayerTrail";
|
||||
|
||||
export class NwahPlayer {
|
||||
constructor(name) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.isOutside = false;
|
||||
this.color = colorFromName(name);
|
||||
this.arrow = new NwahMarker([0, 0], {
|
||||
color: this.color,
|
||||
className: 'nwah-player'
|
||||
});
|
||||
this.name = name;
|
||||
|
||||
this.trail = new NwahPlayerTrail({color: this.color});
|
||||
}
|
||||
|
||||
addTo(nwahMap) {
|
||||
this.nwahMap = nwahMap;
|
||||
this.map = nwahMap.map;
|
||||
|
||||
if (this.map) {
|
||||
this.trail.addTo(nwahMap);
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (!this.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.map.removeLayer(this.arrow);
|
||||
this.trail.remove();
|
||||
}
|
||||
|
||||
update(x, y, rot, full = false, isOutside = true, cell = null) {
|
||||
if (isOutside !== this.isOutside) {
|
||||
if (isOutside) {
|
||||
this.map.addLayer(this.arrow);
|
||||
} else {
|
||||
this.map.removeLayer(this.arrow);
|
||||
}
|
||||
|
||||
this.isOutside = isOutside;
|
||||
}
|
||||
|
||||
this.trail.push(x, y, isOutside, cell);
|
||||
|
||||
if (!isOutside) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.rot = rot;
|
||||
|
||||
if (this.map) {
|
||||
this.arrow.setRotation(normalizeRadians(this.rot) + 'rad');
|
||||
this.arrow.setLatLng(this.nwahMap.gameCoordsToLatLng(this.x, this.y));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
import {polyline, marker} from "leaflet";
|
||||
import {NwahEntryMarker} from "./NwahEntryMarker";
|
||||
|
||||
export class NwahPlayerTrail {
|
||||
constructor({color = '#fff'} = {color: '#fff'}) {
|
||||
this.nwahMap = null;
|
||||
this.color = color;
|
||||
this.currentLine = null;
|
||||
this.items = [];
|
||||
this.lastEntry = null;
|
||||
this.isOutside = false;
|
||||
this.lastPosition = null;
|
||||
this.map = null;
|
||||
this.expirationTime = 10 /* min */ * 60 * 1000;
|
||||
}
|
||||
|
||||
addTo(nwahMap) {
|
||||
this.nwahMap = nwahMap;
|
||||
this.map = nwahMap.map;
|
||||
this.items.forEach(line => line.item.addTo(map));
|
||||
}
|
||||
|
||||
push(x, y, isOutside, cell) {
|
||||
if (this.isOutside !== isOutside) {
|
||||
if (this.isOutside) {
|
||||
this.lastEntry = new NwahEntryMarker(this.nwahMap.gameCoordsToLatLng(this.lastPosition.x, this.lastPosition.y), {color: this.color});
|
||||
if (this.map) {
|
||||
this.lastEntry.addTo(this.map);
|
||||
}
|
||||
|
||||
this.lastEntry.mark();
|
||||
this.items.push({
|
||||
type: 'entry',
|
||||
item: this.lastEntry,
|
||||
locations: [{time: performance.now()}],
|
||||
marked: true,
|
||||
});
|
||||
} else {
|
||||
if (this.lastEntry !== null) {
|
||||
this.lastEntry.marked = false;
|
||||
this.lastEntry.unmark();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isOutside = isOutside;
|
||||
|
||||
if (!isOutside) {
|
||||
this.currentLine = null;
|
||||
this.cull();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.lastPosition !== null) {
|
||||
let xDist = (this.lastPosition.x - x);
|
||||
let yDist = (this.lastPosition.y - y);
|
||||
|
||||
if (Math.sqrt((xDist * xDist) + (yDist * yDist)) > this.nwahMap.settings.cellSize) {
|
||||
let teleportLine = polyline([this.nwahMap.gameCoordsToLatLng(this.lastPosition.x, this.lastPosition.y), this.nwahMap.gameCoordsToLatLng(x, y)], {
|
||||
color: this.color,
|
||||
opacity: 0.2,
|
||||
});
|
||||
teleportLine.addTo(this.map);
|
||||
this.items.push({
|
||||
type: 'teleport',
|
||||
item: teleportLine,
|
||||
locations: [{time: performance.now()}],
|
||||
});
|
||||
|
||||
if (this.currentLine) {
|
||||
this.currentLine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.currentLine === null) {
|
||||
this.currentLine = polyline([], {
|
||||
dashArray: '4',
|
||||
color: this.color,
|
||||
});
|
||||
|
||||
this.items.push(this.lastItem = {type: 'path', item: this.currentLine, locations: []});
|
||||
|
||||
if (this.map) {
|
||||
this.currentLine.addTo(this.map);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastPosition = {x, y};
|
||||
let latLng = this.nwahMap.gameCoordsToLatLng(x, y);
|
||||
this.lastItem.locations.push({time: performance.now(), location: latLng});
|
||||
this.lastItem.item.addLatLng(latLng);
|
||||
this.cull();
|
||||
}
|
||||
|
||||
remove() {
|
||||
let items = this.items;
|
||||
this.items = [];
|
||||
items.forEach(x => this.map.removeLayer(x.item));
|
||||
}
|
||||
|
||||
cull() {
|
||||
let i = 0;
|
||||
let goodAfter = performance.now() - this.expirationTime;
|
||||
for (; i < this.items.length; i++) {
|
||||
let item = this.items[i];
|
||||
let okLocs = [];
|
||||
for (let j = 0; j < item.locations.length; j++) {
|
||||
if (item.locations[j].time < goodAfter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
okLocs.push(item.locations[j]);
|
||||
}
|
||||
|
||||
if (okLocs.length === 0) {
|
||||
if (item.marked) {
|
||||
break;
|
||||
}
|
||||
|
||||
this.map.removeLayer(item.item);
|
||||
continue;
|
||||
}
|
||||
|
||||
item.locations = okLocs;
|
||||
|
||||
if (item.type === 'path') {
|
||||
item.item.setLatLngs(item.locations.map(x => x.location));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
this.items = this.items.slice(i);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import {Component} from "preact";
|
||||
|
||||
export default class Bar extends Component {
|
||||
render({current, base, type}, state, context) {
|
||||
const ceilCurrent = Math.ceil(current);
|
||||
const perc = (ceilCurrent / base) * 100;
|
||||
return <div className={`bar ${type}`}>
|
||||
<div className="inner-bar" style={{width: Math.max(perc) + '%'}}>
|
||||
<span className="inner-text">{ceilCurrent}/{base} ({Math.ceil(perc)}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import {Component} from "preact";
|
||||
import Bar from "./Bar";
|
||||
import {colorFromName} from "../utils";
|
||||
|
||||
export default class Player extends Component {
|
||||
state = {
|
||||
head: null,
|
||||
color: null,
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
let player = this.props.player;
|
||||
this.setState({color: colorFromName(player.name)});
|
||||
try {
|
||||
let render = await this.props.renderer.render([player.head, player.hair]);
|
||||
this.setState({head: URL.createObjectURL(render)});
|
||||
} catch (e) {
|
||||
this.setState({head: null});
|
||||
}
|
||||
}
|
||||
|
||||
render({player, map}, {head, color}, context) {
|
||||
return <div className="player">
|
||||
<div className="name" onClick={() => map.show(player.position)}>
|
||||
<span className="dot" style={{backgroundColor: color}}/>
|
||||
{player.name + " "}
|
||||
<span className="level">lvl. {player.level}</span>
|
||||
</div>
|
||||
{head ?
|
||||
<div className="head">
|
||||
<img src={head} alt=""/>
|
||||
</div> : null}
|
||||
<div className="location">
|
||||
{player.isOutside ? null : <><img alt="[Inside]" src="img/house.png"/> <span>{player.cell}</span></>}
|
||||
</div>
|
||||
<div className="health">
|
||||
<Bar current={player.health} base={player.healthBase} type="health"/>
|
||||
</div>
|
||||
<div className="magicka">
|
||||
<Bar current={player.magicka} base={player.magickaBase} type="magicka"/>
|
||||
</div>
|
||||
<div className="fatigue">
|
||||
<Bar current={player.fatigue} base={player.fatigueBase} type="fatigue"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import {Component} from "preact";
|
||||
import Player from "./Player";
|
||||
|
||||
export default class PlayerList extends Component {
|
||||
render({players, renderer, map}) {
|
||||
return <div className="player-list">
|
||||
{players.map(x => <Player key={x.name} player={x} renderer={renderer} map={map}/>)}
|
||||
</div>
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import PlayerList from "./PlayerList";
|
||||
import {Component} from "preact";
|
||||
import {NwahMap} from "../NwahMap";
|
||||
import {HeadRenderer} from "../head-render/HeadRenderer";
|
||||
|
||||
export default class PlayerMap extends Component {
|
||||
state = {
|
||||
players: [],
|
||||
mapElement: document.createElement('div'),
|
||||
renderer: new HeadRenderer(),
|
||||
follow: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
this.state.map = new NwahMap(props);
|
||||
this.state.mapElement.classList.add('map');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.base.appendChild(this.state.mapElement);
|
||||
this.state.map.init(this.state.mapElement, this);
|
||||
this.state.renderer.init(200, 200);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.state.map.setFollow(this.state.follow);
|
||||
}
|
||||
|
||||
render({}, {players, renderer, map, follow}, {}) {
|
||||
return <main>
|
||||
<div className="head">
|
||||
<h2>N'wah map</h2>
|
||||
<span>
|
||||
<label>Follow players
|
||||
<input type="checkbox" checked={follow}
|
||||
onChange={(e) => this.setState({follow: e.target.checked})}/>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div className="sidebar">
|
||||
<PlayerList players={players} renderer={renderer} map={map}/>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
import * as THREE from 'three';
|
||||
import {Color, Group} from 'three';
|
||||
import {ShapeCollection} from "./ShapeCollection";
|
||||
|
||||
export class HeadRenderer {
|
||||
constructor() {
|
||||
this.shapes = {};
|
||||
this.renderQueue = [];
|
||||
this.isRendering = false;
|
||||
this.scene = null;
|
||||
this.renderer = null;
|
||||
this.camera = null;
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
init(width = window.innerWidth, height = window.innerHeight) {
|
||||
if (window.OffscreenCanvas !== undefined) {
|
||||
this.canvas = new OffscreenCanvas(width, height);
|
||||
} else {
|
||||
let canvasElement = document.createElement("canvas");
|
||||
canvasElement.width = width;
|
||||
canvasElement.height = height;
|
||||
this.canvas = canvasElement;
|
||||
}
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: this.canvas,
|
||||
alpha: true
|
||||
});
|
||||
|
||||
this.renderer.setSize(width, height);
|
||||
|
||||
this.camera = new THREE.PerspectiveCamera(70, width / height, 1, 200);
|
||||
this.camera.position.z = 40;
|
||||
|
||||
this.scene = new THREE.Scene();
|
||||
|
||||
let light = new THREE.PointLight(0xffffff, 1);
|
||||
light.position.set(1, 1, 2);
|
||||
|
||||
// Make sure light follow the camera
|
||||
this.camera.add(light);
|
||||
|
||||
// Add camera to scene so light gets rendered
|
||||
this.scene.add(this.camera);
|
||||
|
||||
let ambientLight = new THREE.AmbientLight(0x404040, 1);
|
||||
this.scene.add(ambientLight);
|
||||
}
|
||||
|
||||
async draw() {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
|
||||
return new Promise(async (res, rej) => {
|
||||
window.requestAnimationFrame(async () => {
|
||||
if (this.canvas.convertToBlob) {
|
||||
await this.canvas.convertToBlob({
|
||||
type: 'image/png'
|
||||
}).then(res, rej);
|
||||
} else {
|
||||
this.canvas.toBlob((blob) => {
|
||||
if (blob) return res(blob);
|
||||
return rej();
|
||||
}, 'image/png')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render(objects) {
|
||||
return new Promise(async (res) => {
|
||||
this.renderQueue.push([objects, res]);
|
||||
|
||||
if (!this.isRendering) {
|
||||
await this.startRender();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async startRender() {
|
||||
if (this.isRendering) return;
|
||||
this.isRendering = true;
|
||||
|
||||
this.clear();
|
||||
while (this.renderQueue.length > 0) {
|
||||
let [objects, res] = this.renderQueue.shift();
|
||||
console.log(objects, res);
|
||||
let shapePromises = objects.map(async x => {
|
||||
let req = await fetch(`blob/shape/${x}.json`);
|
||||
let json = await req.json();
|
||||
let shapeColl = await ShapeCollection.forceLoad(json, 'blob/texture/');
|
||||
this.add(shapeColl);
|
||||
});
|
||||
await Promise.all(shapePromises);
|
||||
this.rotate(-5 * (Math.PI / 180), -30 * (Math.PI / 180), 0);
|
||||
res(await this.draw());
|
||||
this.clear();
|
||||
}
|
||||
|
||||
this.isRendering = false;
|
||||
}
|
||||
|
||||
add(shapeCollection) {
|
||||
this.shapes[shapeCollection.id] = shapeCollection;
|
||||
this.scene.add(shapeCollection.getGroup());
|
||||
}
|
||||
|
||||
remove(shapeCollection) {
|
||||
delete this.shapes[shapeCollection.id];
|
||||
this.scene.remove(shapeCollection.getGroup());
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (let shape of Object.values(this.shapes)) {
|
||||
this.remove(shape)
|
||||
}
|
||||
}
|
||||
|
||||
expose() {
|
||||
window.scene = this.scene;
|
||||
window.camera = this.camera;
|
||||
window.renderer = this.renderer;
|
||||
}
|
||||
|
||||
rotate(x, y, z) {
|
||||
for (let shape of Object.values(this.shapes)) {
|
||||
|
||||
let group = shape.getGroup();
|
||||
group.rotateX(x);
|
||||
group.rotateY(y);
|
||||
group.rotateZ(z);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
import {
|
||||
Face3,
|
||||
Geometry, Matrix4,
|
||||
Mesh, MeshPhongMaterial,
|
||||
RepeatWrapping,
|
||||
TextureLoader,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from 'three'
|
||||
import {uuidv4} from "../utils";
|
||||
|
||||
export class Shape {
|
||||
constructor({
|
||||
vertices = [],
|
||||
faces = [],
|
||||
texture = null,
|
||||
uvSets = [],
|
||||
normals = [],
|
||||
translation = [0, 0, 0],
|
||||
rotation
|
||||
} = {}, filePrefix = "", onLoad = () => {}, forceLoad = false) {
|
||||
|
||||
this.id = uuidv4();
|
||||
|
||||
this.vertices = vertices;
|
||||
this.translation = translation;
|
||||
this.rotation = rotation;
|
||||
this.faces = faces;
|
||||
this.textureInfo = texture;
|
||||
this.normals = normals;
|
||||
this.uvSets = uvSets;
|
||||
|
||||
this.filePrefix = filePrefix;
|
||||
this.isLoaded = false;
|
||||
this.onLoad = onLoad;
|
||||
|
||||
this.geometry = null;
|
||||
this.mesh = null;
|
||||
this.texture = null;
|
||||
this.material = null;
|
||||
|
||||
if (forceLoad) {
|
||||
this.getMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
getMaterial() {
|
||||
if (this.material) {
|
||||
return this.material;
|
||||
}
|
||||
|
||||
const params = {
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
alphaTest: 0.2,
|
||||
};
|
||||
if (this.textureInfo) {
|
||||
this.texture = new TextureLoader().load(this.filePrefix + this.textureInfo.file, () => {
|
||||
this.isLoaded = true;
|
||||
this.onLoad();
|
||||
}, null, (err) => {
|
||||
console.log(err)
|
||||
});
|
||||
|
||||
if (this.textureInfo.wrapS) {
|
||||
this.texture.wrapS = RepeatWrapping;
|
||||
}
|
||||
|
||||
if (this.textureInfo.wrapT) {
|
||||
this.texture.wrapT = RepeatWrapping;
|
||||
}
|
||||
|
||||
this.texture.flipY = false;
|
||||
params.map = this.texture;
|
||||
}
|
||||
|
||||
return this.material = new MeshPhongMaterial(params);
|
||||
}
|
||||
|
||||
getGeometry() {
|
||||
if (this.geometry) {
|
||||
return this.geometry;
|
||||
}
|
||||
|
||||
const geometry = this.geometry = new Geometry();
|
||||
|
||||
for (let [x, y, z] of this.vertices) {
|
||||
geometry.vertices.push(new Vector3(x, y, z))
|
||||
}
|
||||
|
||||
const uvSets = [];
|
||||
|
||||
for (let i = 0; i < this.uvSets.length; i++) {
|
||||
uvSets.push([]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.faces.length; i++) {
|
||||
let [a, b, c] = this.faces[i];
|
||||
|
||||
// Add faces with normals
|
||||
// Normals are stored per vector
|
||||
geometry.faces.push(new Face3(a, b, c, [
|
||||
new Vector3(this.normals[a][0], this.normals[a][1], this.normals[a][2]),
|
||||
new Vector3(this.normals[b][0], this.normals[b][1], this.normals[b][2]),
|
||||
new Vector3(this.normals[c][0], this.normals[c][1], this.normals[c][2])
|
||||
]));
|
||||
|
||||
for (let j = 0; j < this.uvSets.length; j++) {
|
||||
uvSets[j].push([
|
||||
new Vector2(this.uvSets[j][a][0], this.uvSets[j][a][1]),
|
||||
new Vector2(this.uvSets[j][b][0], this.uvSets[j][b][1]),
|
||||
new Vector2(this.uvSets[j][c][0], this.uvSets[j][c][1]),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
geometry.faceVertexUvs = uvSets;
|
||||
|
||||
// First rotate
|
||||
let m = new Matrix4();
|
||||
m.makeBasis(new Vector3(...this.rotation[0]), new Vector3(...this.rotation[1]), new Vector3(...this.rotation[2]));
|
||||
geometry.applyMatrix4(m);
|
||||
|
||||
// Then translate
|
||||
geometry.translate(this.translation[0], this.translation[1], this.translation[2]);
|
||||
|
||||
// Scale up a bit
|
||||
geometry.scale(2, 2, 2);
|
||||
|
||||
// Rotate the face so it faces us \o/
|
||||
geometry.rotateX(290 * (Math.PI / 180));
|
||||
|
||||
geometry.computeFaceNormals();
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
return this.geometry;
|
||||
}
|
||||
|
||||
getMesh() {
|
||||
if (this.mesh) {
|
||||
return this.mesh;
|
||||
}
|
||||
|
||||
this.mesh = new Mesh(this.getGeometry(), this.getMaterial());
|
||||
return this.mesh;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import {Shape} from "./Shape";
|
||||
import {Group} from "three";
|
||||
import {uuidv4} from "../utils";
|
||||
|
||||
export class ShapeCollection {
|
||||
constructor({shapes = [], name = ""}, filePrefix = "", onLoad = () => {
|
||||
}, forceLoad = false) {
|
||||
this.id = uuidv4();
|
||||
let loaded = 0;
|
||||
this.shapes = shapes.map((s) => new Shape(s, filePrefix, () => {
|
||||
loaded++;
|
||||
|
||||
if (loaded === shapes.length) {
|
||||
this.isLoaded = true;
|
||||
this.onLoad();
|
||||
}
|
||||
}, forceLoad));
|
||||
|
||||
this.isLoaded = false;
|
||||
this.onLoad = onLoad;
|
||||
this.group = null;
|
||||
|
||||
if (this.shapes.length === 0) {
|
||||
this.onLoad();
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
static forceLoad(shapeCollection, filePrefix) {
|
||||
return new Promise((res, rej) => {
|
||||
let shapeColl = new ShapeCollection(shapeCollection, filePrefix, () => {
|
||||
res(shapeColl)
|
||||
}, true)
|
||||
});
|
||||
}
|
||||
|
||||
getGroup() {
|
||||
if (this.group) {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
// Create group for all shapes
|
||||
this.group = new Group();
|
||||
|
||||
for (let shape of this.shapes) {
|
||||
this.group.add(shape.getMesh())
|
||||
}
|
||||
|
||||
return this.group
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import {render} from 'preact'
|
||||
import PlayerMap from "./components/PlayerMap";
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
render(<PlayerMap websocketUrl={`ws://${location.host}/ws/players`}/>, document.body);
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
export const PI2 = Math.PI * 2;
|
||||
|
||||
export function normalizeRadians(rad) {
|
||||
return ((rad + Math.PI) % PI2) - Math.PI
|
||||
}
|
||||
|
||||
// I found this on the internet
|
||||
export function uuidv4() {
|
||||
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
||||
const COLORS = [
|
||||
'#fc5c65',
|
||||
'#fd9644',
|
||||
'#fed330',
|
||||
'#26de81',
|
||||
'#2bcbba',
|
||||
'#eb3b5a',
|
||||
'#fa8231',
|
||||
'#f7b731',
|
||||
'#20bf6b',
|
||||
'#0fb9b1',
|
||||
'#45aaf2',
|
||||
'#4b7bec',
|
||||
'#a55eea',
|
||||
'#d1d8e0',
|
||||
'#778ca3',
|
||||
'#2d98da',
|
||||
'#3867d6',
|
||||
'#8854d0',
|
||||
'#a5b1c2',
|
||||
'#4b6584',
|
||||
];
|
||||
|
||||
export function colorFromName(name) {
|
||||
let nr = name
|
||||
.split("")
|
||||
.map(x => (x.charCodeAt(0) - 97))
|
||||
.reduce((a, b) => a + b);
|
||||
|
||||
return COLORS[nr % COLORS.length];
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
const path = require('path');
|
||||
const webpack = require("webpack");
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.js',
|
||||
mode: 'development',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'public/js/dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
React: 'react'
|
||||
})
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [["@babel/preset-env", {
|
||||
"targets": {
|
||||
"browsers": ["last 2 versions"]
|
||||
},
|
||||
exclude: ['transform-regenerator'],
|
||||
modules: false
|
||||
}]],
|
||||
plugins: [
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
'@babel/plugin-proposal-class-properties',
|
||||
'@babel/plugin-transform-runtime'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resolve": {
|
||||
"alias": {
|
||||
"react": "preact/compat",
|
||||
"react-dom/test-utils": "preact/test-utils",
|
||||
"react-dom": "preact/compat",
|
||||
},
|
||||
}
|
||||
};
|