diff --git a/client/public/modules/dnd-5e/icon.png b/client/public/modules/dnd-5e/icon.png
new file mode 100644
index 00000000..ddbadbb0
Binary files /dev/null and b/client/public/modules/dnd-5e/icon.png differ
diff --git a/client/public/modules/dnd-5e/module.json b/client/public/modules/dnd-5e/module.json
new file mode 100644
index 00000000..d49c047e
--- /dev/null
+++ b/client/public/modules/dnd-5e/module.json
@@ -0,0 +1,12 @@
+{
+ "id": "dnd-5e",
+ "title": "Dungeons & Dragons 5e",
+ "description": "Dungeons & Dragons Fifth edition game system support",
+ "authors": [
+ {
+ "name": "Aran Roig"
+ }
+ ],
+ "version": "1.0.0",
+ "color": "#e92026"
+}
\ No newline at end of file
diff --git a/client/src/App.vue b/client/src/App.vue
index f0fbefbb..7b56d061 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -6,10 +6,43 @@ import { RouterLink, RouterView } from 'vue-router'
import { GetUser, UserStatus, LoadUser } from '@/services/User.js'
import { IsAdmin } from './services/User'
+import useEmitter from '@/services/Emitter';
+const emitter = useEmitter();
+
+import { DisplayToast, SetEmitter } from './services/Dragonroll'
+import { ImportModule, GetModulesToLoad } from './services/Modules'
+import { CreateWindow } from './services/Windows';
+
console.clear();
console.log("%cLoaded!!!", "color: #22ff22; font-size: 24px");
LoadUser();
+SetEmitter(emitter);
+onMounted(() => {
+ async function preloadModules(){
+ const modules = GetModulesToLoad();
+ let moduleLoads = [];
+
+ modules.forEach(moduleName => {
+ moduleLoads.push(ImportModule(moduleName));
+ });
+
+ await Promise.all(moduleLoads);
+ DisplayToast('aqua', 'All modules loaded successfully');
+
+ if(GetUser()){
+ CreateWindow('main_menu')
+ // CreateWindow('test');
+ DisplayToast('green', 'Logged in successfully as ' + GetUser().username + '!', 3000)
+ return;
+ }
+ CreateWindow('login');
+ }
+
+ preloadModules();
+})
+
+
diff --git a/client/src/assets/main.css b/client/src/assets/main.css
index bb234972..369c7806 100644
--- a/client/src/assets/main.css
+++ b/client/src/assets/main.css
@@ -164,4 +164,4 @@ button:active {
.param-value {
margin-left: auto;
-}
\ No newline at end of file
+}
diff --git a/client/src/main.js b/client/src/main.js
index 3bd4ad72..9f119bd9 100644
--- a/client/src/main.js
+++ b/client/src/main.js
@@ -27,14 +27,11 @@ import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-bash';
-
VueMarkdownEditor.lang.use('es-Es', esEs);
VueMarkdownEditor.use(vuepressTheme, { Prism });
VueMarkdownEditor.use(createKatexPlugin());
const app = createApp(App).use(VueMarkdownEditor);
-
-
app.config.globalProperties.emitter = emitter
app.config.globalProperties.rollWindows = {
login: reactive([]),
@@ -46,4 +43,5 @@ app.config.globalProperties.rollWindows = {
app.use(router)
-app.mount('#app')
\ No newline at end of file
+app.mount('#app')
+
diff --git a/client/src/services/Dragonroll.js b/client/src/services/Dragonroll.js
index 4ff24302..dbf4d68b 100644
--- a/client/src/services/Dragonroll.js
+++ b/client/src/services/Dragonroll.js
@@ -5,6 +5,7 @@ import Api from '@/services/Api'
import { backendUrl } from './BackendURL';
import { GetUser } from './User';
import { ExitGame } from './Game';
+import { GetModule } from './Modules';
let emitter;
@@ -12,6 +13,8 @@ function SetEmitter(newEmitter){
emitter = newEmitter
}
+let GetEmitter = () => emitter;
+
function DisplayToast(color, text, duration = 1000){
emitter.emit("toast", {color, text, duration});
}
@@ -109,8 +112,13 @@ function GetPlayer(player_campaign){
if(index != -1) return players.value[index];
}
+function GetSystem(){
+ if(currentCampaign) return GetModule(currentCampaign.system)
+}
+
export {
SetEmitter,
+ GetEmitter,
DisplayToast,
@@ -122,6 +130,7 @@ export {
GetClient,
GetPlayerList,
GetPlayer,
+ GetSystem,
GetChatRef,
SendMessage
diff --git a/client/src/services/Modules.js b/client/src/services/Modules.js
new file mode 100644
index 00000000..31fb1500
--- /dev/null
+++ b/client/src/services/Modules.js
@@ -0,0 +1,32 @@
+let modulesToLoad = [
+ "dnd-5e"
+]
+let modules = [];
+
+async function GetJson(url){
+ let obj = await (await fetch(url)).json();
+ return obj;
+}
+
+async function ImportModule(moduleFolder) {
+ let moduleInfo = await GetJson('/modules/' + moduleFolder + '/module.json');
+ modules.push(moduleInfo);
+}
+
+let GetModules = () => modules;
+let GetModulesToLoad = () => modulesToLoad;
+
+function GetModule(id){
+ let module = null;
+ modules.forEach(mod => {
+ if(mod.id == id) module = mod;
+ })
+ return module;
+}
+
+export {
+ ImportModule,
+ GetModules,
+ GetModule,
+ GetModulesToLoad,
+}
\ No newline at end of file
diff --git a/client/src/services/Windows.js b/client/src/services/Windows.js
index 2c486649..c289cab6 100644
--- a/client/src/services/Windows.js
+++ b/client/src/services/Windows.js
@@ -4,6 +4,11 @@ import { Disconnect } from './Dragonroll';
const windows = ref([])
const defValues = {
+ 'test': {
+ id: "example",
+ title: "Example",
+ close: true
+ },
'login': {
id: 'login',
title: 'Login',
@@ -74,6 +79,11 @@ const defValues = {
id: 'environment',
title: 'Edit environment',
close: true
+ },
+ 'system_selector': {
+ id: 'system-selector',
+ title: "Select a game system",
+ close: true
}
}
@@ -214,8 +224,8 @@ function ClearAll(){
}
function ClearWindows(data){
- for (let i = 0; i < windows[data.type].value.length; i++) {
- ClearWindow(windows[data.type].value[i].id);
+ for (let i = 0; i < windows.value.length; i++) {
+ ClearWindow(windows.value[i].id);
}
// reload.value += 1;
}
diff --git a/client/src/views/HomeView.vue b/client/src/views/HomeView.vue
index f1ab5a3d..4a9ef0fa 100644
--- a/client/src/views/HomeView.vue
+++ b/client/src/views/HomeView.vue
@@ -2,9 +2,6 @@
import { onMounted } from 'vue';
import { RouterLink, RouterView } from 'vue-router'
-import useEmitter from '@/services/Emitter';
-const emitter = useEmitter();
-
import WindowManager from '@/views/managers/WindowManager.vue'
import { GetUser } from '@/services/User'
@@ -13,17 +10,6 @@ import Toast from './partials/Toast.vue';
import { DisplayToast, SetEmitter } from '../services/Dragonroll';
import GameManager from './managers/GameManager.vue';
-onMounted(() => {
- SetEmitter(emitter);
- if(GetUser()){
- CreateWindow('main_menu')
- DisplayToast('green', 'Logged in successfully as ' + GetUser().username + '!', 3000)
- return;
- }
- CreateWindow('login');
-}
-);
-
diff --git a/client/src/views/managers/WindowManager.vue b/client/src/views/managers/WindowManager.vue
index 9721f241..683654d5 100644
--- a/client/src/views/managers/WindowManager.vue
+++ b/client/src/views/managers/WindowManager.vue
@@ -19,12 +19,14 @@ import ChatWindow from '../windows/game/ChatWindow.vue'
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'
// Gestionem ventanas
const reload = ReloadRef();
const windows = Windows();
const WindowMap = {
+ test: ExampleWindow,
login: LoginWindow,
main_menu: MainMenuWindow,
register: RegisterWindow,
@@ -37,7 +39,8 @@ const WindowMap = {
chat: ChatWindow,
dice_menu: DiceWindow,
map_buttons: MapButtons,
- environment: EnvironmentWindow
+ environment: EnvironmentWindow,
+ system_selector: SystemSelectorWindow
};
diff --git a/client/src/views/partials/GameSystem.vue b/client/src/views/partials/GameSystem.vue
new file mode 100644
index 00000000..6a5cc3c9
--- /dev/null
+++ b/client/src/views/partials/GameSystem.vue
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/views/partials/SystemSelector.vue b/client/src/views/partials/SystemSelector.vue
new file mode 100644
index 00000000..62df0d6d
--- /dev/null
+++ b/client/src/views/partials/SystemSelector.vue
@@ -0,0 +1,75 @@
+
+
+
+
+
+
No game system selected
+
+
+ {{ systemTitle }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/views/partials/Toast.vue b/client/src/views/partials/Toast.vue
index 2430b81b..dfc2a54f 100644
--- a/client/src/views/partials/Toast.vue
+++ b/client/src/views/partials/Toast.vue
@@ -8,7 +8,16 @@ const emitter = useEmitter();
const text = ref("");
const toast = ref(null);
-emitter.on('toast', data => {
+let toastQueue = [];
+let displayingToast = false;
+
+function DisplayToast(){
+ if(displayingToast) return;
+ if(toastQueue.length == 0) return;
+
+ displayingToast = true;
+ let data = toastQueue.pop();
+
text.value = data.text;
toast.value.classList.add(data.color);
@@ -20,8 +29,15 @@ emitter.on('toast', data => {
toast.value.classList.remove("show");
toast.value.classList.remove("sliding");
toast.value.classList.remove(data.color);
+ displayingToast = false;
+ DisplayToast();
}, 400);
}, data.duration);
+}
+
+emitter.on('toast', data => {
+ toastQueue.push(data);
+ DisplayToast();
});
diff --git a/client/src/views/partials/books/CampaignBookList.vue b/client/src/views/partials/books/CampaignBookList.vue
index b809517f..c22c0f8e 100644
--- a/client/src/views/partials/books/CampaignBookList.vue
+++ b/client/src/views/partials/books/CampaignBookList.vue
@@ -28,7 +28,6 @@ onMounted(() => {
\ No newline at end of file
diff --git a/client/src/views/windows/ExampleWindow.vue b/client/src/views/windows/ExampleWindow.vue
index c9f10d38..151ddc10 100644
--- a/client/src/views/windows/ExampleWindow.vue
+++ b/client/src/views/windows/ExampleWindow.vue
@@ -1,5 +1,5 @@
@@ -40,10 +64,15 @@ function NewCampaign(){
+
+
+
+
+ {{ errorMessage }}
diff --git a/client/src/views/windows/campaigns/SystemSelectorWindow.vue b/client/src/views/windows/campaigns/SystemSelectorWindow.vue
new file mode 100644
index 00000000..a6d17458
--- /dev/null
+++ b/client/src/views/windows/campaigns/SystemSelectorWindow.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/vite.config.js b/client/vite.config.js
index 98f28cda..8ed36b09 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -11,6 +11,7 @@ export default defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
+ 'vue': 'vue/dist/vue.esm-bundler.js'
}
},
proxy: {
diff --git a/server/models/Book.js b/server/models/Book.js
new file mode 100644
index 00000000..282a9a66
--- /dev/null
+++ b/server/models/Book.js
@@ -0,0 +1,12 @@
+const mongoose = require("mongoose");
+const Schema = mongoose.Schema;
+
+const BookSchema = new Schema({
+ title: {type: String, required: true},
+ authors: { type: [String] },
+ system: {type: String, required: true},
+ image: { type: String },
+ data: { type: Object },
+});
+
+module.exports = mongoose.model('Book', BookSchema);
\ No newline at end of file
diff --git a/server/models/Campaign.js b/server/models/Campaign.js
index 179feb26..919d8fe5 100644
--- a/server/models/Campaign.js
+++ b/server/models/Campaign.js
@@ -3,6 +3,7 @@ const Schema = mongoose.Schema;
const CampaignSchema = new Schema({
name: {type: String, required: true},
+ system: {type: String, required: true},
creation_date: { type: Date, default: Date.now},
last_opened: { type: Date, default: Date.now},
invite_code: { type: String, unique: true },
diff --git a/server/models/Character.js b/server/models/Character.js
new file mode 100644
index 00000000..396fd4d4
--- /dev/null
+++ b/server/models/Character.js
@@ -0,0 +1,11 @@
+const mongoose = require("mongoose");
+const Schema = mongoose.Schema;
+
+const CharacterSchema = new Schema({
+ name: {type: String, required: true},
+ data: { type: Object },
+ campaign_user: {type: mongoose.Types.ObjectId, ref: "CampaignUser"},
+ image: { type: String },
+});
+
+module.exports = mongoose.model('Character', CharacterSchema);
\ No newline at end of file
diff --git a/server/models/Entity.js b/server/models/Entity.js
new file mode 100644
index 00000000..1f6cb97d
--- /dev/null
+++ b/server/models/Entity.js
@@ -0,0 +1,11 @@
+const mongoose = require("mongoose");
+const Schema = mongoose.Schema;
+
+const EntitySchema = new Schema({
+ name: {type: String, required: true},
+ data: { type: Object },
+ campaign: {type: mongoose.Types.ObjectId, ref: "Campaign"},
+ image: { type: String },
+});
+
+module.exports = mongoose.model('Entity', EntitySchema);
\ No newline at end of file
diff --git a/server/models/Map.js b/server/models/Map.js
new file mode 100644
index 00000000..551cf8a4
--- /dev/null
+++ b/server/models/Map.js
@@ -0,0 +1,12 @@
+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);
\ No newline at end of file
diff --git a/server/routes/campaign.js b/server/routes/campaign.js
index 45687966..ccd59baa 100644
--- a/server/routes/campaign.js
+++ b/server/routes/campaign.js
@@ -16,10 +16,11 @@ router.post('/register', passport.authenticate('jwt', {session: false}), rateLim
router.post('/create', passport.authenticate('jwt', {session: false}), rateLimitMiddleware, (req, res) => {
let {
- name
+ name,
+ system
} = req.body;
- if(!(name)){
+ if(!(name && system)){
res.json({
status: "error",
msg: "params"
@@ -28,7 +29,7 @@ router.post('/create', passport.authenticate('jwt', {session: false}), rateLimit
}
// Create the campaign
- let campaign = new Campaign({name});
+ let campaign = new Campaign({name, system});
campaign.invite_code = Campaign.generateInvite();
campaign.save().then(campaign => {