Compare commits

..

1 commit

Author SHA1 Message Date
9969561af1 Add Lobby (#43)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2019-10-16 08:31:03 +00:00
16 changed files with 599 additions and 421 deletions

View file

@ -1,18 +1,22 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css"> <link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
<title>mcgvue</title> <title>MLPCARDGAME</title>
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>We're sorry but mcgvue doesn't work properly without JavaScript enabled. Please enable it to
continue.</strong>
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View file

@ -1,86 +0,0 @@
<template>
<section
:class="zoneClass"
@dragenter.prevent="dragenter"
@dragleave.prevent="dragleave"
>
<CardImage
class="ccgcard"
v-for="(card, i) in cards"
:key="i + card.ID"
:id="card.ID"
/>
</section>
</template>
<style lang="scss" scoped>
.zone {
border: 2px solid rgba(255, 255, 255, 0.2);
margin: 2px;
padding: 4px;
border-radius: 5px;
display: flex;
justify-content: center;
}
.ccgcard {
cursor: grab;
transition: all 100ms;
max-width: none;
width: auto;
max-height: 100%;
margin: 0 10px;
&:active {
cursor: grabbing;
}
}
.dragging {
background: rgba(255, 255, 255, 0.3);
}
</style>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { cardImageURL, Card } from "@/mlpccg";
import CardImage from "@/components/Cards/CardImage.vue";
@Component({
components: {
CardImage
}
})
export default class Zone extends Vue {
@Prop({
default: []
})
private cards!: Card[];
private dragging!: boolean;
private data() {
return {
dragging: false
};
}
private imageURL(id: string) {
return cardImageURL(id);
}
private dragenter(e: DragEvent) {
this.dragging = true;
}
private dragleave(e: DragEvent) {
this.dragging = false;
}
private get zoneClass() {
return {
zone: true,
dragging: this.dragging
};
}
}
</script>

View file

@ -19,6 +19,23 @@ export const allSets = [
"Promo" "Promo"
]; ];
export const setNames = {
PR: "Premiere",
CN: "Canterlot Nights",
RR: "Rock and Rave",
CS: "Celestial Solstice",
CG: "Crystal Games",
AD: "Absolute Discord",
EO: "Equestrian Odysseys",
HM: "High Magic",
MT: "Marks In Time",
DE: "Defenders of Equestria",
SB: "Seaquestria and Beyond",
FF: "Friends Forever",
GF: "Promotional",
ST: "Sands of Time"
};
export async function loadSets() { export async function loadSets() {
if (Database == null) { if (Database == null) {
throw new Error("Database was not initialized, init with 'initDB()'"); throw new Error("Database was not initialized, init with 'initDB()'");

View file

@ -1,10 +1,7 @@
import {
PeerMetadata,
NetworkMessage,
PasswordResponse,
RoomInfo
} from "./types";
import EventEmitter from "eventemitter3"; import EventEmitter from "eventemitter3";
import Vue from "vue";
import { NetworkMessage, PasswordResponse, PeerMetadata, RoomInfo } from "./types";
export abstract class Client extends EventEmitter { export abstract class Client extends EventEmitter {
public metadata: PeerMetadata; public metadata: PeerMetadata;
@ -23,13 +20,19 @@ export abstract class Client extends EventEmitter {
case "room-info": case "room-info":
this.roomInfo = data.room; this.roomInfo = data.room;
this.players = data.players; this.players = data.players;
this.emit("handshake");
break; break;
// Someone changed name (or was forced to) // Someone changed name (or was forced to)
case "rename": case "rename":
if (data.oldname == this.metadata.name) { if (data.oldname == this.metadata.name) {
// We got a name change! // We got a name change!
this.metadata.name = data.newname; this.metadata.name = data.newname;
} else { }
// Only mutate player list if we have one
// This is because rename messages can be received during the initial
// handshake, to signal a forced name change before joining.
if (this.players) {
let idx = this.players.indexOf(data.oldname); let idx = this.players.indexOf(data.oldname);
if (idx < 0) { if (idx < 0) {
// Weird // Weird
@ -39,7 +42,7 @@ export abstract class Client extends EventEmitter {
this.players.push(data.newname); this.players.push(data.newname);
break; break;
} }
this.players[idx] = data.newname; Vue.set(this.players, idx, data.newname);
} }
this.emit("rename", data.oldname, data.newname); this.emit("rename", data.oldname, data.newname);
break; break;

View file

@ -1,23 +1,24 @@
import EventEmitter from "eventemitter3";
import Peer, { DataConnection } from "peerjs"; import Peer, { DataConnection } from "peerjs";
import { LocalClient } from ".";
import { import {
RoomInfo, AckMessage,
PasswordRequest, ChatMessage,
Room,
ErrorMessage, ErrorMessage,
JoinMessage,
LeaveMessage,
NetworkMessage,
NetworkPlayer,
PasswordRequest,
PasswordResponse, PasswordResponse,
PeerMetadata, PeerMetadata,
JoinMessage,
RoomInfoMessage,
Player, Player,
NetworkMessage,
RenameMessage, RenameMessage,
LeaveMessage, Room,
NetworkPlayer, RoomInfo,
AckMessage, RoomInfoMessage,
ChatMessage
} from "./types"; } from "./types";
import { LocalClient } from ".";
import EventEmitter from "eventemitter3";
// Increment name, add number at the end if not present // Increment name, add number at the end if not present
// Examples: // Examples:
@ -34,6 +35,12 @@ function nextName(name: string): string {
return name.substr(0, i) + (Number(name.slice(i)) + 1); return name.substr(0, i) + (Number(name.slice(i)) + 1);
} }
function connectionOpen(conn: DataConnection): Promise<void> {
return new Promise(resolve => {
conn.on("open", () => resolve());
});
}
export class PeerServer extends EventEmitter { export class PeerServer extends EventEmitter {
protected peer: Peer; protected peer: Peer;
private room: Room; private room: Room;
@ -64,17 +71,21 @@ export class PeerServer extends EventEmitter {
// Setup peer // Setup peer
this.peer = customPeer ? customPeer : new Peer(); this.peer = customPeer ? customPeer : new Peer();
this.peer.on("open", function(id) { this.peer.on("open", id => {
console.info("Peer ID assigned: %s", id); console.info("Peer ID assigned: %s", id);
this.emit("open", id);
}); });
this.peer.on("connection", conn => { this.peer.on("connection", conn => {
this._connection(conn); this._connection(conn);
}); });
} }
private _connection(conn: DataConnection) { private async _connection(conn: DataConnection) {
const metadata = conn.metadata as PeerMetadata; const metadata = conn.metadata as PeerMetadata;
// Wait for connection to be open
await connectionOpen(conn);
let player: NetworkPlayer = { let player: NetworkPlayer = {
kind: "remote", kind: "remote",
name: metadata.name, name: metadata.name,
@ -140,6 +151,12 @@ export class PeerServer extends EventEmitter {
private addPlayer(player: NetworkPlayer) { private addPlayer(player: NetworkPlayer) {
const playerName = player.name; const playerName = player.name;
// Hacky: Give player list before this player was added, so join
// message doesn't mess things up later
const players = Object.keys(this.room.players);
// Add player to room
this.room.players[playerName] = player; this.room.players[playerName] = player;
// Start listening for new messages // Start listening for new messages
@ -148,6 +165,10 @@ export class PeerServer extends EventEmitter {
this._received.bind(this, this.room.players[playerName]) this._received.bind(this, this.room.players[playerName])
); );
player.conn.on("error", err => {
throw err;
});
// Send the player info about the room // Send the player info about the room
this.send<RoomInfoMessage>(player, { this.send<RoomInfoMessage>(player, {
kind: "room-info", kind: "room-info",
@ -155,7 +176,7 @@ export class PeerServer extends EventEmitter {
...this.room.info, ...this.room.info,
password: "" password: ""
}, },
players: Object.keys(this.room.players) players
}); });
// Notify other players // Notify other players
@ -172,6 +193,9 @@ export class PeerServer extends EventEmitter {
// Close connection with player // Close connection with player
player.conn.close(); player.conn.close();
// Remove player from player list
delete this.room.players[player.name];
// Notify other players // Notify other players
this.broadcast<LeaveMessage>({ this.broadcast<LeaveMessage>({
kind: "player-left", kind: "player-left",
@ -189,16 +213,30 @@ export class PeerServer extends EventEmitter {
this.broadcast(data); this.broadcast(data);
} else { } else {
// Player is telling someone specifically // Player is telling someone specifically
if (data.to in this.players) { if (!(data.to in this.players)) {
this.send<ChatMessage>(this.players[data.to], data);
} else {
this.send<ErrorMessage>(player, { this.send<ErrorMessage>(player, {
kind: "error", kind: "error",
error: `player not found: ${data.to}` error: `player not found: ${data.to}`
}); });
return;
} }
this.send<ChatMessage>(this.players[data.to], data);
} }
break; break;
// Player wants to change name
case "rename":
// Make sure new name is valid
data.oldname = player.name;
if (data.newname in this.players) {
this.send<ErrorMessage>(player, {
kind: "error",
error: "name not available"
});
return;
}
player.name = data.newname;
this.broadcast<RenameMessage>(data);
break;
// Player is leaving! // Player is leaving!
case "leave-req": case "leave-req":
// If we're leaving, end the server // If we're leaving, end the server

View file

@ -1,12 +1,11 @@
import DeckBuilder from "@/views/DeckBuilder.vue";
import DraftView from "@/views/Draft.vue";
import GameView from "@/views/Game.vue";
import Home from "@/views/Home.vue";
import Lobby from "@/views/Lobby.vue";
import SettingsView from "@/views/Settings.vue";
import Vue from "vue"; import Vue from "vue";
import Router from "vue-router"; import Router from "vue-router";
import Home from "@/views/Home.vue";
import DeckBuilder from "@/views/DeckBuilder.vue";
import GameView from "@/views/Game.vue";
import DraftView from "@/views/Draft.vue";
import Lobby from "@/views/Lobby.vue";
import RoomView from "@/views/Room.vue";
import SettingsView from "@/views/Settings.vue";
Vue.use(Router); Vue.use(Router);
@ -46,9 +45,12 @@ export default new Router({
} }
}, },
{ {
path: "/room", path: "/join/:id",
name: "room", name: "lobby-join",
component: RoomView component: Lobby,
meta: {
topnav: "Lobby"
}
}, },
{ {
path: "/settings", path: "/settings",

View file

@ -1,28 +1,58 @@
import { ActionTree } from "vuex"; import { ChatMessage, Client, LocalClient, NetworkMessage, PeerClient, PeerServer } from "@/network";
import { ActionTree, Commit } from "vuex";
import { AppState } from "../types"; import { AppState } from "../types";
import { NetworkState, StartServerOptions, ConnectOptions } from "./types"; import { ConnectOptions, NetworkState, StartServerOptions } from "./types";
import { PeerServer, LocalClient, PeerClient } from "@/network";
function bindClientEvents(commit: Commit, client: Client) {
client.on("handshake", () => {
commit("playerListChanged", client.players);
});
client.on("player-joined", () => commit("playerListChanged", client.players));
client.on("player-left", () => commit("playerListChanged", client.players));
client.on("rename", () => commit("playerListChanged", client.players));
}
const actions: ActionTree<NetworkState, AppState> = { const actions: ActionTree<NetworkState, AppState> = {
startServer({ commit }, options: StartServerOptions) { startServer({ commit }, options: StartServerOptions) {
const local = new LocalClient(options.playerInfo); const local = new LocalClient(options.playerInfo);
const server = new PeerServer(options.roomInfo, local, options._customPeer); const server = new PeerServer(options.roomInfo, local, options._customPeer);
server.once("open", id => {
commit("serverAssignedID", id);
});
bindClientEvents(commit, local);
commit("becomeServer", { local, server }); commit("becomeServer", { local, server });
}, },
connect({ commit }, options: ConnectOptions) { connect({ commit }, options: ConnectOptions) {
const client = new PeerClient(options.playerInfo, options._customPeer); const client = new PeerClient(options.playerInfo, options._customPeer);
commit("becomeClient", { peer: client }); commit("becomeClient", { peer: client, id: options.serverID });
client.on("connected", () => { client.on("connected", () => {
commit("connected"); commit("connectionStatusChanged", "connected");
}); });
client.on("disconnected", () => { client.on("disconnected", () => {
commit("disconnected"); commit("connectionStatusChanged", "disconnected");
}); });
client.on("error", err => { client.on("error", err => {
commit("connectionError", err); commit("connectionError", err);
}); });
bindClientEvents(commit, client);
client.connect(options.serverID); client.connect(options.serverID);
},
sendChatMessage({ commit, dispatch, getters }, message: ChatMessage) {
if (getters.connectionType == "none") {
throw new Error("not connected");
}
dispatch("sendMessage", message);
commit("receivedChatMessage", message);
},
sendMessage({ getters }, message: NetworkMessage) {
if (getters.connectionType == "none") {
throw new Error("not connected");
}
(getters.client as Client).send(message);
} }
}; };

View file

@ -1,4 +1,6 @@
import { Client } from "@/network";
import { GetterTree } from "vuex"; import { GetterTree } from "vuex";
import { AppState } from "../types"; import { AppState } from "../types";
import { NetworkState } from "./types"; import { NetworkState } from "./types";
@ -13,8 +15,45 @@ const getters: GetterTree<NetworkState, AppState> = {
return null; return null;
}, },
sessionID(state): string | null {
return state.serverID;
},
client(state): Client | null {
switch (state.peerType) {
case "server":
return state.local;
case "client":
return state.peer;
}
return null;
},
connectionType(state): "client" | "server" | "none" { connectionType(state): "client" | "server" | "none" {
return state.peerType; return state.peerType;
},
busy(state): boolean {
if (state.peerType == "client") {
if (state.connectionStatus == "connecting") {
return true;
}
}
return false;
},
inRoom(state): boolean {
if (state.peerType == "client") {
return state.connectionStatus == "connected";
}
if (state.peerType == "server") {
return true;
}
return false;
},
players(state): string[] {
return state.players;
} }
}; };

View file

@ -10,6 +10,12 @@ const namespaced = true;
export const state: NetworkState = { export const state: NetworkState = {
peerType: "none", peerType: "none",
connectionStatus: null,
peer: null,
server: null,
local: null,
serverID: null,
players: [],
chatLog: [] chatLog: []
}; };

View file

@ -1,37 +1,43 @@
import { ChatMessage, LocalClient, PeerClient, PeerServer } from "@/network";
import Vue from "vue";
import { MutationTree } from "vuex"; import { MutationTree } from "vuex";
import { NetworkState, ServerNetworkState, ClientNetworkState } from "./types";
import { LocalClient, PeerServer, PeerClient } from "@/network"; import { ClientNetworkState, ConnectionStatus, NetworkState, ServerNetworkState } from "./types";
const mutations: MutationTree<NetworkState> = { const mutations: MutationTree<NetworkState> = {
becomeServer(state, payload: { local: LocalClient; server: PeerServer }) { becomeServer(state, payload: { local: LocalClient; server: PeerServer }) {
state = { state.peerType = "server";
...state, state.players = [payload.local.name];
peerType: "server", (state as ServerNetworkState).local = payload.local;
local: payload.local, (state as ServerNetworkState).server = payload.server;
server: payload.server
};
}, },
becomeClient(state, payload: { peer: PeerClient }) { becomeClient(state, payload: { peer: PeerClient; id: string }) {
state = { state.peerType = "client";
...state, (state as ClientNetworkState).connectionStatus = "connecting";
connectionStatus: "connecting", (state as ClientNetworkState).peer = payload.peer;
peerType: "client", (state as ClientNetworkState).serverID = payload.id;
peer: payload.peer
};
}, },
connected(state) { connectionStatusChanged(state, status: ConnectionStatus) {
(state as ClientNetworkState).connectionStatus = "connected"; (state as ClientNetworkState).connectionStatus = status;
},
disconnected(state) {
(state as ClientNetworkState).connectionStatus = "disconnected";
}, },
connectionError(state, error) { connectionError(state, error) {
(state as ClientNetworkState).connectionStatus = "error"; (state as ClientNetworkState).connectionStatus = "error";
(state as ClientNetworkState).connectionError = error; (state as ClientNetworkState).connectionError = error;
},
receivedChatMessage(state, message: ChatMessage) {
state.chatLog.push(message);
},
serverAssignedID(state, id: string) {
state.serverID = id;
},
playerListChanged(state, players: string[]) {
Vue.set(state, "players", players);
} }
}; };

View file

@ -1,29 +1,30 @@
import { import { ChatMessage, LocalClient, PeerClient, PeerMetadata, PeerServer, RoomInfo } from "@/network";
PeerClient,
PeerServer,
LocalClient,
RoomInfo,
PeerMetadata
} from "@/network";
import Peer from "peerjs"; import Peer from "peerjs";
export interface ChatMessage { export type ConnectionStatus =
who: string; | "connecting"
to: string; | "connected"
message: string; | "disconnected"
} | "error";
export interface SharedNetworkState { export interface SharedNetworkState {
chatLog: ChatMessage[]; chatLog: ChatMessage[];
serverID: string | null;
players: string[];
} }
export interface NoNetworkState extends SharedNetworkState { export interface NoNetworkState extends SharedNetworkState {
peerType: "none"; peerType: "none";
connectionStatus: null;
connectionError?: Error;
peer: null;
server: null;
local: null;
} }
export interface ClientNetworkState extends SharedNetworkState { export interface ClientNetworkState extends SharedNetworkState {
peerType: "client"; peerType: "client";
connectionStatus: "connecting" | "connected" | "disconnected" | "error"; connectionStatus: ConnectionStatus;
connectionError?: Error; connectionError?: Error;
peer: PeerClient; peer: PeerClient;
} }

View file

@ -54,8 +54,6 @@ $border-opacity: 0.6;
.draftview { .draftview {
background: url("../assets/images/backgrounds/draftbg.webp") center; background: url("../assets/images/backgrounds/draftbg.webp") center;
background-repeat: no-repeat;
background-size: cover;
display: grid; display: grid;
height: 100vh; height: 100vh;
gap: 10px; gap: 10px;

View file

@ -1,235 +1,14 @@
<template> <template>
<section class="game"> <section class="game"></section>
<section class="topbar"></section>
<section class="board">
<Zone class="opp-zone" :cards="zones['opp-home']" id="opp-home" />
<section class="problems">
<section class="problem own-problem">
<Zone
class="opp-zone"
:cards="zones['own-problem-opp-zone']"
id="pown-opp-zone"
/>
<section class="problem-card opp-zone">
<CardImage
v-if="problems['own-problem']"
:id="problems['own-problem'].ID"
/>
</section>
<Zone
class="own-zone"
:cards="zones['own-problem-own-zone']"
id="pown-own-zone"
/>
</section>
<section class="problem opp-problem">
<Zone
class="opp-zone"
:cards="zones['opp-problem-opp-zone']"
id="popp-opp-zone"
/>
<section class="problem-card">
<CardImage
v-if="problems['opp-problem']"
:id="problems['opp-problem'].ID"
/>
</section>
<Zone
class="own-zone"
:cards="zones['opp-problem-own-zone']"
id="popp-own-zone"
/>
</section>
</section>
<Zone class="own-zone" :cards="zones['own-home']" id="own-home" />
</section>
<section class="hand">
<article
draggable="true"
v-for="(card, i) in hand"
:key="i + card.ID"
@dragstart="dragStartCard.bind(this, card)"
>
<CardImage :id="card.ID" />
</article>
</section>
</section>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped></style>
$zone-gap: 5px;
$card-gap: 5px;
$side-min-size: 250px;
$top-bar-height: 50px;
$hand-min-height: 100px;
$hand-max-height: 150px;
.game {
background: url("../assets/images/backgrounds/boardbg.webp") center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
display: grid;
gap: $zone-gap;
grid:
$top-bar-height 2fr minmax($hand-min-height, $hand-max-height) / minmax(
$side-min-size,
1fr
)
5fr minmax($side-min-size, 1fr);
}
.topbar {
grid-column: 1/4;
grid-row: 1;
background: linear-gradient(
to bottom,
rgba(0, 0, 30, 0.9),
rgba(0, 0, 50, 0.6)
);
}
.board {
grid-column: 2/3;
grid-row: 2;
display: grid;
grid: 1fr 2.5fr 1fr / 1fr;
max-height: 100vh;
.problems {
display: flex;
flex: 1;
flex-flow: row;
.problem {
display: grid;
grid: 1.5fr 0.2fr 1fr 0.2fr 1.5fr / 1fr 1.5fr 1fr;
flex: 1;
.own-zone {
grid-row: 4/6;
grid-column: 1/4;
z-index: 2;
}
.opp-zone {
grid-row: 1/3;
grid-column: 1/4;
z-index: 2;
}
.problem-card {
grid-row: 2/5;
grid-column: 2;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
img {
height: auto;
max-height: 100%;
}
}
}
}
}
.hand {
grid-column: 2;
grid-row: 3;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10px, max-content));
margin: 0;
padding: 0;
padding-right: 100px;
article {
margin: $card-gap;
margin-bottom: 0;
overflow-y: hidden;
cursor: grab;
transition: all 100ms;
min-width: 150px;
max-width: 200px;
&:hover {
margin-top: $card-gap - 30px;
}
&:active {
cursor: grabbing;
}
}
overflow: visible;
}
.opp-zone {
transform: rotate(180deg);
}
@media (max-width: 1200px) {
.game {
grid-template-columns: $side-min-size 5fr;
}
.hand {
grid-column: 1/3;
padding: 0 10px;
}
}
</style>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { Card, cardImageURL, getCards } from "@/mlpccg";
import Zone from "@/components/GameBoard/Zone.vue";
import CardImage from "@/components/Cards/CardImage.vue";
@Component({ @Component({
components: { components: {}
CardImage,
Zone
}
}) })
export default class GameView extends Vue { export default class GameView extends Vue {}
private hand!: Card[];
private zones!: Record<string, Card[]>;
private problems!: Record<string, Card | null>;
private data() {
return {
hand: [],
zones: {
"own-home": [],
"opp-home": [],
"own-problem-own-zone": [],
"own-problem-opp-zone": [],
"opp-problem-own-zone": [],
"opp-problem-opp-zone": []
},
problems: {
"own-problem": null,
"opp-problem": null
}
};
}
private async mounted() {
const cards = await getCards({ Sets: ["FF"] });
this.hand = cards.slice(0, 9);
this.zones["own-home"] = cards.slice(0, 3);
this.zones["own-problem-opp-zone"] = cards.slice(10, 11);
this.zones["opp-home"] = cards.slice(20, 23);
console.log(cards);
const problem = cards.find(c => c.ID == "ff130");
this.problems["own-problem"] = problem!;
this.problems["opp-problem"] = problem!;
}
private imageURL(id: string) {
return cardImageURL(id);
}
private dragStartCard(card: Card, e: DragEvent) {
if (e.dataTransfer) {
e.dataTransfer.setData("id", card.ID);
e.dataTransfer.dropEffect = "move";
e.dataTransfer.effectAllowed = "all";
}
}
}
</script> </script>

View file

@ -1,26 +1,374 @@
<template> <template>
<section class="lobby"> <section class="lobby">
<TopNav /> <TopNav />
<section v-if="!inRoom" class="body">
<section id="info">
<b-field label="Your name">
<b-input :disabled="busy" v-model="playerName"></b-input>
</b-field>
</section>
<section id="join">
<header>
<h1>Join someone's session</h1>
</header>
<b-field label="Session ID" class="only-full">
<b-input :disabled="busy" v-model="joinSessionID"></b-input>
</b-field>
<div class="center submit only-full">
<b-button
type="is-primary"
@click="join"
class="wide"
:disabled="busy || !canJoin"
>
Join
</b-button>
</div>
<div class="only-mobile">
<b-field class="full">
<b-input
:disabled="busy"
placeholder="Session ID"
v-model="joinSessionID"
></b-input>
<p class="control">
<b-button
type="is-primary"
@click="join"
:disabled="busy || !canJoin"
>
Join
</b-button>
</p>
</b-field>
</div>
</section>
<section id="host">
<header>
<h1>Host a new session</h1>
</header>
<div class="columns">
<div class="column">
<b-field label="Max players">
<b-numberinput
:disabled="busy"
controls-position="compact"
v-model="hostMaxPlayers"
min="2"
max="8"
></b-numberinput>
</b-field>
</div>
<div class="column">
<b-field label="Password">
<b-input :disabled="busy" v-model="hostPassword"></b-input>
</b-field>
</div>
</div>
<div class="center">
<b-button
:disabled="busy"
type="is-primary"
@click="create"
class="wide"
>
Create
</b-button>
</div>
</section>
</section>
<section class="room" v-else>
<section class="info">
Invite your friends:
<span class="selectable"
><a :href="inviteLink">{{ inviteLink }}</a></span
>
</section>
<section class="chat"></section>
<section class="players">
<header>Players</header>
<ul>
<li class="selectable" v-for="player in players" :key="player">
{{ player }}
</li>
</ul>
</section>
<section class="player-options">
<b-field>
<b-input :disabled="busy" v-model="wantedName"></b-input>
<p class="control">
<b-button
@click="changeName"
type="is-primary"
:disabled="!nameAvailable"
>Change name</b-button
>
</p>
</b-field>
<b-button type="is-danger">Leave room</b-button>
</section>
</section>
</section> </section>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "@/assets/scss/_variables.scss";
.lobby { .lobby {
background: url("../assets/images/backgrounds/menubg.webp") center; background: url("../assets/images/backgrounds/menubg.webp") center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
height: 100vh; height: 100vh;
display: flex;
flex-flow: column;
}
.body {
flex: 1;
display: grid;
padding: 10px;
padding-top: 0;
grid-template: 150px 1fr / 1fr 1fr;
#info {
grid-row: 1;
grid-column: 1 / end;
}
#join,
#host {
grid-row: 2;
}
#join {
grid-column: 2;
}
#host {
grid-column: 1;
}
section {
margin: 10px;
border: 1px solid rgba($white, 20%);
border-radius: 10px;
padding: 20px;
header {
font-family: $fantasy;
h1 {
text-align: center;
font-size: 18pt;
}
margin-bottom: 20px;
}
}
}
.wide {
min-width: 30%;
margin: 10px auto;
}
.full {
width: 100%;
margin: 10px;
}
.center {
width: 100%;
text-align: center;
}
.full-btn {
flex: 1;
:global(.button) {
width: 100%;
}
}
.room {
padding: 10px 20px;
margin: 10px;
border: 1px solid rgba($white, 20%);
border-radius: 10px;
display: grid;
grid-template: 50px 1fr / 200px 1fr 300px;
.info {
grid-column: 1 / max;
grid-row: 1;
}
.players {
header {
font-weight: bold;
margin-bottom: 5px;
}
grid-row: 2 / max;
grid-column: 1;
}
.chat {
grid-row: 2 / max;
grid-column: 2;
}
.player-options {
grid-row: 2 / max;
grid-column: 3;
}
}
.only-mobile {
display: none;
}
.selectable {
user-select: all;
}
@media (max-width: 500px) {
.only-full {
display: none;
}
.only-mobile {
display: inherit;
}
.body {
display: flex;
flex-direction: column;
section {
padding: 10px;
header h1 {
font-size: 14pt;
}
}
}
.room {
display: flex;
flex-flow: column;
& > * {
padding: 10px;
}
}
} }
</style> </style>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import TopNav from "@/components/Navigation/TopNav.vue"; import TopNav from "@/components/Navigation/TopNav.vue";
import { StartServerOptions, ConnectOptions } from "@/store/network/types";
import { Action, Getter } from "vuex-class";
import { Client, NetworkMessage } from "@/network";
const networkNS = { namespace: "network" };
@Component({ @Component({
components: { components: {
TopNav TopNav
} }
}) })
export default class Lobby extends Vue {} export default class Lobby extends Vue {
private playerName!: string;
private hostMaxPlayers!: number;
private hostPassword!: string;
private joinSessionID!: string;
private wantedName!: string;
@Action("startServer", networkNS)
private startServer!: (options: StartServerOptions) => void;
@Action("sendMessage", networkNS)
private sendMessage!: (message: NetworkMessage) => void;
@Action("connect", networkNS)
private connect!: (options: ConnectOptions) => void;
@Getter("inRoom", networkNS)
private inRoom!: boolean;
@Getter("busy", networkNS)
private busy!: boolean;
@Getter("sessionID", networkNS)
private sessionID!: string | null;
@Getter("players", networkNS)
private players!: string[];
private data() {
const playerName =
"Guest-" +
Math.random()
.toString()
.slice(2, 8);
return {
playerName,
hostMaxPlayers: 8,
hostPassword: "",
joinSessionID: "",
wantedName: playerName
};
}
private mounted() {
if ("id" in this.$route.params) {
this.joinSessionID = this.$route.params.id;
this.join();
}
}
private async create() {
this.wantedName = this.playerName;
this.startServer({
playerInfo: {
name: this.playerName
},
roomInfo: {
max_players: this.hostMaxPlayers,
password: this.hostPassword
}
});
}
private async join() {
this.wantedName = this.playerName;
this.connect({
serverID: this.joinSessionID,
playerInfo: {
name: this.playerName
}
});
}
private async changeName() {
this.sendMessage({
kind: "rename",
oldname: this.playerName,
newname: this.wantedName
});
}
private get canJoin(): boolean {
return this.joinSessionID != "";
}
private get inviteLink(): string {
let subpath = "";
const joinIndex = location.pathname.indexOf("/join");
if (joinIndex > 0) {
subpath = location.pathname.substring(0, joinIndex);
}
const lobbyIndex = location.pathname.indexOf("/lobby");
if (lobbyIndex > 0) {
subpath = location.pathname.substring(0, lobbyIndex);
}
return `${location.origin}${subpath}/join/${this.sessionID}`;
}
private get nameAvailable(): boolean {
return this.wantedName != "" && !this.players.includes(this.wantedName);
}
}
</script> </script>

View file

@ -1,14 +0,0 @@
<template>
<section class="room"></section>
</template>
<style lang="scss" scoped></style>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
@Component({
components: {}
})
export default class RoomView extends Vue {}
</script>

View file

@ -1,5 +1,12 @@
module.exports = { module.exports = {
publicPath: process.env.SUBPATH ? process.env.SUBPATH : "", configureWebpack: {
devServer: {
disableHostCheck: true,
host: "0.0.0.0"
}
},
publicPath: process.env.SUBPATH ? process.env.SUBPATH : "/",
pluginOptions: { pluginOptions: {
gitDescribe: { gitDescribe: {