first commit

This commit is contained in:
Reid 2023-07-18 22:22:52 -07:00
commit 5302ecc6cc
43 changed files with 2530 additions and 0 deletions

View 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

View 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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View 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)