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
This commit is contained in:
loplkc loplkc 2022-02-27 21:28:11 -05:00
parent 1564575a7f
commit 846b86f74e
11 changed files with 164 additions and 86 deletions

View file

@ -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);

View file

@ -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

View file

@ -7,14 +7,14 @@ assert(CAMERA, 'Camera of "' + Players.LocalPlayer.DisplayName + '"does not exis
function enumTypeIs<EnumAsType>(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<Enum.KeyCode>(value, Enum.KeyCode)
return enumTypeIs<Enum.KeyCode>(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<Enum.UserInputState>(state, Enum.UserInputState)) {
// + Translate to simple boolean
export function translateInputState(state: unknown) {
// if (enumTypeIs<Enum.UserInputState>(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;
@ -100,19 +106,19 @@ class actionHandler implements actionBinder { // + Needs a semaphore if concurre
}
}
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 {

View file

@ -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(
@ -23,30 +24,42 @@ function openMainMenu(playerGui: PlayerGui) {
}
}
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);

View file

@ -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);

View file

@ -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);
}
}
}

4
src/services.d.ts vendored
View file

@ -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;

View file

@ -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
}

View file

@ -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);
}

View file

@ -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];
}

View file

@ -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");
}