From 846b86f74ef9195095aeda592277b3f7c9e2ee2c Mon Sep 17 00:00:00 2001 From: loplkc Date: Sun, 27 Feb 2022 21:28:11 -0500 Subject: [PATCH] Added groundwork for action system + "UnknownTable" universal type ~ Moved data shared by scripts on the client and server to "Shared.ts" ~ ESLint on this machine reformatted a ton of things --- src/client/ClientMessenger.ts | 2 +- src/client/EffectMaker.ts | 47 +++++++++++------- src/client/InputHandler.ts | 72 +++++++++++++++------------- src/client/init.client.ts | 33 +++++++++---- src/server/ServerMessenger.ts | 2 +- src/server/main.server.ts | 21 +++++--- src/services.d.ts | 4 +- src/shared/AbilityManager.ts | 4 ++ src/shared/EntityManager.ts | 34 +++++++++++-- src/shared/PlayerManager.ts | 27 +++++++---- src/shared/{Remotes.ts => Shared.ts} | 4 ++ 11 files changed, 164 insertions(+), 86 deletions(-) create mode 100644 src/shared/AbilityManager.ts rename src/shared/{Remotes.ts => Shared.ts} (66%) diff --git a/src/client/ClientMessenger.ts b/src/client/ClientMessenger.ts index da21c94..5c3c350 100644 --- a/src/client/ClientMessenger.ts +++ b/src/client/ClientMessenger.ts @@ -1,4 +1,4 @@ -import { Input, Output } from "shared/Remotes"; +import { Input, Output } from "shared/Shared"; export function bindToServerMessage(functionToBind: Callback) { assert(Output?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); Output.OnClientEvent.Connect(functionToBind); diff --git a/src/client/EffectMaker.ts b/src/client/EffectMaker.ts index e6abd4a..d4699df 100644 --- a/src/client/EffectMaker.ts +++ b/src/client/EffectMaker.ts @@ -1,6 +1,14 @@ const TweenService = game.GetService("TweenService"); -export type effectKeypoint = [time: number, color: Color3, size: Vector3, cframe: CFrame, transparency: number, easingStyle?: Enum.EasingStyle, easingDirection?: Enum.EasingDirection]; -type effect = [number, effectKeypoint[], MeshPart, number] // Time since last keypoint, effect keypoints, effect meshPart, effect priority +export type effectKeypoint = [ + time: number, + color: Color3, + size: Vector3, + cframe: CFrame, + transparency: number, + easingStyle?: Enum.EasingStyle, + easingDirection?: Enum.EasingDirection, +]; +type effect = [number, effectKeypoint[], MeshPart, number]; // Time since last keypoint, effect keypoints, effect meshPart, effect priority export interface effectMaker { meshPartEffect: ( @@ -8,7 +16,7 @@ export interface effectMaker { material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number, - ) => void; + ) => void; particleEffect: () => void; } @@ -23,7 +31,7 @@ interface effectHandler extends effectMaker, effectRunner { */ class effectHandler implements effectMaker, effectRunner { constructor(effectFolder: Folder) { - this.EFFECT_FOLDER = effectFolder + this.EFFECT_FOLDER = effectFolder; } meshPartEffect(meshPart: MeshPart, material: Enum.Material, effectKeypoints: effectKeypoint[], priority?: number) { const effectMeshPart = meshPart.Clone(); @@ -32,32 +40,37 @@ class effectHandler implements effectMaker, effectRunner { effectMeshPart.Size = effectKeypoints[0][2]; effectMeshPart.CFrame = effectKeypoints[0][3]; effectMeshPart.Transparency = effectKeypoints[0][4]; - let effectDuration = 0 - effectKeypoints.forEach(effectKeypoint => { - effectDuration += effectKeypoint[0] + let effectDuration = 0; + effectKeypoints.forEach((effectKeypoint) => { + effectDuration += effectKeypoint[0]; }); effectMeshPart.CastShadow = false; effectMeshPart.CanCollide = false; effectMeshPart.Anchored = true; effectMeshPart.Parent = this.EFFECT_FOLDER; // Insert the effect before the effect that will end after it - const effectsToRun = this.effectsToRun + const effectsToRun = this.effectsToRun; for (let index = 0; index < effectsToRun.size(); index += 1) { const effectInArray = effectsToRun[index]; - let effectInArrayDuration = -effectInArray[0] - effectInArray[1].forEach(effectKeypoint => { - effectInArrayDuration += effectKeypoint[0] + let effectInArrayDuration = -effectInArray[0]; + effectInArray[1].forEach((effectKeypoint) => { + effectInArrayDuration += effectKeypoint[0]; }); if (effectInArrayDuration > effectDuration) { - effectsToRun.insert(index, [0, effectKeypoints, effectMeshPart, priority || 0] as effect); - break + effectsToRun.insert(index, [0, effectKeypoints, effectMeshPart, priority !== undefined || 0] as effect); + break; } } - effectsToRun.insert(effectsToRun.size(), [0, effectKeypoints, effectMeshPart, priority || 0] as effect); + effectsToRun.insert(effectsToRun.size(), [ + 0, + effectKeypoints, + effectMeshPart, + priority !== undefined || 0, + ] as effect); } particleEffect() {} runEffects(timeSinceLastFrame: number) { - let effectsToRun = this.effectsToRun; + const effectsToRun = this.effectsToRun; print(tostring(effectsToRun.size()) + " effects to run."); for (const effect of effectsToRun) { // Update the effect time @@ -72,7 +85,7 @@ class effectHandler implements effectMaker, effectRunner { timeOfNextKeypoint = nextKeypoint[0]; } else { effect[2].Destroy(); - effectsToRun.remove(0); + effectsToRun.remove(0); continue; // Move on if this effect is done } } @@ -102,7 +115,7 @@ class effectHandler implements effectMaker, effectRunner { EFFECT_FOLDER: Folder; effectsToRun: effect[] = []; -} +} export function makeEffectRunner(effectFolder: Folder): effectRunner { return new effectHandler(effectFolder); diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index 599c27f..404d776 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -7,14 +7,14 @@ assert(CAMERA, 'Camera of "' + Players.LocalPlayer.DisplayName + '"does not exis function enumTypeIs(value: unknown, EnumAsObject: Enum): value is EnumAsType { if (typeIs(value, "EnumItem")) { - return value.Name in EnumAsObject + return value.Name in EnumAsObject; } else { - return false + return false; } } -type validInput = Enum.KeyCode // + Include controller "keys" +type validInput = Enum.KeyCode; // + Include controller "keys" function isValidInput(value: unknown): value is validInput { - return enumTypeIs(value, Enum.KeyCode) + return enumTypeIs(value, Enum.KeyCode); } const actionAssignmentsReference: string[] = [ "clicker1", // What is used to click on things (enemies in game, UI elements) @@ -24,8 +24,9 @@ const actionAssignmentsReference: string[] = [ "diamond4", "special1", // Special controls "special2", -] -export interface actionAssignments { // Based on the reference array +]; +export interface actionAssignments { + // Based on the reference array clicker1?: validInput; // What is used to click on things (enemies in game, UI elements) diamond1?: validInput; // Diamond controls diamond2?: validInput; @@ -34,20 +35,17 @@ export interface actionAssignments { // Based on the reference array special1?: validInput; // Special controls special2?: validInput; } -type action = keyof actionAssignments +type action = keyof actionAssignments; function isValidAction(value: string): value is keyof actionAssignments { return value in actionAssignmentsReference; // uh oh } -type actionBinding = [action, ((actionName?: string, state?: Enum.UserInputState, inputObject?: InputObject) => void)]; -type unknownTable = {[numberKey: number]: unknown, [stringKey: string]: unknown} -export function isUnknownTable(thing: unknown): thing is unknownTable { - return typeIs(thing, "table") -} +type actionBinding = [action, (actionName: string, state: Enum.UserInputState, inputObject: InputObject) => void]; -function getMouseLocation(filterDescendantsInstances: any[]): [Vector3, Vector3, Instance | undefined] { // May be unnecessary +function getMouseLocation(filterDescendantsInstances: Instance[]): [Vector3, Vector3, Instance | undefined] { + // May be unnecessary const hitParams = new RaycastParams(); hitParams.FilterType = Enum.RaycastFilterType.Blacklist; - hitParams.FilterDescendantsInstances = filterDescendantsInstances + hitParams.FilterDescendantsInstances = filterDescendantsInstances; const mouseLocation = UserInputService.GetMouseLocation(); const unitRay = CAMERA.ViewportPointToRay(mouseLocation.X, mouseLocation.Y); const cast = Workspace.Raycast(unitRay.Origin, unitRay.Direction.mul(1000), hitParams); @@ -58,10 +56,17 @@ function getMouseLocation(filterDescendantsInstances: any[]): [Vector3, Vector3, } } -export function translateInputState(state: any) { - if (enumTypeIs(state, Enum.UserInputState)) { - // + Translate to simple boolean +export function translateInputState(state: unknown) { + // if (enumTypeIs(state, Enum.UserInputState)) { + if (state === Enum.UserInputState.Begin) { + return true; + } else if (state === Enum.UserInputState.End) { + return false; + } else { + warn("Non-standard UserInputState"); + return false; } + // } } export interface actionBinder { @@ -70,25 +75,26 @@ export interface actionBinder { unbindFunctionsFromActions: (actions: action[]) => void; } -class actionHandler implements actionBinder { // + Needs a semaphore if concurrency issues arise +class actionHandler implements actionBinder { + // + Needs a semaphore if concurrency issues arise constructor() { // Fortnite } assignInputsToActions(actionAssignments: unknownTable) { - let newActionAssignments: actionAssignments = {} - actionAssignmentsReference.forEach(action => { - const input: unknown = actionAssignments[action] + const newActionAssignments: actionAssignments = {}; + actionAssignmentsReference.forEach((action) => { + const input: unknown = actionAssignments[action]; if (isValidAction(action) && isValidInput(input)) { - newActionAssignments[action] = input + newActionAssignments[action] = input; } - }) + }); } bindFunctionsToActions(actionBindings: actionBinding[]) { const actionAssignments = this.actionAssignments; const boundActions = this.boundActions; if (actionAssignments) { - actionBindings.forEach(actionBinding => { - const action = actionBinding[0] + actionBindings.forEach((actionBinding) => { + const action = actionBinding[0]; const input = actionAssignments[action]; if (!boundActions[action] && input) { boundActions[action] = true; @@ -96,24 +102,24 @@ class actionHandler implements actionBinder { // + Needs a semaphore if concurre } else { // ??? } - }); + }); } } unbindFunctionsFromActions(actions: action[]) { - const boundActions = this.boundActions - actions.forEach(action => { + const boundActions = this.boundActions; + actions.forEach((action) => { if (boundActions[action]) { boundActions[action] = undefined; ContextActionService.UnbindAction(action); } else { // ??? } - }) + }); } - + actionAssignments?: actionAssignments; - boundActions: {[action: string]: boolean | undefined} = {}; -} + boundActions: { [action: string]: boolean | undefined } = {}; +} export function makeActionBinder(): actionBinder { return new actionHandler(); @@ -128,4 +134,4 @@ function handleInput(input: InputObject, otherInteraction: boolean) { } */ -// UserInputService.InputBegan.Connect(handleInput); \ No newline at end of file +// UserInputService.InputBegan.Connect(handleInput); diff --git a/src/client/init.client.ts b/src/client/init.client.ts index 45f0beb..31f3ccb 100644 --- a/src/client/init.client.ts +++ b/src/client/init.client.ts @@ -1,10 +1,11 @@ // "init": The main client-side thread. const Players = game.GetService("Players"); const RunService = game.GetService("RunService"); +import { isUnknownTable } from "shared/Shared"; import { bindToServerMessage, messageServer } from "./ClientMessenger"; import { handleGuiInput, drawGui, closeGui } from "./GuiHandler"; import { makeEffectRunner, effectRunner } from "./EffectMaker"; -import { makeActionBinder, actionBinder, isUnknownTable } from "./InputHandler"; +import { makeActionBinder, actionBinder, translateInputState } from "./InputHandler"; const LOCALPLAYER = Players.LocalPlayer; const PLAYERGUI = LOCALPLAYER.WaitForChild("PlayerGui", 1) as PlayerGui; assert( @@ -19,34 +20,46 @@ function openMainMenu(playerGui: PlayerGui) { for (const mainMenuButton of mainMenuButtons) { mainMenuButton[0].Activated.Connect(function () { handleGuiInput(messageServer, mainMenuButton[1][0], mainMenuButton[1][1]); - }); + }); } } +function handlePlayerAction(action: string, state: Enum.UserInputState, inputObject: InputObject) { + messageServer("PlayerAction", [action, translateInputState(state)]); +} + +function handleGuiAction() {} + function handleServerMessage(messageType: unknown, messageContent: unknown) { if (messageType === "init") { openMainMenu(PLAYERGUI); inMainMenu = true; + mainActionBinder.bindFunctionsToActions([["clicker1", handleGuiAction]]); } else if (messageType === "enterGame") { closeGui(PLAYERGUI, "MainMenu"); inMainMenu = false; + mainActionBinder.unbindFunctionsFromActions(["clicker1"]); + mainActionBinder.bindFunctionsToActions([ + ["clicker1", handlePlayerAction], + ["special1", handlePlayerAction], + ["special2", handlePlayerAction], + ]); } else if (messageType === "bindActions") { if (isUnknownTable(messageContent)) { - mainActionBinder.assignInputsToActions(messageContent) + mainActionBinder.assignInputsToActions(messageContent); } } } // Bind functions - const effectRunners: effectRunner[] = []; // + Put stuff in the effectRunners table -RunService.RenderStepped.Connect(function(deltaTime) { - effectRunners.forEach(effectRunner => { - effectRunner.runEffects(deltaTime) +RunService.RenderStepped.Connect(function (deltaTime) { + effectRunners.forEach((effectRunner) => { + effectRunner.runEffects(deltaTime); }); -}) +}); -const mainActionBinder: actionBinder = makeActionBinder() +const mainActionBinder: actionBinder = makeActionBinder(); -bindToServerMessage(handleServerMessage); \ No newline at end of file +bindToServerMessage(handleServerMessage); diff --git a/src/server/ServerMessenger.ts b/src/server/ServerMessenger.ts index 5426043..9b1ba5c 100644 --- a/src/server/ServerMessenger.ts +++ b/src/server/ServerMessenger.ts @@ -1,4 +1,4 @@ -import { Input, Output } from "shared/Remotes"; +import { Input, Output } from "shared/Shared"; export function bindToClientMessage(functionToBind: Callback) { assert(Input?.IsA("RemoteEvent"), 'Remote event "Input" is of incorrect class or nil'); Input.OnServerEvent.Connect(functionToBind); diff --git a/src/server/main.server.ts b/src/server/main.server.ts index ce5f73d..12b9385 100644 --- a/src/server/main.server.ts +++ b/src/server/main.server.ts @@ -1,20 +1,21 @@ // "main": This is the core of reality. It serves as the highest-level abstraction. // + Prevent this from coupling with the entity manager, if possible const Players = game.GetService("Players"); -import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager"; +import { isUnknownTable } from "shared/Shared"; +import { makePlayerStorage, playerStorage, storedPlayer } from "shared/PlayerManager"; import { bindToClientMessage, messageClient, messageAllClients } from "./ServerMessenger"; -const playerStorage: playerStorage = makePlayerStorage(); +const mainPlayerStorage: playerStorage = makePlayerStorage(); function addPlayer(player: Player) { - playerStorage.initPlayer(player); + mainPlayerStorage.initPlayer(player); messageClient(player, "init", "idk"); } function removePlayer(player: Player) { - playerStorage.deinitPlayer(player); + mainPlayerStorage.deinitPlayer(player); } // Handling input is a complicated process that requires passing a large variety of data to a large variety of places, so it's staying here for now function handleClientMessage(player: Player, messageType: unknown, messageContent: unknown) { - const storedPlayer = playerStorage.fetchPlayer(player); + const storedPlayer = mainPlayerStorage.fetchPlayer(player); if (messageType === "EnterGame") { try { storedPlayer.loadIn(); @@ -27,9 +28,17 @@ function handleClientMessage(player: Player, messageType: unknown, messageConten messageClient(player, "promptError", errorMessage); } } + } else if (messageType === "PlayerAction") { + if (isUnknownTable(messageContent)) { + const action = messageContent[0]; + const state = messageContent[1]; + if (typeIs(action, "string") && typeIs(state, "boolean")) { + storedPlayer.ability(action, state); + } + } } else if (messageType === "move") { if (typeIs(messageContent, "Vector3")) { - storedPlayer.setPosition(messageContent) + storedPlayer.setPosition(messageContent); } } } diff --git a/src/services.d.ts b/src/services.d.ts index fa90d7f..6cffac0 100644 --- a/src/services.d.ts +++ b/src/services.d.ts @@ -9,9 +9,7 @@ type effectState = [CFrame, Vector3, Color3, number]; // The number is transpare type effectEntry = [meshType, EnumItem, effectState[]]; // The enumitem is material */ -// Genuinely require being on both sides - but in the services file? No shot! - - +type unknownTable = { [numberKey: number]: unknown; [stringKey: string]: unknown }; /*interface hookInEntry { name: string; guiObject: GuiObject; diff --git a/src/shared/AbilityManager.ts b/src/shared/AbilityManager.ts new file mode 100644 index 0000000..9c1d3b4 --- /dev/null +++ b/src/shared/AbilityManager.ts @@ -0,0 +1,4 @@ +export interface ability { + use: () => void; // ? Does it pass back information to the entity manager that it does something with? Is an ability really just a transform of entities? + // ... this transform would need the positions and stats of the entities involved as well as potential obstacles +} diff --git a/src/shared/EntityManager.ts b/src/shared/EntityManager.ts index f15361a..dd6a7e9 100644 --- a/src/shared/EntityManager.ts +++ b/src/shared/EntityManager.ts @@ -1,11 +1,13 @@ // "EntityManager": Create entities objects and deck them out with functions to use. // + Functions are here, as to avoid storing unecessary data in the server store. import { makePuppet, puppet, puppetEntry } from "./Puppetmaster"; -type stats = [maxHealth: number, attack: number, speed: number, defense: number] // values used for calculation, only modified by buffs and debuffs -type amounts = [health: number, barrier: number] // values used to store an entity's current status (more existential than stats) +import { ability } from "./AbilityManager"; +type stats = [maxHealth: number, attack: number, speed: number, defense: number]; // values used for calculation, only modified by buffs and debuffs +type amounts = [health: number, barrier: number]; // values used to store an entity's current status (more existential than stats) export interface entity { setPosition: (location: Vector3) => void; + ability: (ability: string, state: boolean) => void; } class entityHandler implements entity { @@ -13,15 +15,37 @@ class entityHandler implements entity { this.baseStats = baseStats; this.baseAmounts = baseAmounts; this.puppet = makePuppet(puppetEntry); - }; + } setPosition(location: Vector3) { this.puppet.movePuppet(location); } + ability(abilityName: string, activated: boolean) { + const abilities = this.abilities; + if (abilities) { + if (activated) { + const ability = abilities[abilityName]; + if (ability !== undefined) { + ability.use(); + } + } else { + // + Ability cancellation - perhaps the useAbility inside the entity returns a function to store that the ability watches that ceases the ability when executed + } + } + // Blah blah blah + } + baseStats: stats; - baseAmounts: amounts; // Health, Barrier (things of indescribable importance) + baseAmounts: amounts; puppet: puppet; + abilities?: { + [key: string]: ability; + }; } -export function makeEntity(puppetEntry: puppetEntry, baseStats = [100, 1, 16, 0] as stats, baseAmounts = [100, 0] as amounts) { +export function makeEntity( + puppetEntry: puppetEntry, + baseStats = [100, 1, 16, 0] as stats, + baseAmounts = [100, 0] as amounts, +) { return new entityHandler(baseStats, baseAmounts, puppetEntry); } diff --git a/src/shared/PlayerManager.ts b/src/shared/PlayerManager.ts index 4c61b3a..3651edf 100644 --- a/src/shared/PlayerManager.ts +++ b/src/shared/PlayerManager.ts @@ -1,12 +1,14 @@ // "PlayerManager": Handle the data of players. This involves receiving them when they arrive, cleaning up after they exit, teleporting them, etc. import { makeEntity, entity } from "./EntityManager"; -interface saveDataEntry { // + May need to move this to archiver +interface saveDataEntry { + // + May need to move this to archiver placeholder: string; } export interface storedPlayer { teleportToServer: () => void; setPosition: (location: Vector3) => void; + ability: (ability: string, state: boolean) => void; loadIn: () => void; } @@ -14,25 +16,30 @@ class storedPlayerHandler implements storedPlayer { constructor(player: Player) { this.player = player; this.inMainMenu = true; - this.saveData = {placeholder: "fortnite"}; + this.saveData = { placeholder: "fortnite" }; } teleportToServer() { // + Do checking related to where the player is allowed to go // + Teleport player to other server, sending a message to have them load in automatically - }; + } setPosition(location: Vector3) { if (this.entity) { this.entity.setPosition(location); } - }; + } + ability(ability: string, state: boolean) { + if (this.entity) { + this.entity.ability(ability, state); + } + } loadIn() { this.entity = makeEntity(["Character", this.player]); // + Give the entity the stats it's supposed to have, load from save data maybe? - }; + } player: Player; inMainMenu: boolean; // + Other data that is unique to players but does not persist between sessions - saveData: saveDataEntry; + saveData: saveDataEntry; // This gets synced with the actual datastore entity?: entity; } @@ -43,18 +50,18 @@ export interface playerStorage { } class playerStorageHandler implements playerStorage { - constructor() {}; + constructor() {} initPlayer(player: Player) { this.playerStorageArray[player.UserId] = new storedPlayerHandler(player); // + Load player's datastore into server store - }; + } deinitPlayer(player: Player) { const entry = this.playerStorageArray[player.UserId]; assert(entry, "Trying to remove entry of player " + player.DisplayName + ", but entry does not exist!"); // ? Tell the entity to unload, if it still exists (the entity will tell the other clients to remove the player) // + Unload player's server store to datastores return undefined; // A nil entry to replace the entry to be wiped and maybe a success value in a wrapper - }; + } fetchPlayer(player: Player) { return this.playerStorageArray[player.UserId]; } @@ -63,4 +70,4 @@ class playerStorageHandler implements playerStorage { export function makePlayerStorage() { return new playerStorageHandler(); -} \ No newline at end of file +} diff --git a/src/shared/Remotes.ts b/src/shared/Shared.ts similarity index 66% rename from src/shared/Remotes.ts rename to src/shared/Shared.ts index 6f69c8d..b334761 100644 --- a/src/shared/Remotes.ts +++ b/src/shared/Shared.ts @@ -2,3 +2,7 @@ const ReplicatedStorage = game.GetService("ReplicatedStorage"); export const Input = ReplicatedStorage.WaitForChild("Input", 1); export const Output = ReplicatedStorage.WaitForChild("Output", 1); + +export function isUnknownTable(thing: unknown): thing is unknownTable { + return typeIs(thing, "table"); +}