From 524339a3a005d23386c0e49e9922c189973c48f7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20N=C3=A9grier?= <d.negrier@thecodingmachine.com>
Date: Thu, 23 Dec 2021 18:07:51 +0100
Subject: [PATCH] Adding hightlight to player names when they follow each
 others

---
 .../Components/FollowMenu/FollowMenu.svelte   |  4 +-
 front/src/Connexion/RoomConnection.ts         | 19 +---
 front/src/Phaser/Game/GameScene.ts            | 23 ++++-
 front/src/Stores/FollowStore.ts               | 87 ++++++++++++++++++-
 front/src/WebRtc/ColorGenerator.ts            | 26 +++++-
 5 files changed, 132 insertions(+), 27 deletions(-)

diff --git a/front/src/Components/FollowMenu/FollowMenu.svelte b/front/src/Components/FollowMenu/FollowMenu.svelte
index 72f55613..c38571bd 100644
--- a/front/src/Components/FollowMenu/FollowMenu.svelte
+++ b/front/src/Components/FollowMenu/FollowMenu.svelte
@@ -72,9 +72,7 @@ vim: ft=typescript
 
     function reset() {
         gameScene.connection?.emitFollowAbort();
-        followStateStore.set(followStates.off);
-        followRoleStore.set(followRoles.leader);
-        followUsersStore.set([]);
+        followUsersStore.stopFollowing();
     }
 
     function request() {
diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts
index b9a3ca12..6e88d3d8 100644
--- a/front/src/Connexion/RoomConnection.ts
+++ b/front/src/Connexion/RoomConnection.ts
@@ -271,28 +271,17 @@ export class RoomConnection implements RoomConnection {
             } else if (message.hasFollowrequestmessage()) {
                 const requestMessage = message.getFollowrequestmessage() as FollowRequestMessage;
                 if (!localUserStore.getIgnoreFollowRequests()) {
-                    followStateStore.set(followStates.requesting);
-                    followRoleStore.set(followRoles.follower);
-                    followUsersStore.set([requestMessage.getLeader()]);
+                    followUsersStore.addFollowRequest(requestMessage.getLeader());
                 }
             } else if (message.hasFollowconfirmationmessage()) {
                 const responseMessage = message.getFollowconfirmationmessage() as FollowConfirmationMessage;
-                followUsersStore.set([...get(followUsersStore), responseMessage.getFollower()]);
+                followUsersStore.addFollower(responseMessage.getFollower());
             } else if (message.hasFollowabortmessage()) {
                 const abortMessage = message.getFollowabortmessage() as FollowAbortMessage;
                 if (get(followRoleStore) === followRoles.follower) {
-                    followStateStore.set(followStates.off);
-                    followRoleStore.set(followRoles.leader);
-                    followUsersStore.set([]);
+                    followUsersStore.stopFollowing();
                 } else {
-                    let followers = get(followUsersStore);
-                    const oldFollowerCount = followers.length;
-                    followers = followers.filter((name) => name !== abortMessage.getFollower());
-                    followUsersStore.set(followers);
-                    if (followers.length === 0 && oldFollowerCount > 0) {
-                        followStateStore.set(followStates.off);
-                        followRoleStore.set(followRoles.leader);
-                    }
+                    followUsersStore.removeFollower(abortMessage.getFollower());
                 }
             } else if (message.hasErrormessage()) {
                 const errorMessage = message.getErrormessage() as ErrorMessage;
diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts
index b31ed83b..4800e259 100644
--- a/front/src/Phaser/Game/GameScene.ts
+++ b/front/src/Phaser/Game/GameScene.ts
@@ -1,7 +1,7 @@
 import type { Subscription } from "rxjs";
 import AnimatedTiles from "phaser-animated-tiles";
 import { Queue } from "queue-typescript";
-import { get } from "svelte/store";
+import { get, Unsubscriber } from "svelte/store";
 
 import { userMessageManager } from "../../Administration/UserMessageManager";
 import { connectionManager } from "../../Connexion/ConnectionManager";
@@ -91,6 +91,8 @@ import { deepCopy } from "deep-copy-ts";
 import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
 import { MapStore } from "../../Stores/Utils/MapStore";
 import { SetPlayerDetailsMessage } from "../../Messages/generated/messages_pb";
+import { followUsersColorStore, followUsersStore } from "../../Stores/FollowStore";
+import { getColorRgbFromHue } from "../../WebRtc/ColorGenerator";
 
 export interface GameSceneInitInterface {
     initPosition: PointInterface | null;
@@ -165,9 +167,11 @@ export class GameScene extends DirtyScene {
     private createPromise: Promise<void>;
     private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
     private iframeSubscriptionList!: Array<Subscription>;
-    private peerStoreUnsubscribe!: () => void;
-    private emoteUnsubscribe!: () => void;
-    private emoteMenuUnsubscribe!: () => void;
+    private peerStoreUnsubscribe!: Unsubscriber;
+    private emoteUnsubscribe!: Unsubscriber;
+    private emoteMenuUnsubscribe!: Unsubscriber;
+    private followUsersColorStoreUnsubscribe!: Unsubscriber;
+
     private biggestAvailableAreaStoreUnsubscribe!: () => void;
     MapUrlFile: string;
     roomUrl: string;
@@ -646,6 +650,16 @@ export class GameScene extends DirtyScene {
             }
         });
 
+        this.followUsersColorStoreUnsubscribe = followUsersColorStore.subscribe((color) => {
+            if (color !== undefined) {
+                this.CurrentPlayer.setOutlineColor(color);
+                this.connection?.emitPlayerOutlineColor(color);
+            } else {
+                this.CurrentPlayer.removeOutlineColor();
+                this.connection?.emitPlayerOutlineColor(null);
+            }
+        });
+
         Promise.all([this.connectionAnswerPromise as Promise<unknown>, ...scriptPromises]).then(() => {
             this.scene.wake();
         });
@@ -1443,6 +1457,7 @@ ${escapedMessage}
         this.peerStoreUnsubscribe();
         this.emoteUnsubscribe();
         this.emoteMenuUnsubscribe();
+        this.followUsersColorStoreUnsubscribe();
         this.biggestAvailableAreaStoreUnsubscribe();
         iframeListener.unregisterAnswerer("getState");
         iframeListener.unregisterAnswerer("loadTileset");
diff --git a/front/src/Stores/FollowStore.ts b/front/src/Stores/FollowStore.ts
index 6c85ab17..856d02fe 100644
--- a/front/src/Stores/FollowStore.ts
+++ b/front/src/Stores/FollowStore.ts
@@ -1,4 +1,6 @@
-import { writable } from "svelte/store";
+import { derived, writable } from "svelte/store";
+import { getColorRgbFromHue } from "../WebRtc/ColorGenerator";
+import { gameManager } from "../Phaser/Game/GameManager";
 
 export const followStates = {
     off: "off",
@@ -14,4 +16,85 @@ export const followRoles = {
 
 export const followStateStore = writable(followStates.off);
 export const followRoleStore = writable(followRoles.leader);
-export const followUsersStore = writable<number[]>([]);
+//export const followUsersStore = writable<number[]>([]);
+
+function createFollowUsersStore() {
+    const { subscribe, update, set } = writable<number[]>([]);
+
+    return {
+        subscribe,
+        addFollowRequest(leader: number): void {
+            followStateStore.set(followStates.requesting);
+            followRoleStore.set(followRoles.follower);
+            set([leader]);
+        },
+        addFollower(user: number): void {
+            update((followers) => {
+                followers.push(user);
+                return followers;
+            });
+        },
+        /**
+         * Removes the follower from the store.
+         * Will update followStateStore and followRoleStore if nobody is following anymore.
+         * @param user
+         */
+        removeFollower(user: number): void {
+            update((followers) => {
+                const oldFollowerCount = followers.length;
+                followers = followers.filter((id) => id !== user);
+
+                if (followers.length === 0 && oldFollowerCount > 0) {
+                    followStateStore.set(followStates.off);
+                    followRoleStore.set(followRoles.leader);
+                }
+
+                return followers;
+            });
+        },
+        stopFollowing(): void {
+            set([]);
+            followStateStore.set(followStates.off);
+            followRoleStore.set(followRoles.leader);
+        },
+    };
+}
+
+export const followUsersStore = createFollowUsersStore();
+
+/**
+ * This store contains the color of the follow group. It is derived from the ID of the leader.
+ */
+export const followUsersColorStore = derived(
+    [followStateStore, followRoleStore, followUsersStore],
+    ([$followStateStore, $followRoleStore, $followUsersStore]) => {
+        console.log($followStateStore);
+        if ($followStateStore !== followStates.active) {
+            return undefined;
+        }
+
+        if ($followUsersStore.length === 0) {
+            return undefined;
+        }
+
+        let leaderId: number;
+        if ($followRoleStore === followRoles.leader) {
+            // Let's get my ID by a quite complicated way....
+            leaderId = gameManager.getCurrentGameScene().connection?.getUserId() ?? 0;
+        } else {
+            leaderId = $followUsersStore[0];
+        }
+
+        // Let's compute a random hue between 0 and 1 that varies enough to be interesting
+        const hue = ((leaderId * 197) % 255) / 255;
+
+        let { r, g, b } = getColorRgbFromHue(hue);
+        if ($followRoleStore === followRoles.follower) {
+            // Let's make the followers very slightly darker
+            r *= 0.9;
+            g *= 0.9;
+            b *= 0.9;
+        }
+        return (Math.round(r * 255) << 16) | (Math.round(g * 255) << 8) | Math.round(b * 255);
+    }
+);
diff --git a/front/src/WebRtc/ColorGenerator.ts b/front/src/WebRtc/ColorGenerator.ts
index be192f9f..f78671e6 100644
--- a/front/src/WebRtc/ColorGenerator.ts
+++ b/front/src/WebRtc/ColorGenerator.ts
@@ -1,13 +1,29 @@
 export function getRandomColor(): string {
+    const { r, g, b } = getColorRgbFromHue(Math.random());
+    return toHexa(r, g, b);
+}
+
+function toHexa(r: number, g: number, b: number): string {
+    return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
+}
+
+export function getColorRgbFromHue(hue: number): { r: number; g: number; b: number } {
     const golden_ratio_conjugate = 0.618033988749895;
-    let hue = Math.random();
     hue += golden_ratio_conjugate;
     hue %= 1;
     return hsv_to_rgb(hue, 0.5, 0.95);
 }
 
+function stringToDouble(string: string): number {
+    let num = 1;
+    for (const char of string.split("")) {
+        num *= char.charCodeAt(0);
+    }
+    return (num % 255) / 255;
+}
+
 //todo: test this.
-function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
+function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } {
     const h_i = Math.floor(hue * 6);
     const f = hue * 6 - h_i;
     const p = brightness * (1 - saturation);
@@ -48,5 +64,9 @@ function hsv_to_rgb(hue: number, saturation: number, brightness: number): string
         default:
             throw "h_i cannot be " + h_i;
     }
-    return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
+    return {
+        r,
+        g,
+        b,
+    };
 }