first commit
This commit is contained in:
commit
5302ecc6cc
43 changed files with 2530 additions and 0 deletions
21
src/ServerScriptService/ecs/systems/server/damageHurts.ts
Normal file
21
src/ServerScriptService/ecs/systems/server/damageHurts.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { World } from "@rbxts/matter"
|
||||
import { Damage, Health } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* A system that reduces health when Damage components are applied.
|
||||
*
|
||||
* This only adjusts any attached Health component.
|
||||
*/
|
||||
function damageHurts(world: World): void {
|
||||
for (const [id, health, damage] of world.query(Health, Damage)) {
|
||||
world.insert(
|
||||
id,
|
||||
health.patch({
|
||||
health: health.health - damage.damage
|
||||
})
|
||||
)
|
||||
world.remove(id, Damage)
|
||||
}
|
||||
}
|
||||
|
||||
export = damageHurts
|
37
src/ServerScriptService/ecs/systems/server/healthKills.ts
Normal file
37
src/ServerScriptService/ecs/systems/server/healthKills.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { World } from "@rbxts/matter"
|
||||
import { Health, Model } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* A system that mirrors health to Humanoids.
|
||||
*
|
||||
* If a health component has an attached model that contains a humanoid. This
|
||||
* will reflect the changes in health to that humanoid.
|
||||
*
|
||||
* If the health is out of bounds between 0 and the max health it is clamped
|
||||
* between these values.
|
||||
*/
|
||||
function healthKills(world: World): void {
|
||||
for (const [id, record] of world.queryChanged(Health)) {
|
||||
if (!record.new) continue
|
||||
const health = math.clamp(record.new.health, 0, record.new.maxHealth)
|
||||
|
||||
if (health !== record.new.health) {
|
||||
world.insert(
|
||||
id,
|
||||
record.new.patch({
|
||||
health: health
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const model = world.get(id, Model)
|
||||
if (!model) continue
|
||||
const humanoid = model.model?.FindFirstChildOfClass("Humanoid")
|
||||
if (humanoid) {
|
||||
humanoid.MaxHealth = record.new.maxHealth
|
||||
humanoid.Health = health
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export = healthKills
|
|
@ -0,0 +1,24 @@
|
|||
import { useThrottle, World } from "@rbxts/matter"
|
||||
import { Health } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
function healthRegenerates(world: World): void {
|
||||
for (const [id, health] of world.query(Health)) {
|
||||
if (health.health <= 0) continue
|
||||
|
||||
const newHealth = math.clamp(health.health + health.regeneration, 0, health.maxHealth)
|
||||
|
||||
if (useThrottle(1)) {
|
||||
world.insert(
|
||||
id,
|
||||
health.patch({
|
||||
health: newHealth
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export = healthRegenerates
|
|
@ -0,0 +1,33 @@
|
|||
import { Players } from "@rbxts/services"
|
||||
import { World, useEvent } from "@rbxts/matter"
|
||||
import { Health, Model, PlayerCharacter } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
function playersArePlayerCharacters(world: World): void {
|
||||
Players.GetPlayers().forEach((player, _) => {
|
||||
for (const [_, character] of useEvent(player, "CharacterAdded")) {
|
||||
world.spawn(
|
||||
Model({
|
||||
model: character
|
||||
}),
|
||||
PlayerCharacter({
|
||||
player: Players.GetPlayerFromCharacter(character) as Player,
|
||||
humanoid: character.WaitForChild("Humanoid") as Humanoid
|
||||
}),
|
||||
Health({
|
||||
health: 80,
|
||||
maxHealth: 100,
|
||||
regeneration: 0.25
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
for (const [id] of world.query(PlayerCharacter).without(Model)) {
|
||||
world.despawn(id)
|
||||
}
|
||||
}
|
||||
|
||||
export = playersArePlayerCharacters
|
|
@ -0,0 +1,41 @@
|
|||
import { World, useEvent } from "@rbxts/matter"
|
||||
import { Model, PlayerCharacter } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* @todo
|
||||
*/
|
||||
function playersRagdollOnDeath(world: World): void {
|
||||
for (const [_, playerCharacter, model] of world.query(PlayerCharacter, Model)) {
|
||||
if (!model.model) continue
|
||||
|
||||
playerCharacter.humanoid.BreakJointsOnDeath = false
|
||||
|
||||
for (const [_] of useEvent(playerCharacter.humanoid, "Died")) {
|
||||
model.model.GetDescendants().forEach((v, _) => {
|
||||
if (v.IsA("Motor6D")) {
|
||||
const attachment0 = new Instance("Attachment")
|
||||
const attachment1 = new Instance("Attachment")
|
||||
attachment0.CFrame = v.C0
|
||||
attachment1.CFrame = v.C1
|
||||
attachment0.Parent = v.Part0
|
||||
attachment1.Parent = v.Part1
|
||||
|
||||
const ballSocketConstraint = new Instance("BallSocketConstraint")
|
||||
ballSocketConstraint.Attachment0 = attachment0
|
||||
ballSocketConstraint.Attachment1 = attachment1
|
||||
ballSocketConstraint.LimitsEnabled = true
|
||||
ballSocketConstraint.TwistLimitsEnabled = true
|
||||
ballSocketConstraint.Parent = v.Parent
|
||||
|
||||
v.Destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id] of world.query(PlayerCharacter).without(Model)) {
|
||||
world.despawn(id)
|
||||
}
|
||||
}
|
||||
|
||||
export = playersRagdollOnDeath
|
|
@ -0,0 +1,30 @@
|
|||
import { useEvent, World } from "@rbxts/matter"
|
||||
import { Model } from "ReplicatedStorage/ecs/components"
|
||||
|
||||
/**
|
||||
* A system that removes missing {@link Model | Models}.
|
||||
*
|
||||
* If a model is removed from the game, this system will remove the
|
||||
* corresponding model from the world.
|
||||
*
|
||||
* If a model is removed from the world, this system will remove the
|
||||
* corresponding model from the game.
|
||||
*/
|
||||
function removeMissingModels(world: World): void {
|
||||
for (const [id, model] of world.query(Model)) {
|
||||
if (!model.model) continue
|
||||
for (const _ of useEvent(model.model, "AncestryChanged")) {
|
||||
if (!model.model.IsDescendantOf(game)) {
|
||||
world.remove(id, Model)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, record] of world.queryChanged(Model)) {
|
||||
if (record.new) continue
|
||||
record.old?.model?.Destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export = removeMissingModels
|
83
src/ServerScriptService/ecs/systems/server/replication.ts
Normal file
83
src/ServerScriptService/ecs/systems/server/replication.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { useEvent, World } from "@rbxts/matter"
|
||||
import { Players } from "@rbxts/services"
|
||||
import * as Components from "ReplicatedStorage/ecs/components"
|
||||
import { getEvent } from "ReplicatedStorage/remotes"
|
||||
|
||||
type ComponentName = keyof typeof Components
|
||||
type ComponentConstructor = (typeof Components)[ComponentName]
|
||||
|
||||
const REPLICATED_COMPONENT_NAMES: readonly ComponentName[] = ["Model", "Health"]
|
||||
|
||||
const replicatedComponents: ReadonlySet<ComponentConstructor> = REPLICATED_COMPONENT_NAMES.reduce(
|
||||
(set: Set<ComponentConstructor>, name: ComponentName) => {
|
||||
return set.add(Components[name])
|
||||
},
|
||||
new Set()
|
||||
)
|
||||
|
||||
getEvent("EcsReplication")
|
||||
|
||||
function replication(world: World): void {
|
||||
const replicationEvent = getEvent("EcsReplication")
|
||||
|
||||
let payload: Map<string, Map<ComponentName, { data?: Components.GooplerComponent }>> | undefined
|
||||
for (const [_, player] of useEvent(Players, "PlayerAdded")) {
|
||||
if (!payload) {
|
||||
payload = new Map()
|
||||
|
||||
for (const [id, entityData] of world) {
|
||||
const entityPayload: Map<ComponentName, { data?: Components.GooplerComponent }> =
|
||||
new Map()
|
||||
payload.set(tostring(id), entityPayload)
|
||||
|
||||
for (const [component, componentData] of entityData) {
|
||||
if (replicatedComponents.has(component)) {
|
||||
// Here we are certain that the component has the name of one of our
|
||||
// components because it exists in our set of components.
|
||||
entityPayload.set(tostring(component) as ComponentName, { data: componentData })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print("Sending initial payload to", player)
|
||||
replicationEvent.FireClient(player, payload)
|
||||
}
|
||||
|
||||
const changes: Map<
|
||||
string,
|
||||
Map<ComponentName, { data?: Components.GooplerComponent }>
|
||||
> = new Map()
|
||||
for (const component of replicatedComponents) {
|
||||
// Here we are certain that the component has the name of one of our
|
||||
// components since it came from our set.
|
||||
const name = tostring(component) as ComponentName
|
||||
|
||||
for (const [id, record] of world.queryChanged(component)) {
|
||||
const key = tostring(id)
|
||||
|
||||
if (!changes.has(key)) {
|
||||
changes.set(key, new Map())
|
||||
}
|
||||
|
||||
if (world.contains(id)) {
|
||||
changes.get(key)?.set(name, { data: record.new })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!changes.isEmpty()) {
|
||||
replicationEvent.FireAllClients(changes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A system that replicates all replicated components to the client.
|
||||
*
|
||||
* This system runs after after all other systems to ensure any changes made are
|
||||
* included.
|
||||
*/
|
||||
export = {
|
||||
system: replication,
|
||||
priority: math.huge
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { World } from "@rbxts/matter"
|
||||
import { Workspace } from "@rbxts/services"
|
||||
import { Model, Transform } from "ReplicatedStorage/ecs/components"
|
||||
import removeMissingModels from "./removeMissingModels"
|
||||
|
||||
function updateTransforms(world: World): void {
|
||||
for (const [id, record] of world.queryChanged(Transform)) {
|
||||
if (!world.contains(id)) continue
|
||||
const model = world.get(id, Model)
|
||||
if (!model || !record.new || record.new._doNotReconcile) continue
|
||||
model.model?.PivotTo(record.new.cframe)
|
||||
}
|
||||
|
||||
for (const [id, record] of world.queryChanged(Model)) {
|
||||
if (!world.contains(id)) continue
|
||||
const transform = world.get(id, Transform)
|
||||
if (!transform) continue
|
||||
record.new?.model?.PivotTo(transform.cframe)
|
||||
}
|
||||
|
||||
for (const [id, model, transform] of world.query(Model, Transform)) {
|
||||
if (!model.model) continue
|
||||
|
||||
let primaryPart: BasePart
|
||||
if (model.model.IsA("Model")) {
|
||||
if (!model.model.PrimaryPart) continue
|
||||
primaryPart = model.model.PrimaryPart
|
||||
} else if (model.model.IsA("BasePart")) {
|
||||
primaryPart = model.model
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
if (primaryPart.Anchored) continue
|
||||
|
||||
if (transform.cframe.Y < Workspace.FallenPartsDestroyHeight) {
|
||||
world.despawn(id)
|
||||
continue
|
||||
}
|
||||
|
||||
if (transform.cframe !== primaryPart.CFrame) {
|
||||
world.insert(
|
||||
id,
|
||||
Transform({
|
||||
cframe: primaryPart.CFrame,
|
||||
_doNotReconcile: true
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A system that updates {@link Transform | Transforms}.
|
||||
*
|
||||
* If a Transform is updated, the corresponding {@link Model} is updated to
|
||||
* match the Transform.
|
||||
*
|
||||
* If a Model is updated, the new referenced instance is updated to match the
|
||||
* Transform.
|
||||
*
|
||||
* If an non-anchored model moves, the Transform is updated to match the updated
|
||||
* instance transform.
|
||||
*
|
||||
* This system runs after {@link removeMissingModels} to ensure updates aren't
|
||||
* performed unnecessarily.
|
||||
*/
|
||||
export = {
|
||||
system: updateTransforms,
|
||||
after: [removeMissingModels]
|
||||
}
|
13
src/ServerScriptService/main.server.ts
Normal file
13
src/ServerScriptService/main.server.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { start } from "ReplicatedStorage/ecs"
|
||||
import { Host } from "ReplicatedStorage/hosts"
|
||||
import { setEnvironment } from "ReplicatedStorage/idAttribute"
|
||||
import { getEvent } from "ReplicatedStorage/remotes"
|
||||
|
||||
const HOST = Host.Server
|
||||
|
||||
// We only do this here at the moment to create a dummy event for replication.
|
||||
// In the future this will be created by the replication system.
|
||||
getEvent("EcsReplication")
|
||||
|
||||
setEnvironment(HOST)
|
||||
start(HOST)
|
Loading…
Add table
Add a link
Reference in a new issue