Started inside ui

This commit is contained in:
BinarySandia04 2024-08-05 16:55:37 +02:00
parent 44771fc4af
commit 6f780382d0
22 changed files with 536 additions and 25 deletions

View File

@ -11,6 +11,7 @@
"@kangc/v-md-editor": "^2.3.17",
"axios": "^1.5.1",
"babel-plugin-prismjs": "^2.1.0",
"ef-infinite-canvas": "^0.6.6",
"form-data": "^4.0.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",
@ -5263,6 +5264,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/ef-infinite-canvas": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/ef-infinite-canvas/-/ef-infinite-canvas-0.6.6.tgz",
"integrity": "sha512-EB9hawOUrFJ0eJz3MSxRnLcTodoji99zZVQSdvCTHVq1/6LQF/BVI/L3K8+1VgPW/aTSOTJiOGzqrm+PpMx0EA==",
"license": "MIT"
},
"node_modules/electron-to-chromium": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.2.tgz",

View File

@ -12,6 +12,7 @@
"@kangc/v-md-editor": "^2.3.17",
"axios": "^1.5.1",
"babel-plugin-prismjs": "^2.1.0",
"ef-infinite-canvas": "^0.6.6",
"form-data": "^4.0.0",
"jquery": "^3.7.1",
"jquery-ui": "^1.13.3",

View File

@ -59,6 +59,8 @@
--color-background-softest: var(--c-white-softest);
--color-button-active: var(--c-white-muter);
--text-disabled: #7e7e7e;
--color-scrollbar: var(--c-white-muter);
--color-hover: var(--c-white-mute);
@ -92,6 +94,7 @@
--separator: var(--c-white-mute);
--chat-background: var(--c-blacker);
--text-disabled: #7e7e7e;
--color-hover: var()
}

View File

@ -60,6 +60,13 @@ a {
flex-grow: 1;
}
.btn-green {
background-color: var(--c-button-green);
}
.btn-green:hover {
background-color: var(--c-button-green-hover);
}
hr {
border: 0;
height: 1px;

View File

@ -35,6 +35,7 @@ VueMarkdownEditor.use(createKatexPlugin());
const app = createApp(App).use(VueMarkdownEditor);
app.config.globalProperties.emitter = emitter
app.config.globalProperties.rollWindows = {
login: reactive([]),
register: reactive([]),

View File

@ -5,17 +5,31 @@ import Api from '@/services/Api'
import { backendUrl } from './BackendURL';
import { GetUser } from './User';
let emitter;
function SetEmitter(newEmitter){
emitter = newEmitter
}
function DisplayToast(color, text, duration = 1000){
emitter.emit("toast", {color, text, duration});
}
export const socket = io(backendUrl)
let currentCampaign = null;
let currentPlayer = null;
const players = ref([]);
let GetPlayerList = () => { return players; };
let GetCampaign = () => { return currentCampaign; };
let GetClient = () => { return currentPlayer; };
socket.on('update-players', data => {
players.value = [];
Object.keys(data).forEach((key) => {
players.value.push(data[key]);
if(GetUser()._id == data[key].user._id) currentPlayer = data[key];
});
})
@ -34,10 +48,22 @@ function Disconnect(){
currentCampaign = null;
}
function GetPlayer(player_campaign){
let index = players.value.findIndex((p) => {return p._id == player_campaign});
if(index != -1) return players.value[index];
}
export {
SetEmitter,
DisplayToast,
DisplayCampaign,
ConnectToCampaign,
Disconnect,
GetCampaign,
GetClient,
GetPlayerList,
GetPlayer,
};

View File

@ -0,0 +1,20 @@
import { ref } from "vue";
const inGameRef = ref(false);
let InGameRef = () => inGameRef;
function LaunchGame(){
inGameRef.value = true;
console.log("jdksadjlo")
}
function ExitGame(){
inGameRef.value = false;
}
export {
LaunchGame,
ExitGame,
InGameRef
};

View File

@ -2,14 +2,22 @@
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'
import { CreateWindow } from '@/services/Windows'
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');
@ -20,6 +28,8 @@ onMounted(() => {
</script>
<template>
<Toast></Toast>
<GameManager></GameManager>
<WindowManager></WindowManager>
</template>

View File

@ -0,0 +1,39 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import { InGameRef } from '../../services/Game';
import IconButton from '../partials/game/IconButton.vue';
import { AddSound } from '../../services/Sound';
const game = ref(null);
const in_game = InGameRef();
watch(game, () => {
if(game) AddSound(game.value);
})
</script>
<template>
<div class="game-root" ref="game" v-if="in_game">
<div class="vertical-button">
<IconButton icon="icons/iconoir/regular/menu.svg"></IconButton>
<IconButton icon="icons/iconoir/regular/cursor-pointer.svg"></IconButton>
<IconButton icon="icons/game-icons/000000/delapouite/rolling-dice-cup.svg"></IconButton>
</div>
<div class="horizontal-button">
</div>
</div>
</template>
<style scoped lang="scss">
.vertical-button {
position: absolute;
top: 10px;
left: 10px;
display: flex;
flex-direction: column;
}
</style>

View File

@ -2,38 +2,60 @@
import { onMounted, onUpdated, ref } from 'vue';
import Api from '@/services/Api.js'
import { backendUrl } from '../../services/BackendURL';
import { GetClient, GetPlayer, GetPlayerList } from '../../services/Dragonroll';
import { GetUser } from '../../services/User';
const props = defineProps(['player']);
const player = props.player;
let player = props.player;
const playerName = ref("");
const isDm = ref(false);
const avatar = ref(null);
const container = ref(null);
const status = ref("");
function retrieveAvatar(){
let userAvatarDisplay = avatar.value;
Api().get('/user/retrieve-avatar?username=' + player.data.username).then((response) => {
Api().get('/user/retrieve-avatar?username=' + player.user.username).then((response) => {
if(response.data.image) userAvatarDisplay.src = backendUrl + "public/" + response.data.image;
}).catch((err) => console.log("Internal error"));
}
onMounted(() => {
playerName.value = player.data.username;
playerName.value = player.user.username;
if(player.is_dm) isDm.value = true;
if(player.online) status.value = "online";
else status.value = "";
retrieveAvatar();
if(player.user._id == GetUser()._id){
container.value.classList.add("current-player");
}
});
onUpdated(() => {
player = GetPlayer(player._id);
playerName.value = player.user.username;
if(player.is_dm) isDm.value = true;
if(player.online) status.value = "online";
else status.value = "";
});
</script>
<template>
<div class="main-player-container">
<div class="main-player-container" ref="container">
<div class="main-player-container-inner">
<img class="user-icon" src="img/def-avatar.jpg" ref="avatar" draggable="false">
<div class="main-user-info">
<b>{{ playerName }}</b><span class="dm" v-if="isDm"> DM</span><br>Miauler
<b>{{ playerName }}</b><span class="dm" v-if="isDm"> DM</span>
<br>
<span class="offline-indicator" v-if="status == ''">Offline</span>
<span class="online-indicator" v-if="status == 'online'">Online</span>
</div>
<div class="main-user-actions">
@ -45,6 +67,38 @@ onMounted(() => {
<style scoped lang="scss">
.current-player {
background-color: var(--color-background-softer);
}
.offline-indicator {
&:before {
content: '';
display: inline-block;
width: 9px;
height: 9px;
background-color: var(--text-disabled);
border-radius: 8px;
margin-bottom: 1px;
margin-right: 5px;
}
color: var(--text-disabled);
}
.online-indicator {
&:before {
content: '';
display: inline-block;
width: 9px;
height: 9px;
background-color: var(--c-text-green);
border-radius: 8px;
margin-bottom: 1px;
margin-right: 5px;
}
color: var(--c-text-green);
}
#send-avatar-form {
display: none;
}

View File

@ -14,11 +14,14 @@ const players = GetPlayerList();
onMounted(() => {
});
</script>
<template>
<PlayerEntry v-for="player in players" :key="player.data._id" :player="player" class="player-list"></PlayerEntry>
<div class="player-list">
<PlayerEntry v-for="player in players" :key="player._id" :player="player"></PlayerEntry>
</div>
</template>

View File

@ -0,0 +1,104 @@
<script setup>
import { ref } from 'vue';
import useEmitter from '@/services/Emitter';
const emitter = useEmitter();
const text = ref("");
const toast = ref(null);
emitter.on('toast', data => {
text.value = data.text;
toast.value.classList.add(data.color);
toast.value.classList.add("show");
setTimeout(() => {
toast.value.classList.add("sliding");
setTimeout(() => {
toast.value.style = {};
toast.value.classList.remove("show");
toast.value.classList.remove("sliding");
toast.value.classList.remove(data.color);
}, 400);
}, data.duration);
});
</script>
<template>
<div class="toast" ref="toast">
<div class="toast-container">{{ text }}</div>
</div>
</template>
<style scoped lang="scss">
.toast-container {
height: 100%;
background-color: var(--color-background-soft);
padding: 10px;
margin-left: 5px;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
transform: translate(2px,0px)
}
.toast {
position: absolute;
display: none;
top: 10px;
left: 50%;
transform: translate(-50%, 0);
min-width: 400px;
min-height: 40px;
border-radius: 6px;
text-align: center;
animation: slide-in 0.4s ease-in-out;
@keyframes slide-in {
0% {
transform: translate(-50%,-50px);
opacity: 0;
}
100% {
opacity: 1;
}
}
&.sliding {
@keyframes slide-out {
0% {
opacity: 1;
}
100% {
transform: translate(-50%,-50px);
opacity: 0;
}
}
animation: slide-out .4s ease-in-out forwards;
}
&.show {
display: block;
}
/* Colors!!!! */
&.red {
background-color: rgb(243, 68, 68);
}
&.green {
background-color: rgb(92, 199, 92);
}
&.aqua {
background-color: rgb(113, 250, 250);
}
}
</style>

View File

@ -0,0 +1,21 @@
<script setup>
import { onMounted, onUpdated, ref } from 'vue';
const props = defineProps(['book']);
const book = props.book;
onMounted(() => {
});
</script>
<template>
<div class="book-item">A</div>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,34 @@
<script setup>
import { onMounted, onUpdated, ref } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import BookItem from './BookItem.vue';
/*
const props = defineProps(['campaign']);
const players = GetPlayerList();
*/
const props = defineProps(['books']);
const books = props.books;
onMounted(() => {
});
</script>
<template>
<div class="book-list">
<BookItem v-for="book in books" :key="book._id" :book="book"></BookItem>
</div>
</template>
<style scoped lang="scss">
.book-list {
height: 100%;
background-color: var(--color-background);
}
</style>

View File

@ -0,0 +1,37 @@
<script setup>
const props = defineProps(['icon']);
let icon = props.icon;
</script>
<template>
<div class="icon-button sound-click">
<img class="icon" :src="icon">
</div>
</template>
<style scoped lang="scss">
.icon-button {
height: 32px;
width: 32px;
background-color: var(--color-background-soft);
border-radius: 6px;
display: flex;
justify-content: center;
align-items: center;
margin: 2px;
transition: .3s background-color;
}
.icon-button:hover {
background-color: var(--color-background-softer);
}
.icon {
height: 24px;
width: 24px;
}
</style>

View File

@ -6,12 +6,13 @@ import SuccessMessage from '@/views/others/SuccessMessage.vue'
import { onMounted, onUpdated, ref } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import { SetUser } from '@/services/User'
import { SetUser, GetUser } from '@/services/User'
import Api from '@/services/Api.js'
import WindowHandle from '@/views/partials/WindowHandle.vue';
import { ClearWindows, CreateWindow } from '../../services/Windows';
import { DisplayToast } from '../../services/Dragonroll';
const handle = ref(null);
@ -46,7 +47,6 @@ function login(){
errorMessage.value = "";
SetUser(data.token);
console.log("Logged successfully");
ShowMainMenu();
}
}).catch((error) => {
@ -68,6 +68,7 @@ function ShowRegister(){
function ShowMainMenu(){
ClearWindows({type: "login"});
CreateWindow('main_menu');
DisplayToast('green', 'Logged in successfully as ' + GetUser().username + '!', 3000)
}
</script>

View File

@ -10,6 +10,7 @@ import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Win
import Api from '@/services/Api.js'
import { ClearWindows, CreateWindow } from '../../services/Windows';
import { DisplayToast } from '../../services/Dragonroll';
const email = ref("");
const name = ref("");
@ -56,7 +57,7 @@ function register(){
} else {
errorMessage.value = "";
console.log("Logged successfully");
ShowLogin("Account created successfully");
DisplayToast('green', 'Account created successfully, now log in!', 3000);
}
}).catch((error) => {
console.log(error);

View File

@ -66,7 +66,7 @@ function RefreshCampaigns(){
<div class="window-second-header">
<h2>Other campaigns</h2>
<div class="campaign-list">
<CampaignEntry v-for="camp in otherCampaigns" :key="camp._id" :data="camp"></CampaignEntry>
</div>
</div>

View File

@ -4,6 +4,10 @@ import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Win
import WindowHandle from '@/views/partials/WindowHandle.vue';
import PlayerList from '../../partials/PlayerList.vue';
import { DisplayToast, GetCampaign, GetClient } from '../../../services/Dragonroll';
import CampaignBookList from '../../partials/books/CampaignBookList.vue';
import { ClearWindow } from '../../../services/Windows';
import { LaunchGame } from '../../../services/Game';
const handle = ref(null);
@ -15,7 +19,20 @@ onMounted(() => {
SetupHandle(id, handle);
SetSize(id, {x: 1200, y: 750});
ResetPosition(id, "center");
console.log(data);
});
function CopyCode(){
navigator.clipboard.writeText(GetCampaign().invite_code);
DisplayToast('aqua', "Copied invite code successfully!", 1000);
}
function Launch(){
ClearWindow('campaign_preview');
LaunchGame();
}
</script>
@ -30,8 +47,22 @@ onMounted(() => {
<div class="campaign-preview-column left">
<h2>Players</h2>
<PlayerList :campaign="data.campaign"></PlayerList>
<div class="buttons-row">
<button class="btn-primary button-row sound-click" v-on:click.prevent="CopyCode">Copy invite code</button>
</div>
</div>
<div class="campaign-preview-column center">
<h1 class="campaign-title">{{ data.campaign.name }}</h1>
<div class="campaign-main-container">
<div class="campaign-main-container-scroll">
<div class="">Dnd 5e</div>
<h2>Books</h2>
<CampaignBookList class="small-book-list"></CampaignBookList>
</div>
</div>
<div class="buttons-row">
<button class="btn-primary button-row sound-click btn-green" v-on:click.prevent="Launch">Launch game</button>
</div>
</div>
<div class="campaign-preview-column right">
<h2>Chat</h2>
@ -43,12 +74,47 @@ onMounted(() => {
<style scoped lang="scss">
.small-book-list {
height: 400px;
margin: 20px;
overflow: auto;
}
.campaign-main-container-scroll {
overflow-y: scroll;
height: 100%;
max-height: 520px;
}
.campaign-main-container {
height: 100%;
h2 {
text-align: left;
margin-left: 20px;
font-size: 28px;
}
overflow: auto;
}
.campaign-title {
font-weight: normal;
text-align: left;
padding: 20px;
margin-bottom: 30px;
}
.button-row {
margin-bottom: 10px;
}
.campaign-preview-container {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 3fr 5fr 5fr;
grid-template-columns: 3fr 5fr 4fr;
}
.campaign-preview-column {
@ -61,6 +127,8 @@ onMounted(() => {
&.center {
background-color: var(--color-background-semisoft);
display: flex;
}
&.right {

View File

@ -3,6 +3,9 @@ import { onMounted, onUpdated, ref } from 'vue';
import { SetupHandle, SetSize, SetPosition, ResetPosition } from '@/services/Windows';
import WindowHandle from '@/views/partials/WindowHandle.vue';
import { DisplayToast } from '../../../services/Dragonroll';
import { ClearWindow } from '../../../services/Windows';
import Api from '@/services/Api'
const handle = ref(null);
@ -17,6 +20,26 @@ onMounted(() => {
SetSize(id, {x: 300, y: 150});
ResetPosition(id, "center");
});
function JoinCampaign(){
let invite_code = code.value;
Api().post('/campaign/join', {
invite_code
}).then(response => {
if(response.data.status == "ok"){
DisplayToast('green', "Successfully joined the campaign!", 2000);
let campaign = response.data.campaign;
ConnectToCampaign(campaign);
DisplayCampaign(campaign);
} else if(response.data.msg == "already"){
DisplayToast('red', "You are already in that campaign!", 2000);
} else {
DisplayToast('red', "Error joining this campaign (maybe the code is not valid?)", 2000);
}
}).catch((err) => console.log(err));
}
</script>
@ -30,7 +53,7 @@ onMounted(() => {
<input id="username-field" type="text" placeholder="Enter campaign code..." name="code" v-model="code" autocomplete="off" >
</div>
<div class="form-field">
<button class="btn-primary sound-click">Join</button>
<button class="btn-primary sound-click" v-on:click.prevent="JoinCampaign">Join</button>
</div>
</form>
</div>

View File

@ -4,31 +4,44 @@ const FilterUser = require('../utils/filters');
let sessions = {};
async function GetOfflinePlayers(campaign){
let players = await CampaignUser.find({campaign}).populate('user').exec();
let finalPlayers = [];
console.log(players)
// TODO: Filter
players.forEach(player => finalPlayers.push(FilterUser(player)));
return finalPlayers;
}
function SetPlayerProperty(campaign, player_id, key, value){
objIndex = sessions[campaign].players.findIndex(player => player.user._id.toString() == player_id);
if(objIndex != -1){
sessions[campaign].players[objIndex][key] = value;
}
}
module.exports = io => {
io.on('connection', (socket) => {
socket.on('enter', (user, campaignId) => {
User.findOne(user).then(user => {
if(user){
socket.user = user;
CampaignUser.findOne({campaign: campaignId, user}).then(campaignUser => {
CampaignUser.findOne({campaign: campaignId, user}).then(async campaignUser => {
if(campaignUser){
socket.join(campaignId);
socket.campaign = campaignId;
if(!sessions[campaignId]) sessions[campaignId] = {
players: {}
players: await GetOfflinePlayers(campaignId)
};
sessions[campaignId].players[socket.user._id] = {
online: true,
is_dm: campaignUser.is_dm,
data: FilterUser(socket.user)
};
console.log(socket.user.username + " ha entrado!");
SetPlayerProperty(campaignId, socket.user._id, "online", true);
io.to(socket.campaign).emit('update-players', sessions[campaignId].players)
console.log(JSON.stringify(sessions, null, 4));
// console.log(JSON.stringify(sessions[campaignId], null, 4));
}
});
}
@ -36,9 +49,9 @@ module.exports = io => {
});
socket.on('exit', () => {
io.to(socket.campaign).emit('update-players', sessions[campaignId].players)
SetPlayerProperty(socket.campaign, socket.user._id, "online", false);
io.to(socket.campaign).emit('update-players', sessions[socket.campaign].players)
socket.leave(socket.campaign)
sessions[socket.campaign].players[socket.user._id].online = false;
console.log(socket.user.username + " ha salido!")
});

View File

@ -46,9 +46,45 @@ router.post('/create', passport.authenticate('jwt', {session: false}), rateLimit
}).catch((err) => {res.json({status: "error", msg: "internal"})});
});
router.post('/join', passport.authenticate('jwt', {session: false}), rateLimitMiddleware, (req, res) => {
let {
invite_code
} = req.body;
if(!(invite_code)){
res.json({
status: "error",
msg: "params"
});
return;
}
Campaign.findOne({invite_code}).then(campaign => {
if(campaign){
let campaignUser = new CampaignUser({
user: req.user,
campaign,
is_dm: false
});
campaignUser.save().then(campaignUser => {
res.json({status: "ok", campaign});
return;
});
} else {
res.json({
status: "error",
msg: "not valid"
});
return;
}
}).catch(err => res.json({status: "error", msg: "internal"}))
});
router.get('/list', passport.authenticate('jwt', {session: false}), (req, res) => {
CampaignUser.find({user: req.user}).populate("campaign").then((data) => {
res.json(data);
console.log(data);
return;
}).catch((err) => res.json({status: "error", msg: "internal"}));
});
@ -56,6 +92,8 @@ router.get('/list', passport.authenticate('jwt', {session: false}), (req, res) =
router.get('/players', passport.authenticate('jwt', {session: false}), (req, res) => {
Campaign.findById(req.query.campaign).then((campaign) => {
CampaignUser.find({campaign}).populate('user').then((data) => {
console.log("djskajdk")
console.log(data);
res.json(data);
return;
}).catch((err) => res.json({status: "error", msg: "internal"}));