burguillos.info/js-src/conquer/index.ts

585 lines
20 KiB
TypeScript

import Map from "ol/Map"
import MapEvent from "ol/MapEvent"
import MapBrowserEvent from "ol/MapBrowserEvent"
import View from "ol/View"
import Projection from "ol/proj/Projection.js"
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import * as olProj from "ol/proj"
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Stroke from 'ol/style/Stroke'
import Fill from 'ol/style/Fill'
import CircleStyle from 'ol/style/Circle'
import Icon from 'ol/style/Icon'
import Style from 'ol/style/Style'
import ConquerLogin from '@burguillosinfo/conquer/login'
import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
import SelfPlayerUI from '@burguillosinfo/conquer/interface/self-player'
import CreateNode from '@burguillosinfo/conquer/create-node'
import MapState from '@burguillosinfo/conquer/map-state'
import MapNode from '@burguillosinfo/conquer/map-node'
import NewNodeUI from '@burguillosinfo/conquer/interface/new-node'
import NewTeamUI from '@burguillosinfo/conquer/interface/new-team'
import WebSocket from '@burguillosinfo/conquer/websocket'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import ConquerUser from '@burguillosinfo/conquer/user'
import FightSelectorSlide from '@burguillosinfo/conquer/fight-selector-slide';
import SelectFightUI from '@burguillosinfo/conquer/interface/select-fight';
import ConquerUserCurrentEnemy from '@burguillosinfo/conquer/user-current-enemy'
type StylesInterface = Record<string, Style>
export default class Conquer {
private conquerContainer: HTMLDivElement
private map: Map
private enabledOnRotate = true
private rotation = 0;
private currentLongitude: number
private intervalSendCoordinates: number | null = null;
private currentLatitude: number
private rotationOffset = 0
private heading = 0
private disableSetRotationOffset = false
private currentPositionFeature: Feature | null
private vectorLayer: VectorLayer<VectorSource> | null = null
private alpha = 0
private beta = 0
private gamma = 0
private conquerLogin: ConquerLogin
private selfPlayerUI: SelfPlayerUI | null = null
private interfaceManager: InterfaceManager
private firstSetCenter = true
private firstSetRotation = true
private state: MapState = MapState.NOTHING
private createNodeObject: CreateNode
private serverNodes: Record<string, MapNode> = {}
private coordinate_1 = 0;
private coordinate_2 = 0;
private fightSelectorSlide: FightSelectorSlide;
private loggedIn = false;
public getServerNodes(): Record<string, MapNode> {
return this.serverNodes
}
public getState(): MapState {
return this.state
}
public setState(state: MapState) {
this.state = state
this.refreshState()
}
public removeState(state: MapState) {
this.state &= ~state
this.refreshState()
}
public addState(state: MapState) {
this.state |= state
this.refreshState()
}
private refreshFightSlide(): void {
if (this.loggedIn && (this.getState() & MapState.NORMAL) !== 0) {
this.fightSelectorSlide.show();
}
if (!this.loggedIn) {
this.fightSelectorSlide.hide();
}
}
private refreshState(): void {
this.refreshFightSlide();
this.createNodeObject.refreshState()
return
}
static start() {
const conquerContainer = document.querySelector(".conquer-container")
if (conquerContainer === null || !(conquerContainer instanceof HTMLDivElement)) {
Conquer.fail('.conquer-container is not a div.')
}
const conquer = new Conquer(conquerContainer)
conquer.run()
}
setCenterDisplaced(lat: number, lon: number) {
if (this.firstSetCenter || !(this.state & MapState.FREE_MOVE)) {
this.coordinate_1 = lon;
this.coordinate_2 = lat;
const olCoordinates = this.realCoordinatesToOl(lat, lon)
const size = this.map.getSize()
if (size === undefined) {
return
}
this.map.getView().centerOn(olCoordinates, size, [size[0]/2, size[1]-60])
this.firstSetCenter = false
}
}
static fail(error: string): never {
alert('Error de interfaz')
throw new Error(error)
}
public isStateCreatingNode(): boolean {
return !!(this.getState() & MapState.CREATE_NODE)
}
public isStateSelectWhereToCreateNode(): boolean {
return !!(this.getState() & MapState.SELECT_WHERE_TO_CREATE_NODE)
}
private createNodeCounter = 0
async onClickWhereToCreateNode(event: MapEvent) {
if (!(event instanceof MapBrowserEvent)) {
return
}
const pixel = event.pixel
const coordinates = this.map.getCoordinateFromPixel(pixel)
const newNodeUI = new NewNodeUI(coordinates)
const oldState = this.getState();
newNodeUI.on('close', () => {
this.interfaceManager.remove(newNodeUI)
this.setState(oldState);
})
this.interfaceManager.push(newNodeUI)
this.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
}
private isStateFillingFormCreateNode(): boolean {
return !!(this.getState() & MapState.FILLING_FORM_CREATE_NODE)
}
async onClickMap(event: MapEvent): Promise<void> {
if (this.isStateCreatingNode() && this.isStateSelectWhereToCreateNode()) {
this.onClickWhereToCreateNode(event)
}
if (!(this.getState() & MapState.NORMAL)) {
return
}
if (this.vectorLayer === null) {
return
}
if (!(event instanceof MapBrowserEvent)) {
return
}
if (event.dragging) {
return
}
const pixel = event.pixel
const features = this.map.getFeaturesAtPixel(pixel)
const feature = features.length ? features[0] : undefined
if (feature === undefined) {
return
}
if (!(feature instanceof Feature)) {
return
}
this.onClickFeature(feature)
}
async onClickSelf(): Promise<void> {
if (!(this.state & MapState.NORMAL)) {
return
}
const selfPlayerUI = new SelfPlayerUI(!!(this.getState() & (MapState.FREE_MOVE)))
selfPlayerUI.on('close', () => {
this.interfaceManager.remove(selfPlayerUI)
})
selfPlayerUI.on('enable-explorer-mode', () => {
this.addState(MapState.FREE_MOVE);
});
selfPlayerUI.on('disable-explorer-mode', () => {
this.removeState(MapState.FREE_MOVE);
});
selfPlayerUI.on('createNodeStart', () => {
this.addState(MapState.CREATE_NODE)
this.removeState(MapState.NORMAL)
})
selfPlayerUI.on('open-create-team', () => {
this.onOpenCreateTeam();
});
this.interfaceManager.push(selfPlayerUI)
this.selfPlayerUI = selfPlayerUI
}
private onOpenCreateTeam(): void {
const newTeamUI = new NewTeamUI();
newTeamUI.on('close', () => {
this.interfaceManager.remove(newTeamUI);
});
this.interfaceManager.push(newTeamUI);
}
private isFeatureEnabledMap: Record<string, boolean> = {}
async onClickFeature(feature: Feature): Promise<void> {
if (this.isFeatureEnabledMap[feature.getProperties().type] === undefined) {
this.isFeatureEnabledMap[feature.getProperties().type] = true
}
if (!this.isFeatureEnabledMap[feature.getProperties().type]) {
return
}
this.isFeatureEnabledMap[feature.getProperties().type] = false
window.setTimeout(() => {
this.isFeatureEnabledMap[feature.getProperties().type] = true
}, 100);
const candidateNode = this.getServerNodes()[feature.getProperties().type];
if (candidateNode !== undefined) {
candidateNode.click(this.interfaceManager);
return;
}
if (feature === this.currentPositionFeature) {
this.onClickSelf()
return
}
}
async onLoginSuccess(): Promise<void> {
this.loggedIn = true;
this.refreshFightSlide();
this.clearIntervalSendCoordinates();
this.createIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
this.createIntervalPollNearbyNodes();
}
private intervalPollNearbyNodes: number | null = null;
private clearIntervalPollNearbyNodes(): void {
if (this.intervalPollNearbyNodes !== null) {
window.clearInterval(this.intervalPollNearbyNodes)
this.intervalPollNearbyNodes = null;
}
}
private async getNearbyNodes(): Promise<void> {
const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let response;
try {
response = await fetch(urlNodes);
} catch (error) {
console.error(error);
return;
}
let responseBody;
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
const serverNodes: Record<string, MapNode> = {};
const nodes = JsonSerializer.deserialize(responseBody, MapNode);
if (!(nodes instanceof Array)) {
console.error('Received null instead of node list.');
return;
}
for (const node of nodes) {
if (!(node instanceof MapNode)) {
console.error('Received node is not a MapNode.');
continue;
}
node.on('update-nodes', async () => {
await this.sendCoordinatesToServer();
this.getNearbyNodes();
});
serverNodes[node.getId()] = node;
}
this.serverNodes = serverNodes;
this.refreshLayers();
}
private createIntervalPollNearbyNodes(): void {
this.getNearbyNodes();
this.intervalPollNearbyNodes = window.setInterval(() => {
this.getNearbyNodes();
}, 40000)
}
private createIntervalSendCoordinates(): void {
this.intervalSendCoordinates = window.setInterval(() => {
this.sendCoordinatesToServer();
}, 40000);
}
private async sendCoordinatesToServer(): Promise<void> {
const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let res;
try {
res = await fetch(urlLog, {
method: 'POST',
body: JSON.stringify([
this.coordinate_1,
this.coordinate_2,
])});
} catch (error) {
console.error(error)
return;
}
let responseBody;
try {
responseBody = await res.json();
} catch(error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (res.status !== 200) {
console.error(responseBody.error);
}
}
private runPreStartState(): void {
const createNodeObject = new CreateNode(this)
this.createNodeObject = createNodeObject
const interfaceManager = new InterfaceManager()
this.interfaceManager = interfaceManager
const conquerLogin = new ConquerLogin(interfaceManager)
conquerLogin.on('login', () => {
this.onLoginSuccess();
});
conquerLogin.on('logout', () => {
this.onLogout();
});
conquerLogin.start()
this.conquerLogin = conquerLogin
this.fightSelectorSlide = new FightSelectorSlide();
this.fightSelectorSlide.on('global-battle', () => {
this.startGlobalBattleSelector();
});
this.fightSelectorSlide.startHook();
}
private async startGlobalBattleSelector(): Promise<void> {
const enemies = await ConquerUserCurrentEnemy.getGlobalEnemies();
if (enemies !== null) {
const selectFightUI = new SelectFightUI(enemies);
selectFightUI.on('close', () => {
this.interfaceManager.remove(selectFightUI);
});
this.interfaceManager.push(selectFightUI);
}
}
private onLogout(): void {
this.loggedIn = false;
this.refreshFightSlide();
this.clearIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
}
private clearIntervalSendCoordinates(): void {
if (this.intervalSendCoordinates !== null) {
window.clearInterval(this.intervalSendCoordinates);
this.intervalSendCoordinates = null;
}
}
async run() {
this.runPreStartState()
this.setState(MapState.NORMAL | MapState.FREE_ROTATION)
const conquerContainer = this.conquerContainer
//layer.on('prerender', (evt) => {
// // return
// if (evt.context) {
// const context = evt.context as CanvasRenderingContext2D
// context.filter = 'grayscale(80%) invert(100%) '
// context.globalCompositeOperation = 'source-over'
// }
//})
//layer.on('postrender', (evt) => {
// if (evt.context) {
// const context = evt.context as CanvasRenderingContext2D
// context.filter = 'none'
// }
//})
olProj.useGeographic()
const osm = new OSM()
osm.setUrls([`${window.location.protocol}//${window.location.hostname}:${window.location.port}/conquer/tile/{z}/{x}/{y}.png`])
this.map = new Map({
target: conquerContainer,
layers: [
new TileLayer({
source: osm
})
],
view: new View({
zoom: 19,
maxZoom: 22,
}),
})
this.setLocationChangeTriggers()
this.setRotationChangeTriggers()
}
setRotationChangeTriggers(): void {
if (window.DeviceOrientationEvent) {
window.addEventListener("deviceorientation", (event) => {
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
this.onRotate(event.alpha, event.beta, event.gamma)
}
}, true)
}
}
addCurrentLocationMarkerToMap(currentLatitude: number,
currentLongitude: number) {
const currentPositionFeature = new Feature({
type: 'currentPositionFeature',
geometry: new Point(this.realCoordinatesToOl(currentLatitude, currentLongitude))
})
this.currentPositionFeature = currentPositionFeature
}
processLocation(location: GeolocationPosition) {
this.currentLatitude = location.coords.latitude
this.currentLongitude = location. coords.longitude
if (location.coords.heading !== null && (this.alpha != 0 || this.beta != 0 || this.gamma != 0) && !this.disableSetRotationOffset) {
this.disableSetRotationOffset = true
this.heading = location.coords.heading
this.rotationOffset = this.compassHeading(this.alpha, this.beta, this.gamma) + (location.coords.heading*Math.PI*2)/360
}
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
this.addCurrentLocationMarkerToMap(this.currentLatitude, this.currentLongitude)
this.refreshLayers()
}
private async refreshLayers(): Promise<void> {
if (this.currentPositionFeature === null) {
return
}
const user = await ConquerUser.getSelfUser()
let color = 'white';
if (user !== null) {
const team = await user.getTeam();
if (team !== null) {
color = team.getColor();
}
}
const styles: StylesInterface = {
currentPositionFeature: new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: '/img/arrow-player.svg',
color: color,
scale: 0.2,
rotation: this.rotation,
rotateWithView: true,
}),
zIndex: 4,
})
};
const features = [];
features.push(this.currentPositionFeature);
for (const key in this.getServerNodes()) {
styles[key] = await this.getServerNodes()[key].getStyle()
features.push(this.getServerNodes()[key].getFeature())
}
const vectorLayer = new VectorLayer<VectorSource>({
source: new VectorSource({
features: features
}),
})
if (this.vectorLayer !== null) {
this.map.removeLayer(this.vectorLayer)
this.vectorLayer = null;
}
vectorLayer.setStyle((feature) => {
return styles[feature.getProperties().type]
})
this.map.addLayer(vectorLayer)
this.vectorLayer = vectorLayer
this.map.on('click', (event: MapEvent) => {
this.onClickMap(event)
})
}
setLocationChangeTriggers(): void {
window.setInterval(() => {
this.disableSetRotationOffset = false
}, 10000)
this.currentPositionFeature = null
window.setTimeout(() => {
window.setInterval(() => {
navigator.geolocation.getCurrentPosition((location) => {
this.processLocation(location)
}, () => {
return
}, {
enableHighAccuracy: true,
})
}, 3000)
}, 1000)
// const initialLatitude = 37.58237
//const initialLongitude = -5.96766
const initialLongitude = 2.500845037550267
const initialLatitude = 48.81050698635832
this.setCenterDisplaced(initialLatitude, initialLongitude)
this.addCurrentLocationMarkerToMap(initialLatitude, initialLongitude)
this.refreshLayers()
navigator.geolocation.watchPosition((location) => {
this.processLocation(location)
}, (err) => {
return
}, {
enableHighAccuracy: true,
})
}
realCoordinatesToOl(lat: number, lon: number): number[] {
return olProj.transform(
[lon, lat],
new Projection({ code: "WGS84" }),
new Projection({ code: "EPSG:900913" }),
)
}
compassHeading(alpha:number, beta:number, gamma:number): number {
const alphaRad = alpha * (Math.PI / 180)
return alphaRad
}
logToServer(logValue: string) {
const urlLog = new URL('/conquer/log', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
urlLog.searchParams.append('log', logValue)
fetch(urlLog).then(() => {
return
}).catch((error) => {
console.error(error)
})
}
onRotate(alpha: number, beta: number, gamma: number) {
if (this.enabledOnRotate) {
this.alpha = alpha
this.beta = beta
this.gamma = gamma
this.enabledOnRotate = false
this.rotation = -(this.compassHeading(alpha, beta, gamma) - this.rotationOffset);
if (this.currentPositionFeature !== null) {
this.currentPositionFeature.changed();
}
if (this.firstSetRotation || !(this.state & MapState.FREE_ROTATION)) {
this.map.getView().setRotation((this.compassHeading(alpha, beta, gamma) - this.rotationOffset))
this.firstSetRotation = false
}
window.setTimeout(() => {
this.enabledOnRotate = true
}, 10)
}
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
}
constructor(conquerContainer: HTMLDivElement) {
this.conquerContainer = conquerContainer
}
}