Implement follow request / confirmation UI

This commit is contained in:
PizZaKatZe 2021-12-12 16:56:26 +01:00
parent d6ef60a3d8
commit 0a410d289d
11 changed files with 481 additions and 79 deletions

View File

@ -14,6 +14,7 @@ import {
SubToPusherRoomMessage,
VariableMessage,
VariableWithTagMessage,
ServerToClientMessage,
} from "../Messages/generated/messages_pb";
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
import { RoomSocket, ZoneSocket } from "src/RoomManager";
@ -95,10 +96,20 @@ export class GameRoom {
return Array.from(this.groups.values());
}
public getGroupIncludingUser(user: User): Group | undefined {
const foundGroups = this.getGroups().filter((grp) => grp.includes(user));
return foundGroups[0];
}
public getUsers(): Map<number, User> {
return this.users;
}
public getUserByName(name: string): User | undefined {
let foundUsers = Array.from(this.users.values());
foundUsers = foundUsers.filter((user: User) => user.name === name);
return foundUsers[0];
}
public getUserByUuid(uuid: string): User | undefined {
return this.usersByUuid.get(uuid);
}
@ -226,6 +237,20 @@ export class GameRoom {
}
}
public sendToOthersInGroupIncludingUser(user: User, message: ServerToClientMessage): void {
this.getGroupIncludingUser(user)
?.getUsers()
.forEach((currentUser: User) => {
if (currentUser.name !== user.name) {
currentUser.socket.write(message);
}
});
}
public sendToUserWithName(name: string, message: ServerToClientMessage): void {
this.getUserByName(name)?.socket.write(message);
}
setSilent(user: User, silent: boolean) {
if (user.silent === silent) {
return;

View File

@ -8,7 +8,9 @@ import {
BatchToPusherMessage,
BatchToPusherRoomMessage,
EmotePromptMessage,
FollowMeRequestMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
@ -117,11 +119,23 @@ const roomManager: IRoomManagerServer = {
user,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasFollowmerequestmessage()) {
socketManager.handleFollowMeRequestMessage(
} else if (message.hasFollowrequestmessage()) {
socketManager.handleFollowRequestMessage(
room,
user,
message.getFollowmerequestmessage() as FollowMeRequestMessage
message.getFollowrequestmessage() as FollowRequestMessage
);
} else if (message.hasFollowconfirmationmessage()) {
socketManager.handleFollowConfirmationMessage(
room,
user,
message.getFollowconfirmationmessage() as FollowConfirmationMessage
);
} else if (message.hasFollowabortmessage()) {
socketManager.handleFollowAbortMessage(
room,
user,
message.getFollowabortmessage() as FollowAbortMessage
);
} else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage();

View File

@ -30,7 +30,9 @@ import {
BanUserMessage,
RefreshRoomMessage,
EmotePromptMessage,
FollowMeRequestMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage,
BatchToPusherRoomMessage,
SubToPusherRoomMessage,
@ -835,24 +837,30 @@ export class SocketManager {
room.emitEmoteEvent(user, emoteEventMessage);
}
handleFollowMeRequestMessage(room: GameRoom, user: User, requestMessage: FollowMeRequestMessage) {
// Find group including the requesting user
let foundGroups = room.getGroups().filter((grp) => grp.includes(user));
if (!foundGroups[0]) {
return;
}
let group = foundGroups[0];
// Send invitations to other group members
requestMessage.setPlayername(user.name);
handleFollowRequestMessage(room: GameRoom, user: User, message: FollowRequestMessage) {
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowmerequestmessage(requestMessage);
group.getUsers().forEach((currentUser: User) => {
if (user.name !== currentUser.name) {
console.log("Inviting " + currentUser.name + " to follow " + user.name);
currentUser.socket.write(clientMessage);
clientMessage.setFollowrequestmessage(message);
room.sendToOthersInGroupIncludingUser(user, clientMessage);
}
handleFollowConfirmationMessage(room: GameRoom, user: User, message: FollowConfirmationMessage) {
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowconfirmationmessage(message);
room.sendToUserWithName(message.getLeader(), clientMessage);
}
handleFollowAbortMessage(room: GameRoom, user: User, message: FollowAbortMessage) {
if (message.getRole() === "leader") {
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowabortmessage(message);
room.sendToOthersInGroupIncludingUser(user, clientMessage);
} else {
const recipient = message.getPlayername();
message.setPlayername(user.name);
const clientMessage = new ServerToClientMessage();
clientMessage.setFollowabortmessage(message);
room.sendToUserWithName(recipient, clientMessage);
}
});
}
}

View File

@ -42,6 +42,8 @@
import AudioManager from "./AudioManager/AudioManager.svelte";
import { showReportScreenStore, userReportEmpty } from "../Stores/ShowReportScreenStore";
import ReportMenu from "./ReportMenu/ReportMenu.svelte";
import { followStateStore, followStates } from "../Stores/InteractStore";
import InteractMenu from "./InteractMenu/InteractMenu.svelte";
export let game: Game;
</script>
@ -102,6 +104,11 @@
<ReportMenu />
</div>
{/if}
{#if $followStateStore !== followStates.off}
<div>
<InteractMenu />
</div>
{/if}
{#if $menuIconVisiblilityStore}
<div>
<MenuIcon />

View File

@ -0,0 +1,238 @@
<!--
vim: ft=typescript
-->
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import type { Unsubscriber } from "svelte/store";
import { get } from "svelte/store";
import { gameManager } from "../../Phaser/Game/GameManager";
import {
followStateStore,
followRoleStore,
followUsersStore,
followRoles,
followStates,
} from "../../Stores/InteractStore";
const gameScene = gameManager.getCurrentGameScene();
let followState: string;
let followRole: string;
let followUsers: string[];
let stateUnsubscriber: Unsubscriber;
let roleUnsubscriber: Unsubscriber;
let nameUnsubscriber: Unsubscriber;
onMount(() => {
followState = get(followStateStore);
followRole = get(followRoleStore);
followUsers = get(followUsersStore);
stateUnsubscriber = followStateStore.subscribe((state) => {
followState = state;
});
roleUnsubscriber = followRoleStore.subscribe((role) => {
followRole = role;
});
nameUnsubscriber = followUsersStore.subscribe((users) => {
followUsers = users;
});
});
onDestroy(() => {
if (stateUnsubscriber) {
stateUnsubscriber();
}
if (roleUnsubscriber) {
roleUnsubscriber();
}
if (nameUnsubscriber) {
nameUnsubscriber();
}
});
function sendFollowRequest() {
gameScene.connection?.emitFollowRequest(gameManager.getPlayerName());
followStateStore.set(followStates.active);
}
function acceptFollowRequest() {
gameScene.CurrentPlayer.enableFollowing();
gameScene.connection?.emitFollowConfirmation(followUsers[0], gameManager.getPlayerName());
}
function abortEnding() {
followStateStore.set(followStates.active);
}
function reset() {
if (followRole === followRoles.leader && followUsers.length > 0) {
gameScene.connection?.emitFollowAbort(followRole, gameManager.getPlayerName());
} else {
gameScene.connection?.emitFollowAbort(followRole, followUsers[0]);
}
followStateStore.set(followStates.off);
followRoleStore.set(followRoles.leader);
followUsersStore.set([]);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
reset();
}
}
</script>
<svelte:window on:keydown={onKeyDown} />
{#if followState === followStates.requesting}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Interaction</h2>
</section>
{#if followRole === followRoles.follower}
<section class="interact-menu-question">
<p>Do you want to follow {followUsers[0]}?</p>
</section>
<section class="interact-menu-action">
<button type="button" class="accept" on:click|preventDefault={acceptFollowRequest}>Yes</button>
<button type="button" class="deny" on:click|preventDefault={reset}>No</button>
</section>
{:else if followRole === followRoles.leader}
<section class="interact-menu-question">
<p>Ask others to follow you?</p>
</section>
<section class="interact-menu-action">
<button type="button" class="accept" on:click|preventDefault={sendFollowRequest}>Yes</button>
<button type="button" class="deny" on:click|preventDefault={reset}>No</button>
</section>
{/if}
</div>
{/if}
{#if followState === followStates.ending}
<div class="interact-menu nes-container is-rounded">
<section class="interact-menu-title">
<h2>Interaction</h2>
</section>
{#if followRole === followRoles.follower}
<section class="interact-menu-question">
<p>Do you want to stop following {followUsers[0]}?</p>
</section>
{:else if followRole === followRoles.leader}
<section class="interact-menu-question">
<p>Do you want to stop leading the way?</p>
</section>
{/if}
<section class="interact-menu-action">
<button type="button" class="accept" on:click|preventDefault={reset}>Yes</button>
<button type="button" class="deny" on:click|preventDefault={abortEnding}>No</button>
</section>
</div>
{/if}
{#if followState === followStates.active || followState === followStates.ending}
<div class="interact-status nes-container is-rounded">
<section class="interact-status">
{#if followRole === followRoles.follower}
<p>Following {followUsers[0]}</p>
{:else if followUsers.length === 0}
<p>Waiting for followers' confirmation</p>
{:else if followUsers.length === 1}
<p>{followUsers[0]} is following you</p>
{:else if followUsers.length === 2}
<p>{followUsers[0]} and {followUsers[1]} are following you</p>
{:else}
<p>{followUsers[0]}, {followUsers[1]} and {followUsers[2]} are following you</p>
{/if}
</section>
</div>
{/if}
<style lang="scss">
.nes-container {
padding: 5px;
}
div.interact-status {
background-color: #333333;
color: whitesmoke;
position: relative;
height: 2.7em;
width: 40vw;
top: 87vh;
margin: auto;
text-align: center;
}
div.interact-menu {
pointer-events: auto;
background-color: #333333;
color: whitesmoke;
position: relative;
height: 19vh;
width: 60vw;
top: 60vh;
margin: auto;
section.interact-menu-title {
margin-bottom: 20px;
display: flex;
justify-content: center;
}
section.interact-menu-question {
margin: 4px;
margin-bottom: 20px;
p {
font-size: 1.05em;
font-weight: bold;
}
}
section.interact-menu-action {
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 20px;
button {
display: inline-block;
margin: 4px;
padding: 0px;
border: medium solid black;
font-weight: bold;
height: 2.5em;
}
.accept {
background-color: #00ff0088;
}
.accept:hover {
background-color: #00ff00cc;
}
.deny {
background-color: #ff000088;
}
.deny:hover {
background-color: #ff0000cc;
}
}
}
@media only screen and (max-width: 800px) {
div.interact-status {
width: 100vw;
top: 78vh;
font-size: 0.75em;
}
div.interact-menu {
height: 21vh;
width: 100vw;
font-size: 0.75em;
}
}
</style>

View File

@ -30,7 +30,9 @@ import {
PingMessage,
EmoteEventMessage,
EmotePromptMessage,
FollowMeRequestMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
SendUserMessage,
BanUserMessage,
VariableMessage,
@ -58,7 +60,15 @@ import { adminMessagesService } from "./AdminMessagesService";
import { worldFullMessageStream } from "./WorldFullMessageStream";
import { connectionManager } from "./ConnectionManager";
import { emoteEventStream } from "./EmoteEventStream";
import { get } from "svelte/store";
import { warningContainerStore } from "../Stores/MenuStore";
import {
followStateStore,
followRoleStore,
followUsersStore,
followRoles,
followStates,
} from "../Stores/InteractStore";
const manualPingDelay = 20000;
@ -258,9 +268,32 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed.
} else if (message.hasFollowmerequestmessage()) {
const requestMessage = message.getFollowmerequestmessage() as FollowMeRequestMessage;
console.log("Follow me request from " + requestMessage.getPlayername());
} else if (message.hasFollowrequestmessage()) {
const requestMessage = message.getFollowrequestmessage() as FollowRequestMessage;
console.log("Got follow request from " + requestMessage.getPlayername());
followStateStore.set(followStates.requesting);
followRoleStore.set(followRoles.follower);
followUsersStore.set([requestMessage.getPlayername()]);
} else if (message.hasFollowconfirmationmessage()) {
const responseMessage = message.getFollowconfirmationmessage() as FollowConfirmationMessage;
console.log("Got follow response from " + responseMessage.getFollower());
followUsersStore.set([...get(followUsersStore), responseMessage.getFollower()]);
} else if (message.hasFollowabortmessage()) {
const abortMessage = message.getFollowabortmessage() as FollowAbortMessage;
console.log("Got follow abort message from", abortMessage.getRole());
if (abortMessage.getRole() === followRoles.leader) {
followStateStore.set(followStates.off);
followRoleStore.set(followRoles.leader);
followUsersStore.set([]);
} else {
let followers = get(followUsersStore);
followers = followers.filter((name) => name !== abortMessage.getPlayername());
followUsersStore.set(followers);
if (followers.length === 0) {
followStateStore.set(followStates.off);
followRoleStore.set(followRoles.leader);
}
}
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
@ -716,11 +749,41 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowMeRequest(): void {
console.log("Emitting follow me request");
const message = new FollowMeRequestMessage();
public emitFollowRequest(user: string | null): void {
if (!user) {
return;
}
console.log("Emitting follow request");
const message = new FollowRequestMessage();
message.setPlayername(user);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowmerequestmessage(message);
clientToServerMessage.setFollowrequestmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowConfirmation(leader: string, follower: string | null): void {
if (!follower) {
return;
}
console.log("Emitting follow confirmation");
const message = new FollowConfirmationMessage();
message.setLeader(leader);
message.setFollower(follower);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowconfirmationmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitFollowAbort(role: string, user: string | null): void {
if (!user) {
return;
}
console.log("Emitting follow abort");
const message = new FollowAbortMessage();
message.setRole(role);
message.setPlayername(user);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setFollowabortmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}

View File

@ -3,7 +3,16 @@ import type { GameScene } from "../Game/GameScene";
import { ActiveEventList, UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import type { RemotePlayer } from "../Entity/RemotePlayer";
import { get } from "svelte/store";
import { userMovingStore } from "../../Stores/GameStore";
import {
followStateStore,
followRoleStore,
followUsersStore,
followRoles,
followStates,
} from "../../Stores/InteractStore";
export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
@ -156,44 +165,34 @@ export class Player extends Character {
userMovingStore.set(moving);
}
moveUser(delta: number): void {
const activeEvents = this.userInputManager.getEventListForGameTick();
if (activeEvents.get(UserInputEvent.Interact)) {
const sortedPlayers = Array.from(this.scene.MapPlayersByKey.values()).sort((p1, p2) => {
const sdistToP1 = Math.pow(p1.x - this.x, 2) + Math.pow(p1.y - this.y, 2);
const sdistToP2 = Math.pow(p2.x - this.x, 2) + Math.pow(p2.y - this.y, 2);
if (sdistToP1 > sdistToP2) {
return 1;
} else if (sdistToP1 < sdistToP2) {
return -1;
} else {
return 0;
public enableFollowing() {
Array.from(this.scene.MapPlayersByKey.values()).forEach((player) => {
if (player.PlayerValue !== get(followUsersStore)[0]) {
return;
}
});
const minFollowDist = 10000;
if (typeof sortedPlayers !== "undefined" && sortedPlayers.length > 0) {
const sdist = Math.pow(sortedPlayers[0].x - this.x, 2) + Math.pow(sortedPlayers[0].y - this.y, 2);
if (sdist < minFollowDist) {
this.follow = {
followPlayer: sortedPlayers[0],
followPlayer: player,
direction: this.previousDirection,
};
followStateStore.set(followStates.active);
});
}
public moveUser(delta: number): void {
const activeEvents = this.userInputManager.getEventListForGameTick();
const state = get(followStateStore);
const role = get(followRoleStore);
if (activeEvents.get(UserInputEvent.Interact)) {
if (state === followStates.off && this.scene.groups.size > 0) {
followStateStore.set(followStates.requesting);
followRoleStore.set(followRoles.leader);
} else if (state === followStates.active) {
followStateStore.set(followStates.ending);
}
}
if (
activeEvents.get(UserInputEvent.MoveUp) ||
activeEvents.get(UserInputEvent.MoveDown) ||
activeEvents.get(UserInputEvent.MoveLeft) ||
activeEvents.get(UserInputEvent.MoveRight)
) {
this.follow = null;
}
if (this.follow === null) {
if ((state !== followStates.active && state !== followStates.ending) || role !== followRoles.follower) {
this.inputStep(activeEvents, delta);
} else {
this.followStep(activeEvents, delta);

View File

@ -0,0 +1,17 @@
import { writable } from "svelte/store";
export const followStates = {
off: "off",
requesting: "requesting",
active: "active",
ending: "ending",
};
export const followRoles = {
leader: "leader",
follower: "follower",
};
export const followStateStore = writable(followStates.off);
export const followRoleStore = writable(followRoles.leader);
export const followUsersStore = writable<string[]>([]);

View File

@ -80,13 +80,18 @@ message QueryJitsiJwtMessage {
string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map!
}
message FollowMeRequestMessage {
message FollowRequestMessage {
string playerName = 1;
}
message FollowMeResponseMessage {
string playerName = 1;
bool accepted = 2;
message FollowConfirmationMessage {
string leader = 1;
string follower = 2;
}
message FollowAbortMessage {
string role = 1;
string playerName = 2;
}
message ClientToServerMessage {
@ -104,8 +109,9 @@ message ClientToServerMessage {
QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
EmotePromptMessage emotePromptMessage = 13;
VariableMessage variableMessage = 14;
FollowMeRequestMessage followMeRequestMessage = 15;
FollowMeResponseMessage followMeResponseMessage = 16;
FollowRequestMessage followRequestMessage = 15;
FollowConfirmationMessage followConfirmationMessage = 16;
FollowAbortMessage followAbortMessage = 17;
}
}
@ -296,8 +302,9 @@ message ServerToClientMessage {
WorldConnexionMessage worldConnexionMessage = 18;
//EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20;
FollowMeRequestMessage followMeRequestMessage = 21;
FollowMeResponseMessage followMeResponseMessage = 22;
FollowRequestMessage followRequestMessage = 21;
FollowConfirmationMessage followConfirmationMessage = 22;
FollowAbortMessage followAbortMessage = 23;
}
}
@ -378,8 +385,9 @@ message PusherToBackMessage {
BanUserMessage banUserMessage = 13;
EmotePromptMessage emotePromptMessage = 14;
VariableMessage variableMessage = 15;
FollowMeRequestMessage followMeRequestMessage = 16;
FollowMeResponseMessage followMeResponseMessage = 17;
FollowRequestMessage followRequestMessage = 16;
FollowConfirmationMessage followConfirmationMessage = 17;
FollowAbortMessage followAbortMessage = 18;
}
}

View File

@ -17,7 +17,9 @@ import {
ServerToClientMessage,
CompanionMessage,
EmotePromptMessage,
FollowMeRequestMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
VariableMessage,
} from "../Messages/generated/messages_pb";
import { UserMovesMessage } from "../Messages/generated/messages_pb";
@ -470,11 +472,18 @@ export class IoSocketController {
client,
message.getEmotepromptmessage() as EmotePromptMessage
);
} else if (message.hasFollowmerequestmessage()) {
socketManager.handleFollowMeRequest(
} else if (message.hasFollowrequestmessage()) {
socketManager.handleFollowRequest(
client,
message.getFollowmerequestmessage() as FollowMeRequestMessage
message.getFollowrequestmessage() as FollowRequestMessage
);
} else if (message.hasFollowconfirmationmessage()) {
socketManager.handleFollowConfirmation(
client,
message.getFollowconfirmationmessage() as FollowConfirmationMessage
);
} else if (message.hasFollowabortmessage()) {
socketManager.handleFollowAbort(client, message.getFollowabortmessage() as FollowAbortMessage);
}
/* Ok is false if backpressure was built up, wait for drain */

View File

@ -8,7 +8,9 @@ import {
CharacterLayerMessage,
EmoteEventMessage,
EmotePromptMessage,
FollowMeRequestMessage,
FollowRequestMessage,
FollowConfirmationMessage,
FollowAbortMessage,
GroupDeleteMessage,
ItemEventMessage,
JoinRoomMessage,
@ -270,9 +272,21 @@ export class SocketManager implements ZoneEventListener {
this.handleViewport(client, viewport.toObject());
}
handleFollowMeRequest(client: ExSocketInterface, requestMessage: FollowMeRequestMessage): void {
handleFollowRequest(client: ExSocketInterface, message: FollowRequestMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowmerequestmessage(requestMessage);
pusherToBackMessage.setFollowrequestmessage(message);
client.backConnection.write(pusherToBackMessage);
}
handleFollowConfirmation(client: ExSocketInterface, message: FollowConfirmationMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowconfirmationmessage(message);
client.backConnection.write(pusherToBackMessage);
}
handleFollowAbort(client: ExSocketInterface, message: FollowAbortMessage): void {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setFollowabortmessage(message);
client.backConnection.write(pusherToBackMessage);
}