More map upgrades.

This commit is contained in:
BinarySandia04 2024-08-09 19:06:38 +02:00
parent 168d683b13
commit 55304c569d
18 changed files with 569 additions and 64 deletions

View File

@ -2,3 +2,12 @@
An open-source virtual table top for role-playing games
Falta posar coses legals de:
- WOC icones
- Iconoir
- Game Assets
- Fonts
- Splash art (Josep)
- Sounds (FreeSound)
- Caeora (Tokens)

View File

@ -46,7 +46,6 @@ const chat = ref([
let GetChatRef = () => chat;
socket.on('update-players', data => {
console.log(data);
players.value = [];
Object.keys(data).forEach((key) => {
players.value.push(data[key]);
@ -107,7 +106,6 @@ function Disconnect(){
}
function GetPlayer(player_campaign){
console.log(players.value);
let index = players.value.findIndex((p) => {return p._id == player_campaign});
if(index != -1) return players.value[index];
}

View File

@ -5,7 +5,6 @@ let InGameRef = () => inGameRef;
function LaunchGame(){
inGameRef.value = true;
console.log("jdksadjlo")
}
function ExitGame(){

View File

@ -1,5 +1,21 @@
import { initCustomFormatter, ref } from 'vue';
import Api from '@/services/Api'
import { GetCampaign } from './Dragonroll';
import { backendUrl } from './BackendURL';
function dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(","),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[arr.length - 1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
let offsetX = 0;
let offsetY = 0;
let scale = 1;
@ -77,6 +93,7 @@ function zoom(amount){
Draw();
}
let xUpperLeft = -Infinity;
let yUpperLeft = -Infinity;
let xDownRight = Infinity;
@ -112,19 +129,21 @@ function drawGrid(cellSize){
ctx.stroke();
ctx.strokeStyle = "red";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.rect(toMapX(-10), toMapY(-10), 10 * scale, 10 * scale);
ctx.stroke();
}
// Ok aqui coses del mapa en si
let gridSize = 150;
let images = [];
let lines_of_sight = [];
let currentMap = {
gridSize: undefined,
images: [],
lines_of_sight: [],
backgroundColor: '#0f0f0f',
title: "Untitled map"
};
let imageData = [];
const currentMapId = ref('');
let GetMapId = () => currentMapId;
let backgroundColor = ref('#0f0f0f');
function Draw(){
@ -135,7 +154,7 @@ function Draw(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
images.forEach((image) => {
imageData.forEach((image) => {
ctx.drawImage(image, toMapX(0), toMapY(0), image.naturalWidth * scale, image.naturalHeight * scale);
});
@ -143,39 +162,117 @@ function Draw(){
ctx.beginPath();
ctx.strokeStyle = "white";
ctx.lineWidth = 3;
lines_of_sight.forEach((line) => {
ctx.moveTo(toMapX(line[0].x * gridSize), toMapY(line[0].y * gridSize));
ctx.lineTo(toMapX(line[1].x * gridSize), toMapY(line[1].y * gridSize));
currentMap.lines_of_sight.forEach((line) => {
ctx.moveTo(toMapX(line[0].x * currentMap.gridSize), toMapY(line[0].y * currentMap.gridSize));
ctx.lineTo(toMapX(line[1].x * currentMap.gridSize), toMapY(line[1].y * currentMap.gridSize));
});
ctx.stroke();
drawGrid(gridSize);
if(currentMap.gridSize) drawGrid(currentMap.gridSize);
}
function ImportDD2VTT(data){
console.log(data);
var image = new Image();
image.onload = function() {
images.push(image);
Draw();
UploadResource(image).then((imagePath) => {
currentMap.images.push(imagePath);
CreateMap(currentMap);
ReloadImages();
})
};
gridSize = data.resolution.pixels_per_grid;
lines_of_sight = data.line_of_sight;
currentMap.gridSize = data.resolution.pixels_per_grid;
currentMap.lines_of_sight = data.line_of_sight;
backgroundColor.value = '#' + data.environment.ambient_light;
currentMap.backgroundColor = '#' + data.environment.ambient_light;
xUpperLeft = data.resolution.map_origin.x * data.resolution.pixels_per_grid;
yUpperLeft = data.resolution.map_origin.y * data.resolution.pixels_per_grid;
xDownRight = xUpperLeft + data.resolution.map_size.x * data.resolution.pixels_per_grid;
yDownRight = yUpperLeft + data.resolution.map_size.y * data.resolution.pixels_per_grid;
image.src = "data:image/png;base64," + data.image;
}
const mapList = ref([]);
let GetMapList = () => mapList;
function UpdateMapList(){
Api().get('/maps/list?campaign=' + GetCampaign()._id).then(response => {
mapList.value = response.data.data;
console.log(mapList.value);
}).catch((err) => console.log(err));
}
function ReloadImages(){
imageData = [];
currentMap.images.forEach(path => {
let image = new Image();
image.src = backendUrl + "public/" + path;
imageData.push(image);
image.onload = Draw;
});
}
function RenameMap(id, new_title){
currentMap.title = new_title;
SaveMap(id);
}
function SaveMap(id){
Api().post('/maps/update?campaign=' + GetCampaign()._id + "&map=" + id, {data: currentMap}).then(response => {
console.log("Map updated");
}).catch(err => console.log(err));
}
function NewMap(){
}
function DeleteMap(map_id){
}
function LoadMap(map){
currentMap = map.data;
currentMapId.value = map._id;
backgroundColor.value = currentMap.backgroundColor;
ReloadImages();
}
function UploadResource(image){
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append("image", dataURLtoFile(image.src));
Api().post('/maps/create-resource?campaign=' + GetCampaign()._id, formData, {
headers: { "Content-Type": "multipart/form-data"}
}).then(response => {
resolve(response.data.data);
}).catch(err => console.log(err));
});
}
function CreateMap(){
Api().post('/maps/create', {
campaign: GetCampaign()._id,
data: currentMap,
}).then(response => {
UpdateMapList();
}).catch(err => console.log(err));
}
let GetBackgroundColor = () => backgroundColor;
function ChangeBackgroundColor(color){
backgroundColor.value = color; // XD
currentMap.backgroundColor = color;
SaveMap(currentMapId.value);
}
export {
toMapX,
@ -188,4 +285,11 @@ export {
// Draw,
ImportDD2VTT,
GetBackgroundColor,
ChangeBackgroundColor,
GetMapId,
UpdateMapList,
GetMapList,
LoadMap,
RenameMap,
};

View File

@ -84,6 +84,16 @@ const defValues = {
id: 'system-selector',
title: "Select a game system",
close: true
},
'map_window': {
id: 'map_window',
title: 'Maps',
close: true
},
'combat_window': {
id: 'combat_window',
title: "Combat",
close: true
}
}
@ -203,7 +213,6 @@ function CreateWindow(type, data = {}){
windows.value.push(finalData);
// reload.value += 1;
console.log(windows.value);
setTimeout(() => SetOnTop(finalData.id), 0);
}
}

View File

@ -27,6 +27,18 @@ function OpenMapButtons(){
CreateWindow('map_buttons');
}
function OpenMapWindows(){
CreateWindow('map_window');
}
function OpenCombatMenu(){
CreateWindow('combat_window');
}
function ToggleGrid(){
}
watch(game, () => {
if(game.value && in_game.value){
AddSound(game.value);
@ -46,11 +58,14 @@ watch(game, () => {
<div class="vertical-button">
<IconButton icon="icons/iconoir/regular/menu.svg" :action="OpenCampaignPreview"></IconButton>
<IconButton icon="icons/iconoir/regular/cursor-pointer.svg"></IconButton>
<IconButton icon="icons/iconoir/regular/orthogonal-view.svg" :action="ToggleGrid"></IconButton>
<IconButton icon="icons/game-icons/000000/delapouite/rolling-dice-cup.svg" :action="OpenDiceMenu"></IconButton>
<IconButton icon="icons/game-icons/000000/lorc/crossed-sabres.svg" :action="OpenCombatMenu"></IconButton>
</div>
<div class="vertical-button gm" v-if="is_dm">
<IconButton icon="icons/iconoir/regular/map.svg" :action="OpenMapButtons"></IconButton>
<IconButton icon="icons/iconoir/regular/map.svg" :action="OpenMapWindows"></IconButton>
<IconButton icon="icons/iconoir/regular/hammer.svg" :action="OpenMapButtons"></IconButton>
</div>
<div class="horizontal-button">

View File

@ -20,6 +20,8 @@ import DiceWindow from '../windows/game/DiceWindow.vue'
import MapButtons from '../windows/dm/MapButtons.vue'
import EnvironmentWindow from '../windows/dm/EnvironmentWindow.vue'
import SystemSelectorWindow from '../windows/campaigns/SystemSelectorWindow.vue'
import MapWindow from '../windows/dm/MapWindow.vue'
import CombatWindow from '../windows/game/CombatWindow.vue'
// Gestionem ventanas
const reload = ReloadRef();
@ -40,7 +42,9 @@ const WindowMap = {
dice_menu: DiceWindow,
map_buttons: MapButtons,
environment: EnvironmentWindow,
system_selector: SystemSelectorWindow
system_selector: SystemSelectorWindow,
map_window: MapWindow,
combat_window: CombatWindow
};
</script>

View File

@ -0,0 +1,80 @@
<script setup>
import IconButton from '@/views/partials/game/IconButton.vue';
import { onMounted, ref, toRaw, watch } from 'vue';
import { GetMapId, LoadMap, RenameMap } from '../../services/Map';
const toggled = ref("");
const title = ref(null);
const mapId = GetMapId();
const props = defineProps(['data']);
let data = props.data;
function ViewMap(){
LoadMap(toRaw(data))
}
function DeleteMap(){
console.log("Delete map");
}
function Rename(){
RenameMap(data._id, title.value.value)
}
onMounted(() => {
title.value.value = data.data.title;
watch(mapId, () => {
if(mapId.value == data._id){
toggled.value = "toggled-yes";
} else {
toggled.value = "toggled-no";
}
});
})
</script>
<template>
<div class="map-entry-container">
<input type="text" ref="title" v-on:change.prevent="Rename">
<div class="horizontal-button">
<div class="toggler" :class="toggled"><IconButton icon="icons/iconoir/regular/eye.svg" :action="ViewMap" size="small"></IconButton></div>
<IconButton icon="icons/iconoir/regular/trash.svg" :action="DeleteMap" size="small"></IconButton>
</div>
</div>
</template>
<style scoped lang="scss">
.toggler {
transition: filter 0.2s;
}
.toggled-yes {
filter: invert(1);
}
.map-entry-container {
display: flex;
width: 100%;
background-color: var(--color-background-softer);
padding: 10px;
align-items: center;
&.selected {
background-color: var(--color-background-softest);
}
}
.horizontal-button {
margin-left: auto;
display: flex;
flex-direction: row;
user-select: none;
}
</style>

View File

@ -0,0 +1,46 @@
<script setup>
import { onMounted } from 'vue';
import { GetMapList, UpdateMapList } from '../../services/Map';
import MapEntry from './MapEntry.vue';
const maps = GetMapList();
onMounted(() => {
UpdateMapList();
})
</script>
<template>
<div class="map-list-container">
<MapEntry v-for="map in maps" :data="map" :id="map._id"></MapEntry>
<div class="no-maps-message" v-show="maps.length == 0">
<span class="create-map">You haven't created any map!</span>
<span class="create-map">Upload or create a new one</span>
</div>
</div>
</template>
<style scoped lang="scss">
.map-list-container {
display: flex;
height: 100%;
width: 100%;
flex-direction: column;
overflow-y: auto;
}
.create-map {
font-size: 14px;
color: var(--text-disabled);
}
.no-maps-message {
display: flex;
flex-direction: column;
padding: 10px;
height: 100%;
}
</style>

View File

@ -74,6 +74,8 @@ emitter.on('toast', data => {
border-radius: 6px;
text-align: center;
z-index: 9999999;
animation: slide-in 0.4s ease-in-out;
@keyframes slide-in {

View File

@ -1,15 +1,16 @@
<script setup>
const props = defineProps(['icon', 'action','size']);
const props = defineProps(['icon', 'action', 'size', 'toggled']);
let icon = props.icon;
let action = props.action;
let size = props.size;
let toggled = props.toggled;
</script>
<template>
<div class="icon-button sound-click" :class="size" v-on:click.prevent="action">
<div class="icon-button sound-click" :class="size + ' ' + toggled" v-on:click.prevent="action">
<img class="icon" draggable="false" :src="icon" :class="size">
</div>
</template>
@ -25,6 +26,15 @@ let size = props.size;
width: 42px;
}
&.small {
height: 24px;
width: 24px;
}
&.toggled {
filter: invert(0.9);
}
background-color: var(--color-background-soft);
border-radius: 6px;
display: flex;
@ -42,10 +52,19 @@ let size = props.size;
.icon {
height: 24px;
width: 24px;
}
.big {
&.big {
height: 38px;
width: 38px;
}
&.small {
height: 18px;
width: 18px;
}
}
</style>

View File

@ -5,7 +5,7 @@ import { onMounted, onUpdated, ref, watch } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import IconButton from '@/views/partials/game/IconButton.vue'
import ColorValue from '../../partials/parameters/ColorValue.vue';
import { GetBackgroundColor } from '../../../services/Map';
import { GetBackgroundColor, ChangeBackgroundColor } from '../../../services/Map';
const props = defineProps(['data']);
const data = props.data;
@ -24,7 +24,7 @@ onMounted(() => {
console.log(env_background.value.GetColor());
watch(env_background.value.GetColor(), () => {
GetBackgroundColor().value = env_background.value.GetColor().value; // XD
ChangeBackgroundColor(env_background.value.GetColor().value);
});
});

View File

@ -4,36 +4,22 @@ import WindowHandle from '@/views/partials/WindowHandle.vue';
import { onMounted, onUpdated, ref } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import IconButton from '@/views/partials/game/IconButton.vue'
import { ImportDD2VTT } from '../../../services/Map';
import { CreateChildWindow, GetPosition } from '../../../services/Windows';
const props = defineProps(['data']);
const data = props.data;
const handle = ref(null);
const mapUploader = ref(null);
let id = data.id;
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {x: 40, y: 500});
SetSize(id, {x: 40, y: 200});
ResetPosition(id, {x: 10, y: 200});
mapUploader.value.addEventListener('change', (event) => {
let file = event.target.files[0];
let reader = new FileReader();
reader.addEventListener('load', (event) => {
ImportDD2VTT(JSON.parse(event.target.result));
});
reader.readAsText(file);
})
});
function UploadButton(){
mapUploader.value.click();
}
function EditEnvironment(){
let winPos = GetPosition(id);
@ -44,19 +30,13 @@ function EditEnvironment(){
<template>
<form id="send-map-form" enctype="multipart/form-data">
<input name="file" type="file" accept=".dd2vtt" ref="mapUploader">
</form>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<div class="vertical-button">
<IconButton icon="icons/iconoir/regular/upload.svg" :action="UploadButton"></IconButton>
<hr>
<IconButton icon="icons/iconoir/regular/square-3d-three-points.svg" :action="UploadButton"></IconButton>
<IconButton icon="icons/iconoir/regular/orthogonal-view.svg" :action="UploadButton"></IconButton>
<IconButton icon="icons/iconoir/regular/square-3d-three-points.svg" :action="EditEnvironment"></IconButton>
<hr>
<IconButton icon="icons/iconoir/regular/sun-light.svg" :action="EditEnvironment"></IconButton>
</div>
@ -65,9 +45,7 @@ function EditEnvironment(){
<style scoped>
#send-map-form {
display: none;
}
.window-wrapper {
display: flex;

View File

@ -0,0 +1,83 @@
<script setup>
import WindowHandle from '@/views/partials/WindowHandle.vue';
import { onMounted, onUpdated, ref, watch } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import { ImportDD2VTT } from '../../../services/Map';
import MapList from '../../partials/MapList.vue';
import IconButton from '@/views/partials/game/IconButton.vue';
const props = defineProps(['data']);
const data = props.data;
const handle = ref(null);
const mapUploader = ref(null);
let id = data.id;
function UploadButton(){
mapUploader.value.click();
}
function NewMapButton(){
}
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {x: 300, y: 600});
ResetPosition(id, {x: 100, y: 10});
mapUploader.value.addEventListener('change', (event) => {
let file = event.target.files[0];
let reader = new FileReader();
reader.addEventListener('load', (event) => {
ImportDD2VTT(JSON.parse(event.target.result));
});
reader.readAsText(file);
})
});
</script>
<template>
<form id="send-map-form" enctype="multipart/form-data">
<input name="file" type="file" accept=".dd2vtt" ref="mapUploader">
</form>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
<div class="horizontal-button">
<IconButton icon="icons/iconoir/regular/upload.svg" :action="UploadButton"></IconButton>
<IconButton icon="icons/iconoir/regular/empty-page.svg" :action="NewMapButton"></IconButton>
</div>
<MapList></MapList>
</div>
</template>
<style scoped>
#send-map-form {
display: none;
}
.window-wrapper {
display: flex;
align-items: center;
user-select: none;
}
.horizontal-button {
margin-top: 5px;
display: flex;
flex-direction: row;
user-select: none;
}
</style>

View File

@ -0,0 +1,38 @@
<script setup>
import WindowHandle from '@/views/partials/WindowHandle.vue';
import { onMounted, onUpdated, ref, watch } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
const props = defineProps(['data']);
const data = props.data;
const handle = ref(null);
let id = data.id;
onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {x: 200, y: 300});
ResetPosition(id, {x: 30, y: 300});
});
</script>
<template>
<div class="window-wrapper" :id="'window-wrapper-' + id">
<WindowHandle :window="id" ref="handle"></WindowHandle>
</div>
</template>
<style scoped>
.window-wrapper {
display: flex;
align-items: center;
user-select: none;
}
</style>

View File

@ -2,11 +2,10 @@ const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const MapSchema = new Schema({
name: {type: String, required: true},
data: { type: Object }, // Data del format dd2vtt
campaign: {type: mongoose.Types.ObjectId, ref: "Campaign"},
image: { type: String },
entities: { type: Object }
});
module.exports = mongoose.model('Entity', EntitySchema);
module.exports = mongoose.model('Map', MapSchema);

121
server/routes/map.js Normal file
View File

@ -0,0 +1,121 @@
const express = require('express');
const router = express.Router();
const passport = require('passport');
const rateLimitMiddleware = require("../config/rate-limiter");
const Campaign = require("../models/Campaign");
const CampaignUser = require("../models/CampaignUser");
const Map = require("../models/Map");
const fs = require('fs');
const upload = require("../config/storage");
router.post('/create-resource', upload.single("image"), passport.authenticate('jwt', {session: false}), (req, res) => {
const imageName = req.file.filename;
Campaign.findById(req.query.campaign).then((campaign) => {
CampaignUser.findOne({campaign, user: req.user}).then((data) => {
if(!data) {
res.json({status: "error", msg: "not-found"})
fs.unlink(imageName);
return;
}
if(data.is_dm){
res.json({
status: "ok",
data: imageName
});
return;
}
res.json({status: "error", msg: "not-dm"})
fs.unlink(imageName);
return;
}).catch((err) => res.json({status: "error", msg: "not-found"}));
}).catch((err) => res.json({status: "error", err}));
});
// rateLimitMiddleware?
router.post('/create', passport.authenticate('jwt', {session: false}), (req, res) => {
Campaign.findById(req.body.campaign).then((campaign) => {
CampaignUser.findOne({campaign, user: req.user}).then((data) => {
if(!data) {
res.json({status: "error", msg: "not-found"})
return;
}
if(data.is_dm){
let mapData = req.body.data;
if(mapData){
let map = new Map({
data: mapData,
campaign
});
map.save().then(map => {
res.json({
status: "ok",
data: map
});
return;
});
} else res.json({status: "error", msg: "args"})
return;
}
}).catch((err) => res.json({status: "error", msg: "not-found"}));
}).catch((err) => res.json({status: "error", err}));
});
router.post('/update', passport.authenticate('jwt', {session: false}), (req, res) => {
Campaign.findById(req.query.campaign).then((campaign) => {
CampaignUser.findOne({campaign, user: req.user}).then((data) => {
if(!data) {
res.json({status: "error", msg: "not-found"})
return;
}
if(data.is_dm){
console.log("Ab");
let mapData = req.body.data;
if(mapData){
console.log("Map data:");
console.log(mapData)
Map.updateOne({_id: req.query.map, campaign}, {data: mapData}).then(map => {
res.json({
status: "ok",
data: map
});
});
} else res.json({status: "error", msg: "args"})
return;
}
}).catch((err) => res.json({status: "error", msg: "not-found"}));
}).catch((err) => res.json({status: "error", err}));
});
router.get('/list', passport.authenticate('jwt', {session: false}), (req, res) => {
Campaign.findById(req.query.campaign).then((campaign) => {
CampaignUser.findOne({campaign, user: req.user}).then((data) => {
if(!data) {
res.json({status: "error", msg: "not-found"})
return;
}
Map.find({campaign}).then(data => {
res.json({status: "ok", data});
return;
}).catch(err => res.json({status: "error", msg: "internal"}));
}).catch((err) => res.json({status: "error", msg: "not-found"}));
}).catch((err) => res.json({status: "error", err}));
});
router.get('/get', passport.authenticate('jwt', {session: false}), (req, res) => {
});
router.delete('/delete', passport.authenticate('jwt', {session: false}), (req, res) => {
});
module.exports = router;

View File

@ -64,6 +64,7 @@ app.use(cors());
// Routes (/ només)
app.use('/user', require('./routes/user'));
app.use('/campaign', require('./routes/campaign'));
app.use('/maps', require('./routes/map'))
app.use('/public', express.static('uploads'));