Compare commits

...

40 Commits

Author SHA1 Message Date
a2999657a8 Fixing unclosed tag. 2024-02-10 21:14:14 +01:00
003f4972cb Adding inital select battle support. 2024-01-24 01:09:33 +01:00
34723db31c Adding initial battle bar. 2024-01-23 20:19:26 +01:00
58c30c1059 Addding initial support for listing enemies. 2024-01-22 01:08:28 +01:00
08f539bf42 Improving navigation and using an arrow icon instead of a circle for the player. 2024-01-15 20:10:57 +01:00
c945cc453b Adding initial arrow player support. 2024-01-15 16:24:00 +01:00
40e392e003 Improving the number of requests for team. 2024-01-14 22:17:54 +01:00
278c7c5112 Adding refreshLayers after betraying your team. 2024-01-14 22:07:59 +01:00
389f325618 Adding initial support for conquering nodes. 2024-01-14 22:02:49 +01:00
16888b9fdb Migrating to postgis. 2024-01-14 05:45:56 +01:00
5f70116da2 Splitting templates in files. 2024-01-14 04:36:36 +01:00
30188a1a76 Completed ability to choose a node as player. 2024-01-14 04:18:14 +01:00
35a65cfa1f Adding the UI to select a team. 2024-01-13 23:14:14 +01:00
e3708066e6 Finished create team endpoint and interface. 2024-01-13 20:29:40 +01:00
ed48de1c38 Creating the create team interface. 2024-01-13 19:59:11 +01:00
589782365b Adding initial support to looking at you team data on profile. 2024-01-13 19:10:40 +01:00
9279e6388a Adding initial team support. 2024-01-13 01:17:57 +01:00
c38474614d Adding initial support for showing node contents. 2024-01-01 01:36:10 +01:00
7994119d66 First bugfix of the year, actually removing free move from status. 2024-01-01 00:53:37 +01:00
ac21ac1387 Adding the ability to free move in the map. 2023-12-31 21:48:11 +01:00
d9e6e664f2 Splitting templates to different files. 2023-12-31 20:56:07 +01:00
8c27095ad1 Adding the capability of create nodes. 2023-12-31 20:43:53 +01:00
85a104caa5 Adding initial js support of protobuf. 2023-12-02 17:22:27 +01:00
24b4f7db9f Merge branch 'main' of git.owlcode.tech:sergiotarxz/burguillos.info into conquer 2023-12-02 15:45:59 +01:00
711f1dc845 Adding tile cache and preprocess. 2023-11-29 19:43:56 +01:00
598dda2aae Merge branch 'main' of git.owlcode.tech:sergiotarxz/burguillos.info into conquer
Conflicts:
      js-src/index.js
      public/js/bundle.js
2023-11-29 18:09:14 +01:00
acec248f4d Adding initial create node support. 2023-11-28 21:10:12 +01:00
e5d9230a74 Adding files I forgot to commit. 2023-11-23 00:41:10 +01:00
d4927e2e1b Improving login workflow to make extensible UI. 2023-11-23 00:35:28 +01:00
d73ff6692a Adding semi-opaque overlay to avoid interation with the app without login. 2023-11-21 19:23:51 +01:00
90d85ed4af Splitting login logic to a separated file to make the code easier to work with. 2023-11-21 19:16:30 +01:00
064ec75ed3 Adding the ability to click on things. 2023-11-21 12:53:58 +01:00
f3f111060b Improving readbility of the javascript. 2023-11-20 21:21:50 +01:00
d6d827fe8d Working login. 2023-11-20 20:20:04 +01:00
1447b2fa6e Adding initial login support. 2023-11-19 23:14:02 +01:00
61b0066f0a Improving the serialize to owner method. 2023-11-19 19:44:09 +01:00
3396c36529 Adding first working sign up. 2023-11-19 19:26:59 +01:00
21d9f46d03 Initial create user support. 2023-11-17 23:16:54 +01:00
2d1430ca87 Multiple bug fixes and adding initial UI support for register and login. 2023-11-13 21:13:21 +01:00
dd2ca2f786 Adding initial conquer support. 2023-11-13 17:32:12 +01:00
65 changed files with 6757 additions and 27 deletions

View File

@ -27,6 +27,13 @@ my $build = Module::Build->new(
'Module::Pluggable' => 0,
'List::AllUtils' => 0,
'Lingua::Stem::Snowball' => 0,
'Mojo::Redis' => 0,
'DBIx::Class' => 0,
'UUID::URandom' => 0,
'Crypt::Bcrypt' => 0,
'DBIx::Class::TimeStamp' => 0,
'DateTime::Format::HTTP' => 0,
'GIS::Distance' => 0,
},
);
$build->create_build_script;

11
generate_proto.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
rm -rf ./js-src/generated/
mkdir ./js-src/generated/
protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
--plugin="protoc-gen-js=./node_modules/.bin/protoc-gen-js" \
--ts_opt=esModuleInterop=true \
--js_out="import_style=commonjs,binary:./js-src/generated" \
--ts_out="./js-src/generated" \
--proto_path="proto" \
$(find proto/ -name '*.proto')

View File

@ -0,0 +1,67 @@
import Conquer from '@burguillosinfo/conquer'
import MapState from '@burguillosinfo/conquer/map-state'
export default class CreateNode {
private conquer: Conquer
private createNodeSlide: HTMLElement
constructor(conquer: Conquer) {
this.conquer = conquer
this.getCreateNodeCancel().addEventListener('click', () => {
this.conquer.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
this.conquer.removeState(MapState.CREATE_NODE)
this.conquer.addState(MapState.NORMAL)
})
this.getCreateNodeNewNodeElement().addEventListener('click', () => {
const state = this.conquer.getState()
if (state & MapState.SELECT_WHERE_TO_CREATE_NODE) {
this.conquer.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
return
}
this.conquer.addState(MapState.SELECT_WHERE_TO_CREATE_NODE)
})
}
private getCreateNodeCancel(): HTMLElement {
const createNodeCancel = document.querySelector('#create-node-exit')
if (createNodeCancel === null || !(createNodeCancel instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-exit.')
}
return createNodeCancel
}
private getCreateNodeNewNodeElement(): HTMLElement {
const createNodeNewElement = document.querySelector('#create-node-new-node')
if (createNodeNewElement === null || !(createNodeNewElement instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-slide.')
}
return createNodeNewElement
}
private getCreateNodeSlide(): HTMLElement {
const createNodeSlide = document.querySelector('#create-node-slide')
if (createNodeSlide === null || !(createNodeSlide instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-slide.')
}
return createNodeSlide
}
public refreshState() {
if (!(this.conquer.getState() & MapState.CREATE_NODE)) {
this.getCreateNodeSlide().classList.add('conquer-display-none')
return
}
this.refreshCreateNodeNewNodeState()
this.getCreateNodeSlide().classList.remove('conquer-display-none')
}
private refreshCreateNodeNewNodeState(): void {
const createNodeNewNode = this.getCreateNodeNewNodeElement()
if (this.conquer.getState() & MapState.SELECT_WHERE_TO_CREATE_NODE) {
createNodeNewNode.innerText = 'Cancelar.'
} else {
createNodeNewNode.innerText = 'Crear nodo.'
}
}
}

View File

@ -0,0 +1,53 @@
import Conquer from '@burguillosinfo/conquer'
export default class FightSelectorSlide {
private callbacks: Record<string, Array<() => void>> = {}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
private runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
private getSelectorSlide(): HTMLElement {
const selectorSlide = document.querySelector('#fight-battle-selector-slide');
if (!(selectorSlide instanceof HTMLElement)) {
Conquer.fail('selectorSlide is not HTMLElement');
}
return selectorSlide;
}
public startHook(): void {
this.createEventListeners();
}
public getGlobalBattleButton(): HTMLElement {
const globalBattleButton = this.getSelectorSlide().querySelector('button.fight-global-button');
if (!(globalBattleButton instanceof HTMLElement)) {
Conquer.fail('globalBattleButton is not HTMLElement');
}
return globalBattleButton;
}
private createEventListeners(): void {
const globalBattleButton = this.getGlobalBattleButton();
globalBattleButton.addEventListener('click', () => {
this.runCallbacks('global-battle');
});
}
public show(): void {
this.getSelectorSlide().classList.remove('conquer-display-none');
}
public hide(): void {
this.getSelectorSlide().classList.add('conquer-display-none');
}
}

584
js-src/conquer/index.ts Normal file
View File

@ -0,0 +1,584 @@
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
}
}

View File

@ -0,0 +1,61 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default class ConquerInterfaceManager {
private interfaces: ConquerInterface[] = []
public push(conquerInterface: ConquerInterface) {
const nodesForInterface = conquerInterface.getNodes()
for (const nodeInInterface of nodesForInterface) {
nodeInInterface.id = ""
document.body.appendChild(nodeInInterface)
}
this.interfaces.push(conquerInterface)
conquerInterface.run()
let startInterface = this.interfaces.length - 2;
if (startInterface < 0) {
startInterface = 0
}
this.recalculateAllZIndexes(startInterface)
}
public remove(conquerInterface: ConquerInterface) {
for (let i = this.interfaces.length - 1; i >= 0; i--) {
if (conquerInterface !== this.interfaces[i]) {
continue
}
this.interfaces.splice(i, 1)
for (const nodeToDelete of conquerInterface.getNodes()) {
document.body.removeChild(nodeToDelete)
}
conquerInterface.prune()
this.recalculateAllZIndexes()
}
}
private recalculateAllZIndexes(start = 0) : void {
let currentZindex = 5;
if (start < 0) {
Conquer.fail('ConquerInterfaceManager.recalculateAllZIndexes must not be passed negative values.')
}
if (start > 0) {
const lastInterface = this.interfaces[start-1];
if (lastInterface === undefined) {
Conquer.fail('Last interface should not be null, dying...')
}
const lastInterfaceNodes = lastInterface.getNodes()
const lastInterfaceLastNode = lastInterfaceNodes[lastInterfaceNodes.length-1]
if (lastInterfaceLastNode === undefined) {
Conquer.fail('Last interface last node should not be null, dying...')
}
currentZindex = parseInt(lastInterfaceLastNode.style.zIndex)
}
for (let i = start; i < this.interfaces.length; i++) {
const conquerInterface = this.interfaces[i]
for (const node of conquerInterface.getNodes()) {
node.style.zIndex = currentZindex + ''
currentZindex++
}
}
}
}

View File

@ -0,0 +1,53 @@
import Conquer from '@burguillosinfo/conquer'
export default abstract class ConquerInterface {
private alreadyGenerated = false
private nodes: HTMLElement[]
private callbacks: Record<string, Array<() => void>> = {}
public getNodes(): HTMLElement[] {
if (!this.alreadyGenerated) {
this.nodes = this.generateNodes()
this.alreadyGenerated = true
}
return this.nodes
}
protected abstract generateNodes(): HTMLElement[]
public run(): void {
return
}
public prune(): void {
this.callbacks = {};
return
}
protected getNodeFromTemplateId(id: string): HTMLElement {
let template = document.getElementById(id)
if (template === null) {
Conquer.fail(`Unable to find template id ${id}.`)
}
const finalNode = template.cloneNode(true)
if (!(finalNode instanceof HTMLElement)) {
Conquer.fail('The node is not an Element.')
}
finalNode.classList.remove('conquer-display-none')
return finalNode
}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
protected runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
}

View File

@ -0,0 +1,29 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default abstract class AbstractTopBarInterface extends ConquerInterface {
constructor() {
super()
const exitButton = this.getExitButton()
exitButton.addEventListener('click', () => {
this.runCallbacks('close')
})
}
protected generateNodes(): HTMLElement[] {
const newNode = this.getNodeFromTemplateId('conquer-interface-with-top-bar-template')
return [newNode]
}
protected getMainNode(): HTMLElement {
return this.getNodes()[0]
}
protected getExitButton(): HTMLElement {
const maybeExitButton = this.getMainNode().querySelector('.conquer-exit-button')
if (maybeExitButton === null || !(maybeExitButton instanceof HTMLElement)) {
Conquer.fail('No exit button.')
}
return maybeExitButton
}
public generateInterfaceElementCentered(): HTMLElement {
return this.getNodeFromTemplateId('conquer-interface-element-padded-template')
}
}

View File

@ -0,0 +1,209 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerLogin from '@burguillosinfo/conquer/login'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default class LoginUI extends ConquerInterface {
private conquerLogin: ConquerLogin
private conquerLoginGoToRegister: HTMLAnchorElement
private conquerLoginError: HTMLParagraphElement
private conquerLoginSuccess: HTMLParagraphElement
private conquerLoginUsername: HTMLInputElement
private conquerLoginPassword: HTMLInputElement
private conquerLoginSubmit: HTMLButtonElement
private conquerRegisterGoToLogin: HTMLAnchorElement
private conquerRegisterUsername: HTMLInputElement
private conquerRegisterPassword: HTMLInputElement
private conquerRegisterRepeatPassword: HTMLInputElement
private conquerRegisterSubmit: HTMLButtonElement
private conquerRegisterError: HTMLParagraphElement
constructor(conquerLogin: ConquerLogin) {
super()
this.conquerLogin = conquerLogin
}
public run() {
this.conquerLogin.on('login', () => {
this.runCallbacks('close');
});
this.storeRegisterElements()
this.storeLoginElements()
}
private getLoginDiv(): HTMLDivElement {
const element = this.getNodes()[1];
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Login is not a div.')
}
return element
}
private getOverlayDiv(): HTMLDivElement {
const element = this.getNodes()[0]
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Overlay transparent is not a div.')
}
return element
}
private getRegisterDiv(): HTMLDivElement {
const element = this.getNodes()[2]
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Register is not a div.')
}
return element
}
public generateNodes(): HTMLElement[] {
const resultArray = []
const overlay = this.getNodeFromTemplateId('conquer-overlay-transparent-template')
overlay.classList.remove('conquer-display-none')
resultArray.push(overlay)
const login = this.getNodeFromTemplateId('conquer-login-template')
login.classList.remove('conquer-display-none')
if (!(login instanceof HTMLDivElement)) {
Conquer.fail('Login is required to be a Div.')
}
resultArray.push(login)
const register = this.getNodeFromTemplateId('conquer-register-template')
resultArray.push(register)
return resultArray
}
private async storeRegisterElements() {
const registerElement = this.getRegisterDiv()
const conquerRegisterGoToLogin = registerElement.querySelector('.conquer-register-go-to-login')
if (conquerRegisterGoToLogin === null || !(conquerRegisterGoToLogin instanceof HTMLAnchorElement)) {
Conquer.fail('Link to go to login from register is invalid.')
}
this.conquerRegisterGoToLogin = conquerRegisterGoToLogin
this.conquerRegisterGoToLogin.addEventListener('click', () => {
this.goToLogin()
})
const conquerRegisterUsername = registerElement.querySelector('.conquer-register-username')
if (conquerRegisterUsername === null || !(conquerRegisterUsername instanceof HTMLInputElement)) {
Conquer.fail('No username field in conquer register.')
}
this.conquerRegisterUsername = conquerRegisterUsername
const conquerRegisterPassword = registerElement.querySelector('.conquer-register-password')
if (conquerRegisterPassword === null || !(conquerRegisterPassword instanceof HTMLInputElement)) {
Conquer.fail('No password field in conquer register.')
}
this.conquerRegisterPassword = conquerRegisterPassword
const conquerRegisterRepeatPassword = registerElement.querySelector('.conquer-register-repeat-password')
if (conquerRegisterRepeatPassword === null || !(conquerRegisterRepeatPassword instanceof HTMLInputElement)) {
Conquer.fail('No repeat password field in conquer register.')
}
this.conquerRegisterRepeatPassword = conquerRegisterRepeatPassword
const conquerRegisterSubmit = registerElement.querySelector('.conquer-register-submit')
if (conquerRegisterSubmit === null || !(conquerRegisterSubmit instanceof HTMLButtonElement)) {
Conquer.fail('No register submit button found.')
}
this.conquerRegisterSubmit = conquerRegisterSubmit
this.conquerRegisterSubmit.addEventListener('click', (event: Event) => {
event.preventDefault()
const username = this.conquerRegisterUsername.value
const password = this.conquerRegisterPassword.value
const repeatPassword = this.conquerRegisterRepeatPassword.value
this.conquerLogin.onRegisterRequest(this, username, password, repeatPassword)
})
const conquerRegisterError = registerElement.querySelector('.conquer-register-error')
if (conquerRegisterError === null || !(conquerRegisterError instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find the conquer error element.')
}
this.conquerRegisterError = conquerRegisterError
}
private storeLoginElements() {
const loginElement = this.getLoginDiv()
const conquerLoginGoToRegister = loginElement.querySelector('.conquer-login-go-to-register')
if (conquerLoginGoToRegister === null || !(conquerLoginGoToRegister instanceof HTMLAnchorElement)) {
Conquer.fail('Link to go to register from login is invalid.')
}
this.conquerLoginGoToRegister = conquerLoginGoToRegister
this.conquerLoginGoToRegister.addEventListener('click', () => {
this.goToRegister()
})
const conquerLoginError = loginElement.querySelector('.conquer-login-error')
if (conquerLoginError === null || !(conquerLoginError instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find conquer login error.')
}
this.conquerLoginError = conquerLoginError
const conquerLoginSuccess = loginElement.querySelector('.conquer-login-success')
if (conquerLoginSuccess === null || !(conquerLoginSuccess instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find conquer login success.')
}
this.conquerLoginSuccess = conquerLoginSuccess
const conquerLoginUsername = loginElement.querySelector('.conquer-login-username')
if (conquerLoginUsername === null || !(conquerLoginUsername instanceof HTMLInputElement)) {
Conquer.fail('Unable to find conquer login username field.')
}
this.conquerLoginUsername = conquerLoginUsername
const conquerLoginPassword = loginElement.querySelector('.conquer-login-password')
if (conquerLoginPassword === null || !(conquerLoginPassword instanceof HTMLInputElement)) {
Conquer.fail('Unable to find conquer login password field.')
}
this.conquerLoginPassword = conquerLoginPassword
const conquerLoginSubmit = loginElement.querySelector('.conquer-login-submit')
if (conquerLoginSubmit === null || !(conquerLoginSubmit instanceof HTMLButtonElement)) {
Conquer.fail('Unable to find the submit button for the login.')
}
this.conquerLoginSubmit = conquerLoginSubmit
this.conquerLoginSubmit.addEventListener('click', (event: Event) => {
event.preventDefault()
const username = this.conquerLoginUsername.value
const password = this.conquerLoginPassword.value
this.conquerLogin.onLoginRequested(this, username, password)
})
}
private async goToRegister(): Promise<void> {
await this.removeLoginRegisterCombo()
const registerElement = this.getRegisterDiv()
registerElement.classList.remove('conquer-display-none')
}
public async removeLoginRegisterCombo(): Promise<void> {
const registerElement = this.getRegisterDiv()
const overlayElement = this.getOverlayDiv()
overlayElement.classList.add('conquer-display-none')
const loginElement = this.getLoginDiv()
loginElement.classList.add('conquer-display-none')
registerElement.classList.add('conquer-display-none')
}
public async addNewLoginSuccessText(message: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.innerText = message
this.conquerLoginSuccess.classList.remove('conquer-display-none')
}
public async addNewLoginError(error: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.classList.add('conquer-display-none')
this.conquerLoginError.innerText = error
this.conquerLoginError.classList.remove('conquer-display-none')
}
public async addNewRegisterError(error: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.classList.add('conquer-display-none')
this.conquerRegisterError.innerText = error
this.conquerRegisterError.classList.remove('conquer-display-none')
}
public async unsetLoginAndRegisterErrors() {
this.conquerRegisterError.classList.add('conquer-display-none')
this.conquerLoginError.classList.add('conquer-display-none')
}
public async goToLogin(): Promise<void> {
await this.removeLoginRegisterCombo()
const loginElement = this.getLoginDiv()
loginElement.classList.remove('conquer-display-none')
}
public async addNewLoginRegisterError(message: string): Promise<void> {
this.addNewRegisterError(message)
this.addNewLoginError(message)
}
}

View File

@ -0,0 +1,104 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
export default class NewNodeUI extends AbstractTopBarInterface {
private coordinates: number[];
public getSubmitButton(): HTMLElement {
const submitButton = this.getMainNode().querySelector('button.new-node-form-submit')
if (submitButton === null || !(submitButton instanceof HTMLElement)) {
Conquer.fail('SubmitButton is null');
}
return submitButton;
}
public getErrorElement(): HTMLElement {
const errorElement = this.getMainNode().querySelector('p.conquer-error');
if (errorElement === null || !(errorElement instanceof HTMLElement)) {
Conquer.fail('No error element set');
}
return errorElement;
}
public getSelectNodeType(): HTMLSelectElement {
const selectElement = this.getMainNode().querySelector('select.conquer-node-type');
if (selectElement === null || !(selectElement instanceof HTMLSelectElement)) {
Conquer.fail('SelectElementNodeType is null');
}
return selectElement
}
public getInputNodeName(): HTMLInputElement {
const nodeName = this.getMainNode().querySelector('input.conquer-node-name')
if (nodeName === null || !(nodeName instanceof HTMLInputElement)) {
Conquer.fail('NodeName is null');
}
return nodeName
}
public getTextAreaNodeDescription(): HTMLTextAreaElement {
const nodeDescription = this.getMainNode().querySelector('textarea.conquer-node-description')
if (nodeDescription === null || !(nodeDescription instanceof HTMLTextAreaElement)) {
Conquer.fail('NodeDescription is null');
}
return nodeDescription
}
constructor(coordinates: number[]) {
super()
this.coordinates = coordinates
}
public run() {
const mainNode = this.getMainNode()
const form = this.getNodeFromTemplateId('conquer-new-node-form-creation-template')
mainNode.append(form)
this.getSubmitButton().addEventListener('click', (event) => {
event.preventDefault();
this.onSubmit();
});
form.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private setError(error: string): void {
const errorElement = this.getErrorElement();
errorElement.classList.remove('conquer-display-none')
errorElement.innerText = error
}
private onSubmit(): void {
const selectNodeType = this.getSelectNodeType();
const inputNodeName = this.getInputNodeName();
const textAreaNodeDescription = this.getTextAreaNodeDescription();
const description = textAreaNodeDescription.value;
const nodeName = inputNodeName.value;
const selectedOptionsNodeType = selectNodeType.selectedOptions;
if (selectedOptionsNodeType.length < 1) {
this.setError('Debes selecionar un tipo de nodo.');
return;
}
const selectedOptionNodeType = selectedOptionsNodeType[0];
const nodeType = selectedOptionNodeType.value;
if (nodeName.length < 5) {
this.setError('Todos los nodos deben tener un nombre mayor a 4 caracteres.');
return;
}
const urlNode = new URL('/conquer/node', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlNode, {
method: 'PUT',
body: JSON.stringify({
description: description,
name: nodeName,
type: nodeType,
coordinates: this.coordinates,
}),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch (error) {
this.setError( 'Respuesta erronea del servidor.');
return;
}
if (res.status !== 200) {
this.setError(responseBody.error);
return;
}
this.runCallbacks('close')
});
}
}

View File

@ -0,0 +1,97 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
export default class NewTeamUI extends AbstractTopBarInterface {
public getSubmitButton(): HTMLElement {
const submitButton = this.getMainNode().querySelector('button.new-team-form-submit')
if (submitButton === null || !(submitButton instanceof HTMLElement)) {
Conquer.fail('SubmitButton is null');
}
return submitButton;
}
public getErrorElement(): HTMLElement {
const errorElement = this.getMainNode().querySelector('p.conquer-error');
if (errorElement === null || !(errorElement instanceof HTMLElement)) {
Conquer.fail('No error element set');
}
return errorElement;
}
public getInputTeamName(): HTMLInputElement {
const teamName = this.getMainNode().querySelector('input.conquer-team-name');
if (teamName === null || !(teamName instanceof HTMLInputElement)) {
Conquer.fail('TeamName is null');
}
return teamName;
}
public getInputTeamColor(): HTMLInputElement {
const teamColor = this.getMainNode().querySelector('input.conquer-team-color');
if (teamColor === null || !(teamColor instanceof HTMLInputElement)) {
Conquer.fail('TeamColor is null');
}
return teamColor;
}
public getTextareaTeamDescription(): HTMLTextAreaElement {
const teamDescription = this.getMainNode().querySelector('textarea.conquer-team-description')
if (teamDescription === null || !(teamDescription instanceof HTMLTextAreaElement)) {
Conquer.fail('TeamDescription is null');
}
return teamDescription
}
constructor() {
super()
}
public run() {
const mainNode = this.getMainNode()
const form = this.getNodeFromTemplateId('conquer-new-team-form-creation-template')
mainNode.append(form)
this.getSubmitButton().addEventListener('click', (event) => {
event.preventDefault();
this.onSubmit();
});
form.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private setError(error: string): void {
const errorElement = this.getErrorElement();
errorElement.classList.remove('conquer-display-none')
errorElement.innerText = error
}
private onSubmit(): void {
const inputTeamName = this.getInputTeamName();
const textareaTeamDescription = this.getTextareaTeamDescription();
const inputTeamColor = this.getInputTeamColor();
const name = inputTeamName.value;
const description = textareaTeamDescription.value;
const color = inputTeamColor.value;
if (name.length < 5) {
this.setError('Todos los equipos deben tener un nombre mayor a 4 caracteres.');
return;
}
const urlTeam = new URL('/conquer/team', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlTeam, {
method: 'PUT',
body: JSON.stringify({
description : description,
name : name,
color : color,
}),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch (error) {
this.setError( 'Respuesta erronea del servidor.');
return;
}
if (res.status !== 200) {
this.setError(responseBody.error);
return;
}
this.runCallbacks('close')
});
}
}

View File

@ -0,0 +1,157 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
import MapNode from '@burguillosinfo/conquer/map-node'
import ConquerUser from '@burguillosinfo/conquer/user'
export default class NodeView extends AbstractTopBarInterface {
private node: MapNode;
private user: ConquerUser;
private view: HTMLElement | null = null;
public getNode(): MapNode {
return this.node;
}
private getNodeNameH2(): HTMLElement {
const element = this.getMainNode().querySelector('h2.node-name');
if (!(element instanceof HTMLElement)) {
Conquer.fail('h2.node-name is not a H2 or does not exist.');
}
return element;
}
private getView(): HTMLElement {
if (this.view === null) {
const view = this.getNodeFromTemplateId('conquer-view-node-template')
this.view = view;
}
return this.view;
}
private getNodeDescriptionParagraph(): HTMLElement {
const element = this.getMainNode().querySelector('p.node-description');
if (!(element instanceof HTMLElement)) {
Conquer.fail('p.node-description is not a P or does not exist.');
}
return element;
}
constructor(node: MapNode) {
super()
this.node = node;
}
public async run() {
const user = await ConquerUser.getSelfUser();
if (user === null) {
this.runCallbacks('close');
return;
}
this.user = user;
const mainNode = this.getMainNode()
this.runCallbacks('update-nodes');
try {
this.node = await this.node.fetch();
} catch (error) {
this.runCallbacks('close');
}
mainNode.append(this.getView())
this.getNodeNameH2().innerText = this.node.getName();
this.getNodeDescriptionParagraph().innerText = this.node.getDescription()
+ "\n"
+ (this.node.isNear()
? 'Estas cerca y puedes interactuar con este sitio.'
: 'Estás demasiado lejos para hacer nada aquí.');
this.populateTeamData();
if (this.node.isNear()) {
await this.runIfNear();
}
this.getView().classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private async populateTeamData() {
const element = document.createElement('p');
const team = await this.node.getTeam();
(() => {
if (team === null) {
element.innerText = 'El nodo no pertenece a ningún equipo todavía.';
return;
}
const spanText = document.createElement('span');
spanText.innerText = 'Equipo: ';
element.append(spanText);
const spanCircle = document.createElement('span');
spanCircle.classList.add('conquer-team-circle');
spanCircle.style.backgroundColor = team.getColor();
element.append(spanCircle);
const spanTeamName = document.createElement('span');
spanTeamName.style.color = team.getColor();
spanTeamName.innerText = ' ' + team.getName();
element.append(spanTeamName);
})();
this.getView().append(element);
}
private async runIfNear(): Promise<void> {
const userTeam = await this.user.getTeam();
const nodeTeam = await this.node.getTeam();
if (userTeam === null) {
const paragraphNoTeam = document.createElement('p');
paragraphNoTeam.innerText = 'Parece que no has seleccionado equipo aun,'
+ ' pulsa el botón de seleccionar equipo para comenzar tu aventura,'
+ ' si quieres cambiar de equipo en el futuro puedes hacerlo sin problemas.';
this.getView().append(paragraphNoTeam);
}
const selectTeamButton = document.createElement('button');
selectTeamButton.innerText = 'Seleccionar equipo';
selectTeamButton.addEventListener('click', () => {
this.runCallbacks('open-select-team');
this.runCallbacks('close');
});
this.getView().append(selectTeamButton);
if (await this.isOpposingNode()) {
const conquerForTeamButton = document.createElement('button');
conquerForTeamButton.innerText = 'Conquistar';
conquerForTeamButton.addEventListener('click', () => {
this.conquerThisNodeForTeam();
});
this.getView().append(conquerForTeamButton);
}
}
private async conquerThisNodeForTeam() {
const urlNode = new URL('/conquer/node/' + this.node.getUUID() + '/try-conquer',
window.location.protocol + '//'
+ window.location.hostname + ':'
+ window.location.port)
const response = await fetch(urlNode, {
method: 'POST',
});
this.runCallbacks('update-nodes');
this.runCallbacks('close');
}
private async isOpposingNode(): Promise<boolean> {
const userTeam = await this.user.getTeam();
const nodeTeam = await this.node.getTeam();
if (userTeam === null) {
return false;
}
if (nodeTeam === null) {
return true;
}
if (nodeTeam.getUUID() !== userTeam.getUUID()) {
return true;
}
return false;
}
private async isNodeFree(): Promise<boolean> {
return await this.node.getTeam() === null;
}
}

View File

@ -0,0 +1,72 @@
import Conquer from '@burguillosinfo/conquer';
import ConquerUser from '@burguillosinfo/conquer/user';
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface';
import ConquerUserCurrentEnemy from '@burguillosinfo/conquer/user-current-enemy'
export default class SelectFightUI extends AbstractTopBarInterface {
private enemies: ConquerUserCurrentEnemy[];
private form: HTMLElement | null = null;
constructor(enemies: ConquerUserCurrentEnemy[]) {
super();
this.enemies = enemies;
}
public async run(): Promise<void> {
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.getMainNode().append(this.getForm());
this.populateEnemies();
this.getMainNode().classList.remove('conquer-display-none');
}
private populateEnemies(): void {
for (const enemy of this.enemies) {
this.appendEnemy(enemy);
}
}
private appendEnemy(enemy: ConquerUserCurrentEnemy) {
const form = this.getForm();
const enemyNode = this.getNodeFromTemplateId('conquer-select-fight-item-template');
this.getNameEnemyNodeElement(enemyNode).innerText = enemy.getSpecies().getName();
this.getLevelEnemyNodeElement(enemyNode).innerText = '' + enemy.getLevel();
this.getImageEnemyNodeElement(enemyNode).src = enemy.getSpecies().getImage();
form.append(enemyNode);
}
private getImageEnemyNodeElement(enemyNode: HTMLElement): HTMLImageElement {
const conquerImage = enemyNode.querySelector('.conquer-image');
if (!(conquerImage instanceof HTMLImageElement)) {
Conquer.fail('conquerImage is not HTMLImageElement.')
}
return conquerImage;
}
private getLevelEnemyNodeElement(enemyNode: HTMLElement): HTMLElement {
const conquerLevel = enemyNode.querySelector('.conquer-level');
if (!(conquerLevel instanceof HTMLElement)) {
Conquer.fail('conquerLevel is not HTMLElement.')
}
return conquerLevel;
}
private getNameEnemyNodeElement(enemyNode: HTMLElement): HTMLElement {
const conquerName = enemyNode.querySelector('.conquer-name');
if (!(conquerName instanceof HTMLElement)) {
Conquer.fail('conquerName is not HTMLElement.')
}
return conquerName;
}
private getForm(): HTMLElement {
if (this.form === null) {
const form = this.getNodeFromTemplateId('conquer-select-fight-list-template')
this.form = form;
}
return this.form;
}
}

View File

@ -0,0 +1,89 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerUser from '@burguillosinfo/conquer/user'
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import MapNode from '@burguillosinfo/conquer/map-node'
import ConquerTeam from '@burguillosinfo/conquer/team';
export default class SelectTeamUI extends AbstractTopBarInterface {
private node: MapNode;
private user: ConquerUser;
private form: HTMLElement | null = null;
constructor(node: MapNode) {
super();
this.node = node;
}
public async run(): Promise<void> {
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.user = user
await this.populateTeams();
this.getForm().classList.remove('conquer-display-none');
this.getMainNode().append(this.getForm());
this.getMainNode().classList.remove('conquer-display-none');
}
private async populateTeams() {
const teams = await ConquerTeam.getTeams();
for (const team of teams) {
this.populateTeam(team);
}
}
private populateTeam(team: ConquerTeam) {
const teamDiv = this.getNodeFromTemplateId('conquer-team-to-select-template')
const nameParagraph = teamDiv.querySelector('p.conquer-name');
const descriptionParagraph = teamDiv.querySelector('p.conquer-description');
const submit = teamDiv.querySelector('button.conquer-submit');
if (!(nameParagraph instanceof HTMLParagraphElement)
|| !(descriptionParagraph instanceof HTMLParagraphElement)
|| !(submit instanceof HTMLButtonElement)) {
Conquer.fail('Select team name inclusive or description container are not correctly defined in template.');
}
nameParagraph.innerText = team.getName();
descriptionParagraph.innerText = team.getDescription();
nameParagraph.style.color = team.getColor();
submit.addEventListener('click', async () => {
this.onSelectTeam(team);
});
this.getForm().append(teamDiv);
}
private async onSelectTeam(team: ConquerTeam) {
const urlTeam = new URL('/conquer/user/team',
window.location.protocol + '//'
+ window.location.hostname + ':'
+ window.location.port);
const response = await fetch(urlTeam, {
method: 'POST',
body: JSON.stringify({
team: team.getUUID(),
node: this.node.getUUID(),
}),
});
let responseBody;
try {
responseBody = await response.json();
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
this.runCallbacks('update-nodes');
this.runCallbacks('close')
} catch (error) {
console.error('Error parsing json', error);
}
}
private getForm(): HTMLElement {
if (this.form === null) {
const form = this.getNodeFromTemplateId('conquer-select-team-list-template')
this.form = form;
}
return this.form;
}
}

View File

@ -0,0 +1,147 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerUser from '@burguillosinfo/conquer/user'
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
export default class SelfPlayerUI extends AbstractTopBarInterface {
private selfPlayer: ConquerUser | null = null
private userWelcome: HTMLElement | null = null
private isExplorerModeEnabled: boolean;
private userTeamData: HTMLElement | null = null;
constructor(isExplorerModeEnabled: boolean) {
super();
this.isExplorerModeEnabled = isExplorerModeEnabled;
}
public async run(): Promise<void> {
const selfPlayerNode = this.getMainNode()
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.selfPlayer = user
this.populateWelcome()
this.populateCreateNodeOption()
this.populateToggleExplorerModeOption();
this.populateCreateTeamButton();
await this.populateUserTeamData();
selfPlayerNode.classList.remove('conquer-display-none')
}
private populateToggleExplorerModeOption(): void {
const toggleExplorerModeButton = document.createElement('button');
this.setTextToggleExplorerModeButton(toggleExplorerModeButton);
toggleExplorerModeButton.addEventListener('click', () => {
(() => {
if (this.isExplorerModeEnabled) {
this.runCallbacks('disable-explorer-mode');
return;
}
this.runCallbacks('enable-explorer-mode');
})();
this.runCallbacks('close');
});
const toggleExplorerModeInterface = this.generateInterfaceElementCentered()
toggleExplorerModeInterface.appendChild(toggleExplorerModeButton)
this.getMainNode().appendChild(toggleExplorerModeInterface)
}
private populateCreateTeamButton(): void {
// Only admins can create teams.
if (!this.selfPlayer?.isAdmin()) {
return;
}
const createTeamButton = document.createElement('button');
createTeamButton.innerText = 'Crea un nuevo equipo';
createTeamButton.addEventListener('click', () => {
this.runCallbacks('open-create-team');
this.runCallbacks('close');
});
const createTeamButtonInterface = this.generateInterfaceElementCentered()
createTeamButtonInterface.append(createTeamButton);
this.getMainNode().appendChild(createTeamButtonInterface);
}
private setTextToggleExplorerModeButton(button: HTMLElement): void {
if (this.isExplorerModeEnabled) {
button.innerText = 'Desactivar movimiento libre en el mapa.';
return;
}
button.innerText = 'Activar movimiento libre en el mapa.';
}
private populateCreateNodeOption() {
// Only admins can create nodes.
if (!this.selfPlayer?.isAdmin()) {
return
}
const createNodeButton = document.createElement('button')
createNodeButton.innerText = 'Crear Nuevo Nodo'
createNodeButton.addEventListener('click', () => {
this.runCallbacks('createNodeStart')
// We close because it is a sensible thing to do.
this.runCallbacks('close')
})
const createNodeButtonInterface = this.generateInterfaceElementCentered()
createNodeButtonInterface.appendChild(createNodeButton)
this.getMainNode().appendChild(createNodeButtonInterface)
}
private async getUserTeamData(): Promise<HTMLElement> {
if (this.userTeamData !== null) {
return this.userTeamData;
}
const element = document.createElement('p');
this.userTeamData = element;
if (this.selfPlayer === null) {
throw new Error('User still not set')
}
const team = await this.selfPlayer.getTeam();
if (team === null) {
element.innerText = 'No tienes equipo aun,'
+ ' ve al nodo más cercano para unirte a un equipo.';
return this.userTeamData;
}
const spanText = document.createElement('span');
spanText.innerText = 'Equipo: ';
element.append(spanText);
const spanCircle = document.createElement('span');
spanCircle.classList.add('conquer-team-circle');
spanCircle.style.backgroundColor = team.getColor();
element.append(spanCircle);
const spanTeamName = document.createElement('span');
spanTeamName.style.color = team.getColor();
spanTeamName.innerText = ' ' + team.getName();
element.append(spanTeamName);
return this.userTeamData;
}
private async populateUserTeamData(): Promise<void> {
const userTeamData = await this.getUserTeamData();
const userTeamDataInterface = this.generateInterfaceElementCentered();
userTeamDataInterface.append(userTeamData);
this.getMainNode().append(userTeamDataInterface);
}
private populateWelcome(): void {
const userWelcome = this.getUserWelcome()
const userWelcomeInterface = this.generateInterfaceElementCentered();
userWelcomeInterface.appendChild(userWelcome)
this.getMainNode().appendChild(userWelcomeInterface)
}
private getUserWelcome(): HTMLElement {
if (this.userWelcome !== null) {
return this.userWelcome
}
const element = document.createElement('h2')
if (this.selfPlayer === null) {
throw new Error('User still not set')
}
element.innerText = `¡Hola, ${this.selfPlayer.getUsername()}!`
this.userWelcome = element
return this.userWelcome
}
}

136
js-src/conquer/login.ts Normal file
View File

@ -0,0 +1,136 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterfaceManager from '@burguillosinfo/conquer/interface-manager'
import LoginUI from '@burguillosinfo/conquer/interface/login'
export type ConquerLoginEventCallback = () => void
export default class Login {
private conquerLogin: HTMLDivElement
private conquerInterfaceManager: ConquerInterfaceManager
private cachedIsLoggedIn: boolean | null = null
constructor(conquerInterfaceManager: ConquerInterfaceManager) {
this.conquerInterfaceManager = conquerInterfaceManager
}
public async start(): Promise<void> {
this.loopCheckLogin()
}
public async onRegisterRequest(loginUI: LoginUI, username: string, password: string, repeatPassword: string): Promise<void> {
const urlUser = new URL('/conquer/user', window.location.protocol +
'//' + window.location.hostname + ':' + window.location.port)
let responseJson
let status
try {
const response = await fetch(urlUser, {
method: 'PUT',
body: JSON.stringify({
username: username,
password: password,
repeat_password: repeatPassword
})
})
responseJson = await response.json()
status = response.status
} catch(e) {
console.error(e)
loginUI.addNewRegisterError('El servidor ha enviado datos inesperados.')
return
}
if (status !== 200) {
loginUI.addNewRegisterError(responseJson.error)
return
}
loginUI.addNewLoginSuccessText(`Usuario registrado ${username}.`)
loginUI.goToLogin()
}
private async loopCheckLogin(): Promise<void> {
window.setInterval(() => {
this.isLogged().then((isLogged) => {
if (isLogged) {
if (this.cachedIsLoggedIn !== true) {
this.cachedIsLoggedIn = true;
this.onLoginSuccess();
}
return;
}
if (this.cachedIsLoggedIn !== false) {
this.cachedIsLoggedIn = false;
this.onLogout()
}
})
}, 5000)
}
private async onLogout(): Promise<void> {
const interfaceManager = this.conquerInterfaceManager
const loginUI = new LoginUI(this)
for (const callback of this.callbacks.logout) {
callback();
}
loginUI.on('close', () => {
interfaceManager.remove(loginUI);
})
interfaceManager.push(loginUI)
}
private callbacks: Record<string, Array<ConquerLoginEventCallback>> = {}
public async on(name: string, callback: ConquerLoginEventCallback) {
if (this.callbacks[name] === undefined) {
this.callbacks[name] = []
}
this.callbacks[name].push(callback)
}
private async onLoginSuccess(): Promise<void> {
this.cachedIsLoggedIn = true
for (const callback of this.callbacks.login) {
callback()
}
}
public async onLoginRequested(loginUI: LoginUI, username: string, password: string): Promise<void> {
const urlUser = new URL('/conquer/user/login', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let responseJson
let status
try {
const response = await fetch(urlUser, {
method: 'POST',
body: JSON.stringify({
username: username,
password: password,
})
})
responseJson = await response.json()
status = response.status
} catch(e) {
console.error(e)
loginUI.addNewLoginError('El servidor ha enviado datos inesperados.')
return
}
if (status !== 200) {
loginUI.addNewLoginError(responseJson.error)
return
}
loginUI.unsetLoginAndRegisterErrors()
const isLogged = await this.isLogged()
if (isLogged) {
this.onLoginSuccess()
}
}
public async isLogged(): Promise<boolean> {
const urlUser = new URL('/conquer/user', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let status
try {
const response = await fetch(urlUser)
status = response.status
} catch {
return false
}
return status === 200
}
}

163
js-src/conquer/map-node.ts Normal file
View File

@ -0,0 +1,163 @@
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Style from 'ol/style/Style'
import Feature from 'ol/Feature'
import CircleStyle from 'ol/style/Circle'
import Point from 'ol/geom/Point'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
import NodeView from '@burguillosinfo/conquer/interface/node-view'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import SelectTeamUI from '@burguillosinfo/conquer/interface/select-team';
import ConquerTeam from '@burguillosinfo/conquer/team';
@JsonObject()
export default class MapNode {
private feature: Feature | null = null;
private callbacks: Record<string, Array<() => void>> = {}
private cachedTeam: ConquerTeam | null = null;
constructor(
@JsonProperty() private uuid: string,
@JsonProperty() private coordinate_1: number,
@JsonProperty() private coordinate_2: number,
@JsonProperty() private type: string,
@JsonProperty() private name: string,
@JsonProperty() private description: string,
@JsonProperty() private kind: string,
@JsonProperty() private is_near: boolean,
@JsonProperty() private team: string,
) {
}
public async getTeam(): Promise<ConquerTeam | null> {
if (this.cachedTeam === null) {
if (this.team === null) {
return null;
}
this.cachedTeam = await ConquerTeam.getTeam(this.team);
}
return this.cachedTeam;
}
public async fetch(): Promise<MapNode> {
const urlNode = new URL('/conquer/node/' + this.uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
const response = await fetch(urlNode);
let responseBody;
const errorThrow = new Error('Unable to fetch node updated.');
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
throw errorThrow;
}
if (response.status !== 200) {
console.error(responseBody.error);
throw errorThrow;
}
const node = JsonSerializer.deserialize(responseBody, MapNode);
if (!(node instanceof MapNode)) {
console.error('Unexpected JSON value for MapNode.');
throw errorThrow;
}
return node;
}
public click(interfaceManager: InterfaceManager): void {
const viewNodeInterface = new NodeView(this);
viewNodeInterface.on('close', () => {
interfaceManager.remove(viewNodeInterface);
});
viewNodeInterface.on('update-nodes', () => {
this.runCallbacks('update-nodes');
});
viewNodeInterface.on('open-select-team', () => {
this.openSelectTeam(interfaceManager);
});
interfaceManager.push(viewNodeInterface);
this.runCallbacks('click');
}
public openSelectTeam(interfaceManager: InterfaceManager): void {
const selectTeamUI = new SelectTeamUI(this);
selectTeamUI.on('update-nodes', () => {
this.runCallbacks('update-nodes');
});
selectTeamUI.on('close', () => {
interfaceManager.remove(selectTeamUI);
});
interfaceManager.push(selectTeamUI);
}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
protected runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
public getType(): string {
return this.type;
}
public isNear(): boolean {
return this.is_near;
}
public getName(): string {
return this.name;
}
public getDescription(): string {
return this.description;
}
public getId(): string {
return 'node-' + this.uuid;
}
public getUUID(): string {
return this.uuid;
}
public getFeature(): Feature {
if (this.feature === null) {
this.feature = new Feature({
geometry: new Point([this.coordinate_1, this.coordinate_2]),
type: 'node-' + this.uuid,
})
}
return this.feature;
}
public async getStyle(): Promise<Style> {
const team = await this.getTeam();
let color = 'white';
if (team !== null) {
color = team.getColor();
}
return new Style({
image: new CircleStyle({
radius: 14,
fill: new Fill({color: color}),
stroke: new Stroke({
color: 'black',
width: 5,
})
})
});
}
}

View File

@ -0,0 +1,11 @@
enum MapState {
NOTHING = 0x0,
NORMAL = 0x1,
FREE_MOVE = 0x2,
FREE_ROTATION = 0x4,
CREATE_NODE = 0x8,
SELECT_WHERE_TO_CREATE_NODE = 0x10,
FILLING_FORM_CREATE_NODE = 0x20,
}
export default MapState

View File

@ -0,0 +1,6 @@
import { JsonSerializer, throwError } from 'typescript-json-serializer';
export default new JsonSerializer({
errorCallback: throwError,
additionalPropertiesPolicy: 'disallow',
});

25
js-src/conquer/specie.ts Normal file
View File

@ -0,0 +1,25 @@
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
@JsonObject()
export default class Specie {
@JsonProperty()
private id: string;
@JsonProperty()
private name: string;
@JsonProperty()
private image: string;
public getId(): string {
return this.id;
}
public getName(): string {
return this.name;
}
public getImage(): string {
return this.image;
}
}

117
js-src/conquer/team.ts Normal file
View File

@ -0,0 +1,117 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Conquer from '@burguillosinfo/conquer'
@JsonObject()
export default class ConquerTeam {
@JsonProperty()
private kind: string;
@JsonProperty()
private uuid: string;
@JsonProperty()
private name: string;
@JsonProperty()
private description: string;
@JsonProperty()
private points: number;
@JsonProperty()
private color: string;
public getUUID(): string {
return this.uuid;
}
public getName(): string {
return this.name;
}
public getDescription(): string {
return this.description;
}
public getColor(): string {
return this.color;
}
constructor(uuid: string, name: string, description: string, points: number, color: string) {
this.kind = 'ConquerTeam';
this.uuid = uuid;
this.name = name;
this.description = description;
this.points = points;
this.color = color;
}
public static async getTeams(): Promise<ConquerTeam[]> {
const urlTeam = new URL('/conquer/teams', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching teams.')
}
const teamData = await response.json()
const teams = JsonSerializer.deserialize(teamData, ConquerTeam);
if (teams === undefined || teams === null) {
Conquer.fail('Teams cannot be null, server error.');
}
if (!(teams instanceof Array)) {
throw new Error('Unable to parse team.');
}
const teamsSanitized: ConquerTeam[] = [];
for (const team of teams) {
if (!(team instanceof ConquerTeam)) {
console.error('Received null team from server, fix this error.');
continue;
}
teamsSanitized.push(team);
}
return teamsSanitized;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Teams.');
}
}
public static async getTeam(uuid: string): Promise<ConquerTeam> {
const urlTeam = new URL('/conquer/team/' + uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (!(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
public static async getSelfTeam(): Promise<ConquerTeam | null> {
const urlTeam = new URL('/conquer/user/team', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (team !== null && !(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
}

View File

@ -0,0 +1,66 @@
import Conquer from '@burguillosinfo/conquer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Specie from '@burguillosinfo/conquer/specie';
import JsonSerializer from '@burguillosinfo/conquer/serializer';
@JsonObject()
export default class ConquerUserCurrentEnemy {
@JsonProperty()
private uuid: string;
@JsonProperty()
private species: Specie;
@JsonProperty()
private level: number;
@JsonProperty()
private max_health: number;
public getUUID(): string {
return this.uuid;
}
public getSpecies(): Specie {
return this.species;
}
public getLevel(): number {
return this.level;
}
public getMaxHealth(): number {
return this.max_health;
}
public static async getGlobalEnemies(): Promise<ConquerUserCurrentEnemy[] | null> {
const urlEnemies = new URL('/conquer/user/enemies/global', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port);
const response = await fetch(urlEnemies);
let responseBody;
try {
responseBody = await response.json();
if (response.status !== 200) {
console.error(responseBody.error);
return null;
}
const enemiesRaw = JsonSerializer.deserialize(responseBody, ConquerUserCurrentEnemy);
const enemiesReturnArray: ConquerUserCurrentEnemy[] = [];
if (!(enemiesRaw instanceof Array)) {
console.error('Incorrect type retrieved from ' + urlEnemies);
return null;
}
for (const enemy of enemiesRaw) {
if (!(enemy instanceof ConquerUserCurrentEnemy)) {
console.error('Incorrect type for enemy, maybe null or undef.', enemy);
return null;
}
enemiesReturnArray.push(enemy);
}
return enemiesReturnArray;
} catch(error) {
console.error(error, 'Invalid response from server seeking for possible battles.');
return null;
}
}
}

78
js-src/conquer/user.ts Normal file
View File

@ -0,0 +1,78 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import ConquerTeam from '@burguillosinfo/conquer/team';
export interface UserData {
is_admin: number
kind: string
last_activity?: string
registration_date?: string
username: string
uuid: string
}
@JsonObject()
export default class ConquerUser {
@JsonProperty()
private is_admin: boolean;
@JsonProperty()
private kind: string;
@JsonProperty()
private last_activity: string | null;
@JsonProperty()
private registration_date: string | null;
@JsonProperty()
private username: string;
@JsonProperty()
private uuid: string;
@JsonProperty()
private team: string | null;
private cachedTeam: ConquerTeam | null = null;
constructor(kind: string, uuid: string, username: string, is_admin = false, registration_date: string | null = null, last_activity: string | null = null) {
this.kind = kind;
this.uuid = uuid;
this.username = username;
this.is_admin = is_admin;
this.registration_date = registration_date;
this.last_activity = last_activity;
}
public async getTeam(): Promise<ConquerTeam | null> {
if (this.cachedTeam === null) {
if (this.team === null) {
return null;
}
this.cachedTeam = await ConquerTeam.getTeam(this.team);
}
return this.cachedTeam;
}
public static async getSelfUser(): Promise<ConquerUser | null> {
const urlUser = new URL('/conquer/user', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlUser)
if (response.status !== 200) {
throw new Error('Invalid response fetching user.')
}
const userData = await response.json()
const user = JsonSerializer.deserialize(userData, ConquerUser);
if (!(user instanceof ConquerUser)) {
throw new Error('Unable to parse user.');
}
return user;
} catch (error) {
console.error(error)
return null
}
}
public getUsername(): string {
if (this.username === null) {
throw new Error('User username cannot be null.')
}
return this.username
}
public isAdmin(): boolean {
return this.is_admin
}
}

View File

@ -0,0 +1,30 @@
export default class ConquerWebSocket {
private webSocket: WebSocket | null = null
private socketReady = false
private getWebSocket(): WebSocket {
if (this.webSocket !== null && this.socketReady) {
return this.webSocket
}
this.webSocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/conquer/websocket`)
this.webSocket.addEventListener('close', (event) => {
this.onSocketClose(event)
})
this.webSocket.addEventListener('error', (event) => {
this.onSocketClose(event)
})
this.webSocket.addEventListener('open', (event) => {
this.onSocketOpen(event)
})
return this.webSocket
}
private onSocketOpen(event: Event) {
this.socketReady = true
}
private onSocketClose(event: Event) {
this.socketReady = false
console.error(event)
}
}

View File

@ -1,12 +1,23 @@
"use strict";
import Tablesort from 'tablesort';
import Conquer from '@burguillosinfo/conquer/index';
import CarouselAd from '@burguillosinfo/carousel-ad'
window.Tablesort = require('tablesort');
require('tablesort/src/sorts/tablesort.number');
let fakeSearchInput
let searchMobile
document.addEventListener("DOMContentLoaded", function () {
onDomContentLoaded();
}, false);
function onDomContentLoaded() {
const path = window.location.pathname
if (path.match(/^(?:\/)?conquer(?:$|\/)/)) {
Conquer.start();
return
}
const menu_expand = document.querySelector('a.menu-expand');
const mobile_foldable = document.querySelector('nav.mobile-foldable');
const transparentFullscreenHide = document.querySelector('div.transparent-fullscreen-hide');
@ -64,8 +75,7 @@ document.addEventListener("DOMContentLoaded", function () {
fakeSearchInput = searchMobile.querySelector('input')
addListenersSearch()
}
}, false);
}
function fillFarmaciaGuardia() {
const farmaciaName = document.querySelector('#farmacia-name');
const farmaciaAddress = document.querySelector('#farmacia-address');

View File

@ -7,6 +7,7 @@ use Mojo::Base 'Mojolicious', -signatures;
# This method will run once at server start
sub startup ($self) {
my $metrics = BurguillosInfo::Controller::Metrics->new;
$self->sessions->default_expiration(0);
$self->hook(
around_dispatch => sub {
my $next = shift;
@ -19,26 +20,66 @@ sub startup ($self) {
);
push @{ $self->commands->namespaces }, 'BurguillosInfo::Command';
$self->hook(
before_render => sub($c, $args) {
before_render => sub ( $c, $args ) {
my $current_route = $c->url_for;
$c->stash(current_route => $current_route);
my $is_android = $c->req->headers->user_agent =~ /android/i;
$c->stash(is_android => $is_android);
$c->stash( current_route => $current_route );
my $is_android = $c->req->headers->user_agent =~ /android/i;
$c->stash( is_android => $is_android );
my $onion_base_url = $self->config->{onion_base_url};
my $base_url = $self->config->{base_url};
if (!defined $onion_base_url) {
my $base_url = $self->config->{base_url};
if ( !defined $onion_base_url ) {
return;
}
$current_route =~ s/^$base_url//;
$c->res->headers->header('Onion-Location' => $onion_base_url.$current_route);
$c->res->headers->header(
'Onion-Location' => $onion_base_url . $current_route );
}
);
my $config = $self->plugin('JSONConfig');
$self->config(
hypnotoad => { proxy => 1, listen => [$self->config('listen') // 'http://localhost:3000'] } );
hypnotoad => {
proxy => 1,
listen => [ $self->config('listen') // 'http://localhost:3000' ]
}
);
$self->config( css_version => int( rand(10000) ) );
$self->secrets( $self->config->{secrets} );
$self->helper(
current_user => sub ($c) {
use BurguillosInfo::Schema;
$self->session(expiration => 0);
my $user_uuid = $c->session->{conquer}{user};
if ( !defined $user_uuid ) {
return;
}
my $user_resultset =
BurguillosInfo::Schema->Schema->resultset('ConquerUser');
my @user_candidates =
$user_resultset->search( { uuid => $user_uuid } );
my $user = $user_candidates[0];
# Just to make clear what happens if there is no user we return.
if ( !defined $user ) {
return;
}
return $user;
}
);
$self->helper(
set_current_user => sub ( $c, $user ) {
$self->session(expiration => 0);
if ( !defined $user
|| !$user->can('uuid')
|| !$user->can('get_from_storage') )
{
die "$user is not a valid user for it's usage in a session.";
}
$user = $user->get_from_storage;
$c->session->{conquer}{user} = $user->uuid;
}
);
# Router
my $r = $self->routes;
@ -48,12 +89,29 @@ sub startup ($self) {
$r->get('/sitemap.xml')->to('Sitemap#sitemap');
$r->get('/robots.txt')->to('Robots#robots');
# $r->get('/:post')->to('Page#post');
$r->get('/stats')->to('Metrics#stats');
$r->get('/conquer')->to('Conquer#index');
$r->put('/conquer/user')->to('UserConquer#create');
$r->get('/conquer/user/team')->to('UserConquer#getSelfTeam');
$r->post('/conquer/user/team')->to('UserConquer#setTeamForUser');
$r->post('/conquer/user/coordinates')->to('UserConquer#setCoordinates');
$r->get('/conquer/team/<uuid>')->to('ConquerTeam#get');
$r->put('/conquer/team')->to('ConquerTeam#put');
$r->get('/conquer/teams')->to('ConquerTeam#getAll');
$r->put('/conquer/node')->to('ConquerNode#create');
$r->get('/conquer/node/near')->to('ConquerNode#nearbyNodes');
$r->get('/conquer/user/enemies/global')->to('ConquerUserCurrentEnemy#listEnemiesGlobal');
$r->post('/conquer/user/enemies/fight')->to('ConquerUserCurrentEnemy#fightEnemy');
$r->get('/conquer/node/<uuid>')->to('ConquerNode#get');
$r->post('/conquer/node/<uuid>/try-conquer')->to('ConquerNode#tryConquer');
$r->get('/conquer/user')->to('UserConquer#get_self');
$r->post('/conquer/user/login')->to('UserConquer#login');
$r->get('/conquer/tile/<zoom>/<x>/<y>.png')->to('ConquerTile#tile');
$r->get('/search.json')->to('Search#search');
$r->get('/farmacia-guardia.json')->to('FarmaciaGuardia#current');
$r->get('/<:category>.rss')->to('Page#category_rss');
$r->get('/:category_slug/atributo/<:attribute_slug>-preview.png')->to('Attribute#get_attribute_preview');
$r->get('/:category_slug/atributo/<:attribute_slug>-preview.png')
->to('Attribute#get_attribute_preview');
$r->get('/:category_slug/atributo/:attribute_slug')->to('Attribute#get');
$r->get('/<:category>-preview.png')->to('Page#get_category_preview');
$r->get('/:category')->to('Page#category');

View File

@ -0,0 +1,15 @@
package BurguillosInfo::Controller::Conquer;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
sub index($self) {
$self->render;
}
1;

View File

@ -0,0 +1,185 @@
package BurguillosInfo::Controller::ConquerNode;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use BurguillosInfo::Schema;
sub get($self) {
my $uuid = $self->param('uuid');
my $user = $self->current_user;
if (!defined $uuid || !$uuid) {
return $self->render(status => 400, json => {
error => 'UUID de nodo invalido.',
});
}
my $schema = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my @nodes = $schema->search({uuid => $uuid});
if (!scalar @nodes) {
return $self->render(status => 404, json => {
error => 'Nodo no encontrado',
});
}
my $node = $nodes[0];
if (defined $user) {
return $self->render(json => $node->serialize($user));
}
return $self->render(json => $node->serialize());
}
sub tryConquer($self) {
my $user = $self->current_user;
my $schema = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'You must be logged to conquer a node.',
});
}
if (!defined $user->team) {
return $self->render(status => 400, json => {
error => 'You must belong to a team to conquer a node.',
});
}
my $uuid = $self->param('uuid');
my ($node) = $schema->search({uuid => $uuid});
if (!defined $node) {
return $self->render(status => 404, json => {
error => 'No existe ese nodo.',
});
}
$node->team($user->team);
$node->update;
$self->render(json => {
ok => $JSON::true,
});
}
sub create ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'No estás autenticado.',
}
);
}
if ( !$user->is_admin ) {
return $self->render(
status => 403,
json => {
error => 'No tienes permiso para hacer eso.',
}
);
}
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $name = $input->{name};
my $coordinates = $input->{coordinates};
my $type = $input->{type};
my $description = $input->{description};
if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
return $self->render(
status => 400,
json => {
error => 'Formato erroneo de coordenadas.',
}
);
}
my ($coordinate_1, $coordinate_2) = $coordinates->@*;
if ( !defined $name || length $name < 5 ) {
return $self->render(
status => 400,
json => {
error =>
'Número incorrecto de carácteres en el nombre del nodo.',
}
);
}
if ( !defined $description ) {
return $self->render(
status => 400,
json => {
error => 'La descripción puede estar vacía, '
. 'pero debe existir, si ves este error '
. 'desde la aplicación es un error de programación.',
}
);
}
if ( !defined $type ) {
return $self->render(
status => 400,
json => {
error => 'Los nodos deben tener un tipo.'
}
);
}
if ( $type ne 'normal' ) {
return $self->render(
status => 400,
json => {
error => 'Tipo de nodo no soportado.',
}
);
}
my $uuid_node = create_uuid_string();
my $node;
eval {
$node = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->new(
{
uuid => $uuid_node,
description => $description,
name => $name,
type => $type,
geometry => \['ST_MakePoint(?, ?)', $coordinate_1, $coordinate_2],
}
);
$node->insert;
$node = $node->get_from_storage;
};
if ($@) {
warn $@;
return $self->render(
status => 500,
json => {
error => 'El servidor no pudo almacenar el nodo, reporta este error.',
}
);
}
return $self->render(
status => 200,
json => $node->serialize,
);
}
sub nearbyNodes($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'No estás loggeado.',
});
}
my @nodes = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->search({});
@nodes = map { $_->serialize($user) } @nodes;
return $self->render(json => \@nodes);
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
1;

View File

@ -0,0 +1,180 @@
package BurguillosInfo::Controller::ConquerTeam;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use JSON;
use BurguillosInfo::Schema;
sub getAll ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch teams.',
}
);
}
my $uuid = $self->param('uuid');
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search({});
return $self->render( json => [ map { $_->serialize } @teams ] );
}
sub get ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch a team.',
}
);
}
my $uuid = $self->param('uuid');
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search(
{
'uuid' => $uuid,
}
);
if ( scalar @teams <= 0 ) {
return $self->render(
status => 404,
json => {
error => 'This team does not exist.',
}
);
}
my $team = $teams[0];
return $self->render( json => $team->serialize );
}
sub getSelfTeam ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch your Team.',
}
);
}
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search(
{
'players.uuid' => $user->uuid
},
{
join => 'players',
}
);
if ( scalar @teams <= 0 ) {
return $self->render( json => undef );
}
my $team = $teams[0];
return $self->render( json => $team );
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
sub put ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'No estás autenticado.',
}
);
}
if ( !$user->is_admin ) {
return $self->render(
status => 403,
json => {
error => 'No tienes permiso para hacer eso.',
}
);
}
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $name = $input->{name};
my $description = $input->{description};
my $color = $input->{color};
if ( !defined $name || length $name < 5 ) {
return $self->render(
status => 400,
json => {
error =>
'Número incorrecto de carácteres en el nombre del equipo..',
}
);
}
if ( !defined $description ) {
return $self->render(
status => 400,
json => {
error => 'La descripción puede estar vacía, '
. 'pero debe existir, si ves este error '
. 'desde la aplicación es un error de programación.',
}
);
}
my $color_regex_char = qr/[0-9a-fA-F]/;
if ( !defined $color || $color !~ /^#(?:${color_regex_char}{6}|${color_regex_char}{3})$/ ) {
return $self->render(
status => 400,
json => {
error => 'Formato de color invalido',
}
);
}
my $uuid_team = create_uuid_string();
my $team;
eval {
$team = BurguillosInfo::Schema->Schema->resultset('ConquerTeam')->new(
{
uuid => $uuid_team,
description => $description,
name => $name,
color => $color,
}
);
$team->insert;
};
if ($@) {
warn $@;
return $self->render(
status => 500,
json => {
error =>
'El servidor no pudo almacenar el equipo, reporta este error.',
}
);
}
return $self->render(
status => 200,
json => $team->serialize,
);
}
1;

View File

@ -0,0 +1,70 @@
package BurguillosInfo::Controller::ConquerTile;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use Path::Tiny;
use Mojo::UserAgent;
use DateTime::Format::HTTP;
use DateTime;
my $cache_files_dir =
path(__FILE__)->parent->parent->parent->parent->child('cache/tiles/');
sub _cache_response ($self) {
my $tomorrow_same_hour_datetime = DateTime->now->add( days => 1 );
$self->res->headers->cache_control("max_age=@{[3600*24]}");
$self->res->headers->expires(
DateTime::Format::HTTP->format_datetime($tomorrow_same_hour_datetime) );
}
sub tile ($self) {
my $zoom = $self->stash('zoom');
my $x = $self->stash('x');
my $y = $self->stash('y');
my $candidate_file = $cache_files_dir->child("$zoom-$x-$y.png");
if ( -f $candidate_file ) {
$self->_cache_response;
return $self->_render_png($candidate_file);
}
if ( !defined $self->current_user ) {
return $self->render(
status => 401,
text => '¡¡No estás loggeado, no puedes cargar mapa nuevo.!!'
);
}
$self->_cache_response;
my $file_to_write = $candidate_file;
my $ua = Mojo::UserAgent->new;
my $png_tile =
$ua->get("https://tile.openstreetmap.org/$zoom/$x/$y.png")->result->body;
open my $fh, '|-', 'convert', '/dev/stdin', '-channel', 'RGB', '-negate',
$file_to_write;
print $fh $png_tile;
close $fh;
$self->_render_png($file_to_write);
$self->_delete_extra_files();
}
sub _delete_extra_files ($self) {
my @files = $cache_files_dir->children;
if ( scalar @files < 20001 ) {
return;
}
@files = sort { -M $a <=> -M $b } @files;
for ( my $i = 0 ; $i < ( scalar @files ) - 20000 ; $i++ ) {
system 'rm', '-v', $files[$i];
}
}
sub _render_png ( $self, $file ) {
system 'touch', $file;
return $self->render( data => $file->slurp_raw, status => 200,
format => 'png' );
}
1;

View File

@ -0,0 +1,103 @@
package BurguillosInfo::Controller::ConquerUserCurrentEnemy;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use JSON;
use BurguillosInfo::Schema;
use BurguillosInfo::Species;
sub listEnemiesGlobal ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'Debes estar autenticado.',
}
);
}
my $current_enemies = $self->_get_enemies($user);
if ( scalar @$current_enemies ) {
return $self->_return_enemies($current_enemies);
}
$self->_generate_enemies_global($user);
$current_enemies = $self->_get_enemies($user);
return $self->_return_enemies($current_enemies);
}
sub _return_enemies ( $self, $current_enemies ) {
return $self->render( json => [ map { $_->serialize } @$current_enemies ] );
}
sub _get_enemies ( $self, $user ) {
my $resultset_current_enemies =
BurguillosInfo::Schema->Schema->resultset('ConquerUserCurrentEnemy');
my @current_enemies = $resultset_current_enemies->search(
{
'user_object.uuid' => $user->uuid,
},
{
join => 'user_object',
}
);
return \@current_enemies;
}
sub _generate_enemies_global ( $self, $user ) {
my $minimum_number_enemies = 2;
my $maximum_number_enemies = 6;
my $number_enemies =
$self->_calculate_number_of_enemies( $minimum_number_enemies,
$maximum_number_enemies );
for ( my $i = 0 ; $i < $number_enemies ; $i++ ) {
$self->_generate_enemy_global($user);
}
}
sub _generate_enemy_global ( $self, $user ) {
my $resultset_current_enemies =
BurguillosInfo::Schema->Schema->resultset('ConquerUserCurrentEnemy');
my $uuid = create_uuid_string();
my $species = BurguillosInfo::Species->new;
my @species = @{ $species->list_can_be_global };
my $selected_species = $species[int( rand( scalar @species ) )];
my $enemy = $resultset_current_enemies->new(
{
uuid => $uuid,
species => $selected_species->id,
level => $self->_get_level_enemy($user),
user => $user->uuid,
}
);
$enemy->insert;
}
sub _get_level_enemy ( $self, $user ) {
my $max_enemy_level = int( $user->level / 2 ) + 1;
if ( $max_enemy_level < $user->level - 10 ) {
$max_enemy_level = $user->level - 10;
}
if ( $max_enemy_level < 3 ) {
$max_enemy_level = 3;
}
my $min_enemy_level = $max_enemy_level - 5;
if ( $min_enemy_level < 2 ) {
$min_enemy_level = 2;
}
return $min_enemy_level +
int( rand( $max_enemy_level - $min_enemy_level + 1 ) );
}
sub _calculate_number_of_enemies ( $self, $min, $max ) {
return $min + int( rand( $max - $min + 1 ) );
}
1;

View File

@ -89,8 +89,6 @@ sub submit_login {
$self->render( text => 'Server error.', status => 500 );
return;
}
say $password;
say $bcrypted_pass;
if ( !bcrypt_check( $password, $bcrypted_pass ) ) {
$self->render( text => 'Wrong password', status => 401 );
return;

View File

@ -0,0 +1,235 @@
package BurguillosInfo::Controller::UserConquer;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use Crypt::Bcrypt qw/bcrypt bcrypt_check/;
use Crypt::URandom qw/urandom/;
use JSON;
use BurguillosInfo::Schema;
my $username_minimum_chars = 3;
my $username_maximum_chars = 15;
my $password_minimum_chars = 8;
my $password_maximum_chars = 4096;
sub setTeamForUser($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->_renderError(401, 'No estás loggeado.');
}
my $input = $self->_expectJson;
if (!defined $input) {
return;
}
my $node_uuid = $input->{node};
my $team_uuid = $input->{team};
my $resultset_team = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my $resultset_node = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my @teams = $resultset_team->search({uuid => $team_uuid});
my @nodes = $resultset_node->search({uuid => $node_uuid});
if (scalar @teams < 1) {
return $self->render(status => 404, json => {
error => 'No se encontró ese equipo.',
});
}
if (scalar @nodes < 1) {
return $self->render(status => 404, json => {
error => 'No se encontró este nodo.',
});
}
my $team = $teams[0];
my $node = $nodes[0];
if (!$node->is_near($user)) {
return $self->render(status => 400, json => {
error => 'Estás demasiado lejos del nodo.',
});
}
$user = $user->get_from_storage;
$user->team_object($team);
$user->update;
return $self->render(json => {
ok => $JSON::true,
});
}
sub get_self ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->_renderError( 401, 'No estás loggeado.' );
}
return $self->render( json => $user->serialize_to_owner, status => 200 );
}
sub create ($self) {
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $username = $input->{username};
my $password = $input->{password};
my $repeat_password = $input->{repeat_password};
return
unless $self->_createCheckInput( $username, $password, $repeat_password );
return $self->_createUser( $username, $password );
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
sub login ($self) {
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $username = $input->{username};
my $password = $input->{password};
my $resultset_conquer_user =
BurguillosInfo::Schema->Schema->resultset('ConquerUser');
my @tentative_users =
$resultset_conquer_user->search( { username => $username } );
my $tentative_user = $tentative_users[0];
if ( !defined $tentative_user ) {
$self->_renderError( 401, 'El usuario especificado no existe.' );
return;
}
if ( !bcrypt_check( $password, $tentative_user->encrypted_password ) ) {
$self->_renderError( 401, 'Contraseña incorrecta.' );
return;
}
my $user = $tentative_user;
$self->set_current_user($user);
$self->render(
json => {
success => $JSON::true
},
status => 200
);
}
sub setCoordinates ($self) {
my $input = $self->_expectJson;
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'Debes estar loggeado para cambiar tus'
. ' coordenadas.',
}
);
}
if ( !defined $input ) {
return;
}
if ( ref $input ne 'ARRAY' && scalar $input->@* == 2 ) {
return $self->render(
status => 400,
json => {
error => 'Mal formato de coordenadas, debe ser '
. 'un array de exactamente 2 números reales.',
}
);
}
$user->coordinates($input);
$user->update;
return $self->render(
status => 200,
json => {
ok => $JSON::true,
}
);
}
sub _createUser ( $self, $username, $password ) {
my $user;
my $uuid = create_uuid_string();
my $new_salt = urandom(16);
my $encrypted_password = bcrypt $password, '2b', 12, $new_salt;
eval {
$user = BurguillosInfo::Schema->Schema->resultset('ConquerUser')->new(
{
uuid => $uuid,
encrypted_password => $encrypted_password,
username => $username
}
);
$user->coordinates( [ 0, 0 ] );
$user->insert;
};
if ($@) {
if ( $@ =~ /Key \((.*?)\)=\((.*?)\) already exists\./ ) {
return $self->_renderError( 400,
"La clave $1 ($2) ya existe en la base de datos.",
);
}
say STDERR $@;
return $self->_renderError( 400,
'No se pudo crear el usuario por razones desconocidas.' );
}
$self->render( status => 200, json => $user->serialize_to_owner );
return 1;
}
sub _renderError ( $self, $status, $message ) {
$self->render( status => $status, json => { error => $message } );
return 0;
}
sub _createCheckInput ( $self, $username, $password, $repeat_password ) {
if ( !defined $username
|| $username !~
/^(?:\w|\d|[ÑÁÉÍÓÚñáéíóú ]){$username_minimum_chars,$username_maximum_chars}$/
)
{
return $self->_renderError( 400,
"Username invalido, las reglas son tamaño entre $username_minimum_chars y $username_maximum_chars"
. ' carácteres y solo se podrán usar letras, números y espacios.'
);
}
if ( !defined $password
|| $password eq $username
|| $password !~ /^.{$password_minimum_chars,$password_maximum_chars}$/
|| $password =~ /^\d+$/ )
{
return $self->_renderError(
400,
'Contraseña invalida, las reglas son la contraseña debe ser'
. ' distinta al nombre de usuario, la contraseña debe tener entre'
. " $password_minimum_chars y $password_maximum_chars carácteres"
. ' (Tu contraseña no se guardará en texto plano, el límite de'
. " $password_maximum_chars caracteres es para evitar denegaciones"
. ' de servicio), la contraseña no puede estar compuesta solo de números.',
);
}
if ( !defined $repeat_password || $password ne $repeat_password ) {
$self->_renderError(
400,
'El campo de repetir contraseña debe coincidir de forma'
. ' totalmente exacta con el campo de contraseña para asegurar'
. ' que podrás recordar la contraseña y/o que no has cometido'
. ' ningún error, si pierdes el acceso a tu cuenta no podrás'
. ' recuperarlo de ningún modo.',
);
return 0;
}
return 1;
}
1;

View File

@ -28,11 +28,11 @@ sub MIGRATIONS {
path TEXT,
FOREIGN KEY (path) REFERENCES paths(path)
)',
'ALTER TABLE paths ADD column last_seen TIMESTAMP;',
'ALTER TABLE paths ADD COLUMN last_seen TIMESTAMP;',
'ALTER TABLE paths ALTER COLUMN last_seen SET DEFAULT NOW();',
'ALTER TABLE requests ADD PRIMARY KEY (uuid)',
'CREATE INDEX request_extra_index on requests (date, path);',
'ALTER TABLE requests ADD column referer text;',
'ALTER TABLE requests ADD COLUMN referer text;',
'CREATE INDEX request_referer_index on requests (referer);',
'ALTER TABLE requests ADD COLUMN country TEXT;',
'CREATE INDEX request_country_index on requests (country);',
@ -49,18 +49,69 @@ sub MIGRATIONS {
id_farmacia TEXT NOT NULL
);',
'CREATE INDEX farmacia_guardia_index on farmacia_guardia (date, id_farmacia, uuid);',
'CREATE TABLE conquer_user (
uuid UUID NOT NULL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
encrypted_password TEXT NOT NULL,
last_activity TIMESTAMP NOT NULL DEFAULT NOW(),
is_admin BOOLEAN NOT NULL DEFAULT false,
registration_date TIMESTAMP NOT NULL DEFAULT NOW()
);',
'CREATE TABLE conquer_node (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
coordinate_1 REAL NOT NULL,
coordinate_2 REAL NOT NULL,
description TEXT NOT NULL
);',
'CREATE INDEX index_conquer_node_coordinate_1 on conquer_node (coordinate_1);',
'CREATE INDEX index_conquer_node_coordinate_2 on conquer_node (coordinate_2);',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_1 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_1 DROP DEFAULT;',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_2 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_2 DROP DEFAULT;',
'CREATE TABLE conquer_teams (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT \'\',
points INTEGER NOT NULL DEFAULT 0
);',
'ALTER TABLE conquer_user ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_node ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_teams ADD COLUMN color TEXT NOT NULL DEFAULT \'#000\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#555\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#aaa\';',
'CREATE EXTENSION IF NOT EXISTS postgis;',
'ALTER TABLE conquer_node ADD COLUMN geometry GEOMETRY NULL;',
'UPDATE conquer_node SET geometry=ST_MakePoint(coordinate_1, coordinate_2);',
'ALTER TABLE conquer_node ALTER COLUMN geometry SET NOT NULL;',
'ALTER TABLE conquer_node DROP COLUMN coordinate_1;',
'ALTER TABLE conquer_node DROP COLUMN coordinate_2;',
'ALTER TABLE conquer_user ADD COLUMN experience INTEGER NOT NULL DEFAULT 125;',
'ALTER TABLE conquer_user ADD COLUMN current_hp INTEGER NOT NULL DEFAULT 999;',
'CREATE TABLE conquer_user_current_enemy (
uuid UUID NOT NULL PRIMARY KEY,
"user" UUID NOT NULL REFERENCES conquer_user(uuid),
species INTEGER NOT NULL,
is_battled BOOLEAN DEFAULT false,
is_selected_to_battle BOOLEAN DEFAULT false,
level INTEGER NOT NULL
);',
'ALTER TABLE conquer_user_current_enemy ALTER COLUMN species TYPE TEXT;',
);
}
sub _populate_locations ($dbh) {
require BurguillosInfo;
require BurguillosInfo::Tracking;
my $tracking = BurguillosInfo::Tracking->new( BurguillosInfo->new );
my $page = 0;
while (1) {
last if !_update_request_page( $dbh, $tracking, $page );
$page += 100;
}
# This subroutine crashes the migrations.
# require BurguillosInfo;
# require BurguillosInfo::Tracking;
# my $tracking = BurguillosInfo::Tracking->new( BurguillosInfo->new );
# my $page = 0;
# while (1) {
# last if !_update_request_page( $dbh, $tracking, $page );
# $page += 100;
# }
}
sub _update_request_page ( $dbh, $tracking, $page ) {

View File

@ -0,0 +1,64 @@
package BurguillosInfo::Schema;
use v5.36.0;
use strict;
use warnings;
use utf8;
our $VERSION = 1;
use feature 'signatures';
use BurguillosInfo;
use parent 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces();
my $schema;
sub Schema ($class) {
if ( !defined $schema ) {
use BurguillosInfo::DB;
BurguillosInfo::DB->connect;
my $app = BurguillosInfo->new;
my $config = $app->{config};
my $database_config = $config->{db};
my $dbname = $database_config->{database};
my $host = $database_config->{host};
my $port = $database_config->{port};
my $user = $database_config->{user};
my $password = $database_config->{password};
my $dsn = 'dbi:Pg:';
if ( !defined $dbname ) {
die "The key database/dbname must be configured.";
}
$dsn .= "dbname=$dbname";
if ( defined $host ) {
$dsn .= ";host=$host";
}
if ( defined $port ) {
$dsn .= ";port=$port";
}
# Undef is perfectly fine for username and password.
$schema = $class->connect(
$dsn, $user,
$password,
{
auto_savepoint => 1,
Callbacks => {
connected => sub {
shift->do('set timezone = UTC');
return;
}
},
quote_char => '"',
}
);
}
return $schema;
}
1;

View File

@ -0,0 +1,121 @@
package BurguillosInfo::Schema::Result::ConquerNode;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
use GIS::Distance;
__PACKAGE__->table('conquer_node');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
default_value => \'0',
},
type => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
},
geometry => {
data_type => 'geometry',
is_nullable => 0,
},
team => {
data_type => 'uuid',
is_nullable => 1,
},
);
sub coordinate_2 ($self) {
require BurguillosInfo::Schema;
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my ($new_self) = $resultset->search(
{ uuid => $self->uuid },
{
'+select' => {
ST_Y => { ST_Centroid => 'geometry' },
-as => 'coordinate_2',
}
}
);
return $new_self->get_column('coordinate_2');
}
sub coordinate_1 ($self) {
require BurguillosInfo::Schema;
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my ($new_self) = $resultset->search(
{ uuid => $self->uuid },
{
'+select' => {
ST_X => { ST_Centroid => 'geometry' },
-as => 'coordinate_1',
}
}
);
return $new_self->get_column('coordinate_1');
}
sub serialize ( $self, $player = undef ) {
$self = $self->get_from_storage();
my $return = {
kind => 'ConquerNode',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
type => $self->type,
coordinate_1 => $self->coordinate_1,
coordinate_2 => $self->coordinate_2,
is_near => $self->is_near($player),
team => $self->team,
};
return $return;
}
sub is_near ( $self, $player ) {
if ( !defined $player ) {
return $JSON::false;
}
# Meters
if ( $self->get_distance_to_player($player) < 100 ) {
return $JSON::true;
}
return $JSON::false;
}
sub get_distance_to_player ( $self, $player ) {
my $longitude_player = $player->last_coordinate_1;
my $latitude_player = $player->last_coordinate_2;
my $longitude_node = $self->coordinate_1;
my $latitude_node = $self->coordinate_2;
my $gis = GIS::Distance->new;
# Setting distance to meters.
my $distance = $gis->distance_metal(
$latitude_node, $longitude_node,
$latitude_player, $longitude_player
) * 1000;
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to( 'team_object',
'BurguillosInfo::Schema::Result::ConquerTeam', 'team' );
1;

View File

@ -0,0 +1,55 @@
package BurguillosInfo::Schema::Result::ConquerTeam;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_teams');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
},
points => {
data_type => 'integer',
is_nullable => 0,
},
color => {
data_type => 'text',
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->has_many( players => 'BurguillosInfo::Schema::Result::ConquerUser', 'team');
__PACKAGE__->has_many( nodes => 'BurguillosInfo::Schema::Result::ConquerNode', 'team');
sub serialize ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerTeam',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
points => $self->points,
color => $self->color,
};
}
1;

View File

@ -0,0 +1,153 @@
package BurguillosInfo::Schema::Result::ConquerUser;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_user');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
team => {
data_type => 'uuid',
is_nullable => 1,
},
username => {
data_type => 'text',
is_nullable => 0,
},
encrypted_password => {
data_type => 'text',
is_nullable => 0,
},
last_activity => {
data_type => 'timestamp',
is_nullable => 0,
default_value => \'NOW()',
},
registration_date => {
data_type => 'timestamp',
is_nullable => 0,
default_value => \'NOW()',
},
is_admin => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'0',
},
last_coordinate_1 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
last_coordinate_2 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
experience => {
data_type => 'integer',
is_nullable => 0,
default_value => \'125',
},
current_hp => {
data_type => 'integer',
is_nullable => 0,
default_value => \'999',
}
);
sub max_health($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $born_value) * $self->level)
/ 100 + $self->level + 10
);
}
sub level($self) {
$self = $self->get_from_storage();
return int($self->experience ** (1/3) + 0.0000000000001);
}
sub attack($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $self->level)*$self->level)
/100
);
}
sub defense($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $self->level)*$self->level)
/100
);
}
sub health($self, $health = undef) {
$self = $self->get_from_storage();
my $hp = $self->current_hp;
if ($hp > $self->max_health) {
$self->current_hp($self->max_health);
$self->update;
$self = $self->get_from_storage();
}
if (defined $health) {
if ($health > $self->max_health) {
$health = $self->max_health;
}
$self->current_hp($health);
$self->update;
$self = $self->get_from_storage();
}
return $self->current_hp;
}
sub coordinates ( $self, $coordinates = undef ) {
if ( defined $coordinates ) {
if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
die 'The second parameter of this subroutine '
. 'must be an ARRAYREF of exactly two elements.';
}
$self->last_coordinate_1( $coordinates->[0] );
$self->last_coordinate_2( $coordinates->[1] );
}
return [ $self->last_coordinate_1, $self->last_coordinate_2 ];
}
sub serialize_to_owner ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerUser',
uuid => $self->uuid,
team => $self->team,
username => $self->username,
is_admin => $self->is_admin ? $JSON::true : $JSON::false,
last_activity => $self->last_activity,
registration_date => $self->registration_date,
};
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to('team_object', 'BurguillosInfo::Schema::Result::ConquerTeam', 'team');
__PACKAGE__->add_unique_constraint( "unique_constraint_username",
['username'] );
1;

View File

@ -0,0 +1,101 @@
package BurguillosInfo::Schema::Result::ConquerUserCurrentEnemy;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
use BurguillosInfo::Species;
__PACKAGE__->table('conquer_user_current_enemy');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
user => {
data_type => 'uuid',
is_nullable => 0,
},
species => {
data_type => 'text',
is_nullable => 0,
},
is_selected_to_battle => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'false',
},
is_battled => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'false',
},
level => {
data_type => 'integer',
is_nullable => 0,
},
);
sub serialize ($self) {
my $species = BurguillosInfo::Species->new;
my $specie = $species->get( $self->species );
return {
uuid => $self->uuid,
species => $specie->serialize,
level => $self->level,
max_health => $self->max_health,
};
}
sub max_health ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return
int( ( ( $base * 2 + $born_value ) * $self->level ) / 100 +
$self->level +
10 );
}
sub experience_drop ($self) {
$self = $self->get_from_storage();
return int( $self->level / 7 * 179 );
}
sub experience ($self) {
$self = $self->get_from_storage();
return int( $self->level**(3) );
}
sub attack ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int( ( ( $base * 2 + $self->level ) * $self->level ) / 100 );
}
sub defense ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int( ( ( $base * 2 + $self->level ) * $self->level ) / 100 );
}
sub health ($self) {
# Combat result is decided from the start of battle.
return $self->max_health;
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to( 'user_object',
'BurguillosInfo::Schema::Result::ConquerUser', 'user' );
1;

View File

@ -0,0 +1,24 @@
package BurguillosInfo::Specie;
use v5.36.0;
use strict;
use warnings;
use feature 'signatures';
use Moo::Role;
sub can_be_global {
return 0;
}
sub serialize ($self) {
return {
id => $self->id,
name => $self->name,
image => $self->image,
};
}
requires 'id name image';
1;

View File

@ -0,0 +1,58 @@
package BurguillosInfo::Species;
use v5.36.0;
use strict;
use warnings;
use Moo;
use Module::Pluggable
search_path => ['BurguillosInfo::Species'],
instantiate => 'new',
on_require_error => sub ( $plugin, $error ) {
die $error;
};
{
my %hash_species;
sub _hash ($self) {
if ( !scalar keys %hash_species ) {
$self->_populate_hash;
}
return {%hash_species};
}
sub _populate_hash ($self) {
my @species = $self->plugins();
print Data::Dumper::Dumper \@species;
for my $specie (@species) {
$self->_check_specie_valid($specie);
if (exists $hash_species{$specie->id}) {
die "Duplicated species id @{[$specie->id]}.";
}
$hash_species{$specie->id} = $specie;
}
}
}
sub _check_specie_valid ( $self, $specie ) {
if ( !$specie->does('BurguillosInfo::Specie') ) {
die "$specie does not implement BurguillosInfo::Specie.";
}
}
sub get($self, $id) {
return $self->_hash->{$id};
}
sub list($self) {
my @species_keys = keys %{$self->_hash};
my $species = [ sort { $a->id cmp $b->id } map { $self->_hash->{$_} } @species_keys ];
return $species;
}
sub list_can_be_global($self) {
return [ grep { $_->can_be_global } $self->list->@* ];
}
1;

View File

@ -0,0 +1,28 @@
package BurguillosInfo::Species::Murcielago;
use v5.34.1;
use strict;
use warnings;
use Moo;
use parent 'BurguillosInfo::Specie';
sub id {
return 'murcielago';
}
sub name {
return 'Murcielago';
}
sub image {
return '/img/conquer/species/murcielago.png';
}
sub can_be_global {
return 1;
}
1;

View File

@ -25,7 +25,10 @@
"dependencies": {
"babel-loader": "^9.1.3",
"ol": "^8.1.0",
"protoc-gen-js": "^3.21.2",
"tablesort": "^5.3.0",
"ts-loader": "^9.5.0"
"ts-loader": "^9.5.0",
"ts-protoc-gen": "^0.15.0",
"typescript-json-serializer": "^6.0.1"
}
}

View File

@ -9,6 +9,162 @@ body {
min-height: 100%;
width: 100%;
height: 100%; }
body span.conquer-team-circle {
display: inline-block;
aspect-ratio: 1 / 1;
height: 1rem;
border-radius: 50%; }
body div.conquer-team-to-select {
padding: 5px;
border-radius: 5px;
background: beige;
border: solid black; }
body div.conquer-team-to-select button {
height: 60px; }
body p.conquer-register-error, body p.conquer-login-error, body p.conquer-login-success, body p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
background: blanchedalmond;
padding: 3px;
border-radius: 10px;
border: solid 1px black;
overflow-y: scroll; }
body form {
display: flex;
flex-direction: column;
width: 100%; }
body form label {
width: 100%; }
body form label input, body form label textarea, body form label select {
width: 100%;
border: none;
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem; }
body form label textarea {
height: 100px; }
body div.conquer-interface-element-padded {
width: calc(100% - 60px);
padding-left: 30px;
padding-right: 30px;
display: flex;
justify-content: center; }
body div.conquer-interface-element-padded.conquer-display-block {
display: block; }
body div.conquer-interface-element-padded.conquer-display-none {
display: none; }
body div.fight-battle-selector-slide {
display: flex;
position: fixed;
z-index: 1;
top: 0;
height: 100px; }
body div.fight-battle-selector-slide.conquer-display-none {
display: none; }
body div.fight-battle-selector-slide img {
height: 50px;
aspect-ratio: 1 / 1; }
body div.create-node-slide {
display: flex;
position: fixed;
z-index: 1;
bottom: 0;
height: 100px; }
body div.create-node-slide.conquer-display-none {
display: none; }
body p.conquer-login-success {
color: green; }
body a.conquer-exit-button {
color: white;
font-weight: bold;
text-decoration: none;
background: darkmagenta;
padding: 10px;
border-radius: 50%;
aspect-ratio: 1 / 1;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: xx-large;
border: 2px black solid; }
body div.conquer-overlay-transparent {
background: black;
opacity: 50%;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1; }
body div.conquer-self-player {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: 0;
left: 0;
width: calc(100% - 12px);
height: calc(100% - 22px);
margin: 5px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: scroll; }
body div.conquer-top-bar {
display: flex;
width: calc(100% - 20px);
border-radius: 30px 30px 0 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
background: darkcyan;
margin-left: 0;
border-bottom: 1px black solid;
display: flex;
justify-content: end; }
body div.conquer-login, body div.conquer-register {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: calc( 50% - 200px - 10px);
left: calc( 50% - 150px - 10px);
padding: 10px;
height: 400px;
width: 300px;
z-index: 1; }
body div.conquer-login form, body div.conquer-register form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center; }
body .conquer-display-none {
display: none; }
body div.conquer-container {
background: black;
height: 100dvh;
width: 100%; }
body div.conquer-select-fight div.conquer-image-container {
display: flex;
justify-content: center;
align-items: center; }
body div.conquer-select-fight div.conquer-button-container {
display: flex;
justify-content: center;
align-items: center; }
body div.ol-control {
display: none; }
body span.round-center {
background: blueviolet;
color: #FEFEFA;

View File

@ -17,6 +17,194 @@ html {
}
body {
span.conquer-team-circle {
display: inline-block;
aspect-ratio: 1 / 1;
height: 1rem;
border-radius: 50%;
}
div.conquer-team-to-select {
padding: 5px;
border-radius: 5px;
background: beige;
border: solid black;
button {
height: 60px;
}
}
p.conquer-register-error, p.conquer-login-error, p.conquer-login-success,p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
background: blanchedalmond;
padding: 3px;
border-radius: 10px;
border: solid 1px black;
overflow-y: scroll;
}
form {
display: flex;
flex-direction: column;
width: 100%;
label {
width: 100%;
input, textarea, select {
width: 100%;
border:none;
background-image:none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem;
}
textarea {
height: 100px;
}
}
}
div.conquer-interface-element-padded {
width: calc(100% - 60px);
padding-left: 30px;
padding-right: 30px;
display: flex;
justify-content: center;
&.conquer-display-block {
display: block;
}
&.conquer-display-none {
display: none;
}
}
div.fight-battle-selector-slide {
display: flex;
position: fixed;
z-index: 1;
top: 0;
height: 100px;
&.conquer-display-none {
display: none;
}
img {
height: 50px;
aspect-ratio: 1 / 1;
}
}
div.create-node-slide {
display: flex;
position: fixed;
z-index: 1;
bottom: 0;
height: 100px;
&.conquer-display-none {
display: none;
}
}
p.conquer-login-success {
color: green;
}
a.conquer-exit-button {
color: white;
font-weight: bold;
text-decoration: none;
background: darkmagenta;
padding: 10px;
border-radius: 50%;
aspect-ratio: 1 / 1;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: xx-large;
border: 2px black solid;
}
div.conquer-overlay-transparent {
background: black;
opacity: 50%;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
div.conquer-self-player {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: 0;
left: 0;
width: calc(100% - 12px);
height: calc(100% - 22px);
margin: 5px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: scroll;
}
div.conquer-top-bar {
display: flex;
width: calc(100% - 20px);
border-radius: 30px 30px 0 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
background: darkcyan;
margin-left: 0;
border-bottom: 1px black solid;
display: flex;
justify-content: end;
}
div.conquer-login,div.conquer-register {
form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: calc( 50% - 200px - 10px );
left: calc( 50% - 150px - 10px );
padding: 10px;
height: 400px;
width: 300px;
z-index: 1;
}
.conquer-display-none {
display: none;
}
div.conquer-container {
background: black;
height: 100dvh;
width: 100%;
}
div.conquer-select-fight {
div.conquer-image-container {
display: flex;
justify-content: center;
align-items: center;
}
div.conquer-button-container {
display: flex;
justify-content: center;
align-items: center;
}
}
div.ol-control {
display: none;
}
span.round-center {
background: $background_div;
color: $background_sidebar;

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="180"
height="180"
viewBox="0 0 180 180"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="arrow-player-killed.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="2.1722222"
inkscape:cx="114.39898"
inkscape:cy="58.925831"
inkscape:window-width="1499"
inkscape:window-height="991"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="layer2" />
<defs
id="defs2" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:0.99906439">
<ellipse
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3.22896;stroke-dasharray:none;stroke-opacity:1"
id="path1514"
cy="90"
cx="90"
rx="87.179558"
ry="88.385521" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:0.99906439">
<path
style="display:inline;fill:#000000;fill-opacity:0.999064;stroke:none;stroke-width:3.42195;stroke-dasharray:none;stroke-opacity:1"
d="m 27.1872,145.48336 c -0.325678,6.59573 4.86944,13.81584 14.628956,9.61774 38.996707,-20.08168 63.312444,-21.05373 94.835564,1.87477 9.98034,4.08129 14.61469,-3.036 15.90751,-6.76847 C 153.79696,107.06667 118.11757,6.5875149 90,1.6144791 63.092475,7.5716541 26.88641,91.183358 27.1872,145.48336 Z"
id="path1682"
sodipodi:nodetypes="cccccc" />
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.805;stroke-dasharray:none;stroke-opacity:0"
d="m 109.99321,42.936903 5.25546,0.250702 -11.87761,18.116623 -5.190631,-0.233775 z"
id="path16212"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.805;stroke-dasharray:none;stroke-opacity:0"
d="m 98.180429,45.18702 4.515381,-0.518513 12.68756,15.571862 -5.93341,0.608601 z"
id="path16538"
sodipodi:nodetypes="ccccc" />
<path
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.805;stroke-dasharray:none;stroke-opacity:0"
d="m 75.170413,42.936903 5.25546,0.250702 -11.87761,18.116623 -5.190631,-0.233775 z"
id="path16212-5"
sodipodi:nodetypes="ccccc" />
<path
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.805;stroke-dasharray:none;stroke-opacity:0"
d="m 63.357632,45.18702 4.515381,-0.518513 12.68756,15.571862 -5.93341,0.608601 z"
id="path16538-3"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:2.39555;stroke-dasharray:none;stroke-opacity:0.997527"
d="m 60.222542,111.59197 2.432165,2.37521 7.776952,-12.17573 5.48651,12.67675 7.484611,-13.49323 6.616088,13.42642 5.917787,-12.95044 6.241525,15.681 7.16239,-16.32398 3.56976,17.01614 7.0569,-13.92744 -1.49286,-2.20634 -5.24564,9.43495 -3.04735,-15.193902 -7.75217,14.476702 -6.68575,-13.508063 -5.724592,11.749843 -6.278623,-12.941158 -7.663829,12.631268 -5.33589,-11.255318 z"
id="path16739" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

107
public/img/arrow-player.svg Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="180"
height="180"
viewBox="0 0 180 180"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="arrow-player.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="false"
inkscape:zoom="4.3444444"
inkscape:cx="86.777494"
inkscape:cy="90.000001"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer3" />
<defs
id="defs2" />
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 3"
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:0.99906439">
<ellipse
style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:10.6795;stroke-dasharray:none;stroke-opacity:1"
id="path1514"
cy="90"
cx="90"
rx="84.558273"
ry="84.660248" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 2"
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:0.99906439">
<path
style="stroke:none;stroke-opacity:1;fill:#000000;fill-opacity:0.999064;display:inline;stroke-width:3.305;stroke-dasharray:none"
d="m 28.658815,141.34251 c -0.325678,6.15261 4.86944,12.88765 14.628956,8.97159 38.996707,-18.73253 63.312439,-19.63928 94.835559,1.74882 9.98035,3.8071 14.6147,-2.83203 15.90752,-6.31374 C 155.26857,105.50676 119.58918,11.778083 91.471615,7.1391499 64.56409,12.696105 28.358025,90.690537 28.658815,141.34251 Z"
id="path1682"
sodipodi:nodetypes="cccccc" />
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="display:inline">
<path
style="fill:#ffffff;fill-opacity:0.999064;stroke:#000000;stroke-width:2.88197;stroke-dasharray:none;stroke-opacity:1"
id="path7203"
sodipodi:type="arc"
sodipodi:cx="74.857635"
sodipodi:cy="50.984001"
sodipodi:rx="12.305052"
sodipodi:ry="10.465668"
sodipodi:start="6.0340459"
sodipodi:end="6.0280938"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="M 86.782769,48.403481 A 12.305052,10.465668 0 0 1 77.909434,61.12269 12.305052,10.465668 0 0 1 62.941585,53.594694 12.305052,10.465668 0 0 1 71.770374,40.853082 12.305052,10.465668 0 0 1 86.764498,48.343158" />
<path
style="display:inline;fill:#ffffff;fill-opacity:0.999064;stroke:#000000;stroke-width:2.8579;stroke-dasharray:none;stroke-opacity:1"
id="path7203-7"
sodipodi:type="arc"
sodipodi:cx="110.57592"
sodipodi:cy="50.984138"
sodipodi:rx="12.09339"
sodipodi:ry="10.471755"
sodipodi:start="6.0340459"
sodipodi:end="6.0280938"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="m 122.29593,48.402118 a 12.09339,10.471755 0 0 1 -8.72071,12.726606 12.09339,10.471755 0 0 1 -14.710381,-7.532374 12.09339,10.471755 0 0 1 8.676921,-12.749023 12.09339,10.471755 0 0 1 14.73621,7.494432" />
<path
style="fill:#000000;fill-opacity:0;stroke:#ffffff;stroke-width:2.26257;stroke-dasharray:none;stroke-opacity:1"
id="path7803"
sodipodi:type="arc"
sodipodi:cx="93.89315"
sodipodi:cy="100.6772"
sodipodi:rx="22.730469"
sodipodi:ry="15.978166"
sodipodi:start="0.031220556"
sodipodi:end="3.1729889"
sodipodi:open="true"
sodipodi:arc-type="arc"
d="M 116.61254,101.17597 A 22.730469,15.978166 0 0 1 104.63719,114.75778 22.730469,15.978166 0 0 1 81.91671,114.25759 22.730469,15.978166 0 0 1 71.173884,100.17563" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 956 B

BIN
public/img/missingicon.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
<div id="conquer-select-team-list-template" class="conquer-display-none conquer-interface-element-padded conquer-display-block">
<h1>Encuentra tu equipo ideal.</h1>
</div>

View File

@ -0,0 +1,5 @@
<div id="conquer-team-to-select-template" class="conquer-team-to-select conquer-display-none">
<p class="conquer-name"></p>
<p class="conquer-description"></p>
<button class="conquer-submit">Elegir este equipo.</button>
</div>

View File

@ -0,0 +1,5 @@
<div id="conquer-view-node-template" class="conquer-display-none conquer-interface-element-padded conquer-display-block">
<h1>Vista de nodo.</h1>
<h2 class="node-name"></h2>
<p class="node-description"></p>
</div>

View File

@ -0,0 +1,4 @@
<div class="create-node-slide conquer-display-none" id="create-node-slide">
<button id="create-node-new-node">Autogenerado.</button>
<button id="create-node-exit">Salir del modo crear nodo.</button>
</div>

View File

@ -0,0 +1,6 @@
<div class="fight-battle-selector-slide conquer-display-none" id="fight-battle-selector-slide">
<button class="fight-global-button">
<img alt="Fight global" src="/img/conquer/earth-bomb.png"/>
<p>Batalla global</p>
</button>
</div>

View File

@ -0,0 +1,2 @@
<div id="conquer-interface-element-padded-template" class="conquer-interface-element-padded conquer-display-none">
</div>

View File

@ -0,0 +1,5 @@
<div id="conquer-interface-with-top-bar-template" class="conquer-self-player conquer-display-none">
<div class="conquer-top-bar">
<a href="#" class="conquer-exit-button">X</a>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div id="conquer-login-template" class="conquer-login conquer-display-none">
<form>
<p class="conquer-login-error conquer-display-none"></p>
<p class="conquer-login-success conquer-display-none"></p>
<label>Nombre de usuario</label>
<input class="conquer-login-username"/>
<label>Contraseña</label>
<input class="conquer-login-password" type="password"/>
<button class="conquer-login-submit">Inicia sesión</button>
<p>¿No tienes cuenta aun? <a href="#" class="conquer-login-go-to-register">Registrate</a></p>
</form>
</div>

View File

@ -0,0 +1,19 @@
<div id="conquer-new-node-form-creation-template" class="conquer-new-node-form-creation conquer-display-none conquer-interface-element-padded">
<form>
<p class="conquer-error conquer-display-none"></p>
<label>Tipo de Nodo<br/>
<select class="conquer-node-type">
<option value="normal">Normal (Conquistable)</option>
</select>
</label>
<div>
<label>Nuevo Nombre de Nodo<br/>
<input type="text" class="conquer-node-name"/></label>
</div>
<label>Descripción de la Zona<br/>
<textarea class="conquer-node-description"></textarea></label>
<div>
<button class="new-node-form-submit">Finalizar creación de nodo.</button>
</div>
</form>
</div>

View File

@ -0,0 +1,18 @@
<div id="conquer-new-team-form-creation-template" class="conquer-new-team-form-creation conquer-display-none conquer-interface-element-padded">
<form>
<p class="conquer-error conquer-display-none"></p>
<label>Nombre del equipo.<br/>
<input class="conquer-team-name"/>
</label>
<label>Descripción del equipo.<br/>
<textarea class="conquer-team-description"></textarea></label>
</label>
<label>Color del equipo<br/>
<input type="color" class="conquer-team-color"/>
</label>
<div>
<button class="new-team-form-submit">Finalizar creación de nodo.</button>
</div>
</form>
</div>

View File

@ -0,0 +1,2 @@
<div id="conquer-overlay-transparent-template" class="conquer-overlay-transparent conquer-display-none">
</div>

View File

@ -0,0 +1,13 @@
<div id="conquer-register-template" class="conquer-register conquer-display-none">
<form>
<p class="conquer-register-error conquer-display-none"></p>
<label>Nombre de usuario</label>
<input class="conquer-register-username"/>
<label>Contraseña</label>
<input class="conquer-register-password" type="password"/>
<label>Repite la contraseña</label>
<input class="conquer-register-repeat-password" type="password"/>
<button class="conquer-register-submit">Finaliza el registro</button>
<p>¿Ya estás registrado? <a href="#" class="conquer-register-go-to-login">Inicia Sesión</a>.</p>
</form>
</div>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
% my $css_version = config 'css_version';
<html>
<head>
<title>Conquista Burguillos</title>
<script src="/js/bundle.js?v=<%=$css_version%>"></script>
<link rel="stylesheet" href="/css/styles.css?v=<%=$css_version%>"/>
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0'/>
</head>
<body>
<div id="conquer-select-fight-list-template" class="conquer-display-none conquer-interface-element-padded conquer-display-block">
<h1>Elige un rival.</h1>
</div>
<div id="conquer-select-fight-item-template" class="conquer-display-none conquer-interface-element-padded conquer-select-fight">
<div class="conquer-image-container">
<img alt="" class="conquer-image"/>
</div>
<p>
<span class="conquer-name"></span>
Nivel: <span class="conquer-level"></span>
</p>
<div class="conquer-button-container">
<button class="conquer-submit">Luchar</button>
</div>
</div>
%= include 'conquer/_fight-battle-selector-slide';
%= include 'conquer/_overlay-transparent-template';
%= include 'conquer/_new-node-form-creation-template';
%= include 'conquer/_create-node-slide';
%= include 'conquer/_interface-element-padded-template';
%= include 'conquer/_interface-with-top-bar-template';
%= include 'conquer/_login-template';
%= include 'conquer/_register-template';
%= include 'conquer/_new-team-form-creation-template';
%= include 'conquer/_conquer-select-team-list-template';
%= include 'conquer/_conquer-team-to-select-template';
%= include 'conquer/_conquer-view-node-template';
<div class="conquer-container">
</div>
</body>
</html>

View File

@ -1,5 +1,7 @@
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"outDir": "./public/js/",
"noImplicitAny": true,
"module": "es2020",