Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified src/StarterCharacter.rbxm
Binary file not shown.
146 changes: 146 additions & 0 deletions src/client/controllers/animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Controller, OnRender, OnInit } from "@flamework/core";
import { Canim, CanimPose, CanimTrack } from "@rbxts/canim";
import { OnCharacterAdded } from "./core";
import { Viewmodel } from "client/types/items";
import { configs, ItemConfig } from "shared/configurations/items";
import { RunService } from "@rbxts/services";
import getObjectSize from "shared/getObjectSize";
import { log } from "shared/log_message";
import { Shobnode } from "client/shobnode";

Shobnode.setup();

export type CanimTracks = CanimPose | CanimTrack;
interface Tracks {
viewmodel: { [animationKey in string]: CanimTracks | undefined };
character: { [animationKey in string]: CanimTracks | undefined };
}

@Controller({})
export class Animation implements OnRender, OnCharacterAdded, OnInit {
private viewmodelAnimator = new Canim();
private characterAnimator = new Canim();

private typeLookup: { [animationKey: string]: "Animation" | "Pose" } = {};
private tracks: Tracks = {
viewmodel: {},
character: {},
};

playAnimation = (animationKey: string): CanimTracks | undefined => {
const animationType = this.typeLookup[animationKey];
if (!animationType) {
throw `Couldn't find type for given animationKey (animationKey: ${animationKey})`;
}

print(this.viewmodelAnimator.identified_bones);
print(this.characterAnimator.identified_bones);

const viewmodelTrack =
(animationType === "Animation" ? this.viewmodelAnimator.play_animation(animationKey) : this.viewmodelAnimator.play_pose(animationKey)) || undefined;
const characterTrack =
(animationType === "Animation" ? this.characterAnimator.play_animation(animationKey) : this.characterAnimator.play_pose(animationKey)) || undefined;

return viewmodelTrack;
};

stopAnimation = (animationKey: string): void => {
this.viewmodelAnimator.stop_animation(animationKey);
this.characterAnimator.stop_animation(animationKey);
};

Comment on lines +30 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Store running tracks so getAnimationTrack works.
We never populate this.tracks, so getAnimationTrack always returns undefined. Persist the tracks when playing and clear them when stopping.

Apply this diff:

 		const viewmodelTrack =
 			(animationType === "Animation" ? this.viewmodelAnimator.play_animation(animationKey) : this.viewmodelAnimator.play_pose(animationKey)) || undefined;
 		const characterTrack =
 			(animationType === "Animation" ? this.characterAnimator.play_animation(animationKey) : this.characterAnimator.play_pose(animationKey)) || undefined;
 
+		this.tracks.viewmodel[animationKey] = viewmodelTrack;
+		this.tracks.character[animationKey] = characterTrack;
+
 		return viewmodelTrack;
 	};
 
 	stopAnimation = (animationKey: string): void => {
 		this.viewmodelAnimator.stop_animation(animationKey);
 		this.characterAnimator.stop_animation(animationKey);
+		this.tracks.viewmodel[animationKey] = undefined;
+		this.tracks.character[animationKey] = undefined;
 	};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
playAnimation = (animationKey: string): CanimTracks | undefined => {
const animationType = this.typeLookup[animationKey];
if (!animationType) {
throw `Couldn't find type for given animationKey (animationKey: ${animationKey})`;
}
print(this.viewmodelAnimator.identified_bones);
print(this.characterAnimator.identified_bones);
const viewmodelTrack =
(animationType === "Animation" ? this.viewmodelAnimator.play_animation(animationKey) : this.viewmodelAnimator.play_pose(animationKey)) || undefined;
const characterTrack =
(animationType === "Animation" ? this.characterAnimator.play_animation(animationKey) : this.characterAnimator.play_pose(animationKey)) || undefined;
return viewmodelTrack;
};
stopAnimation = (animationKey: string): void => {
this.viewmodelAnimator.stop_animation(animationKey);
this.characterAnimator.stop_animation(animationKey);
};
playAnimation = (animationKey: string): CanimTracks | undefined => {
const animationType = this.typeLookup[animationKey];
if (!animationType) {
throw `Couldn't find type for given animationKey (animationKey: ${animationKey})`;
}
print(this.viewmodelAnimator.identified_bones);
print(this.characterAnimator.identified_bones);
const viewmodelTrack =
(animationType === "Animation" ? this.viewmodelAnimator.play_animation(animationKey) : this.viewmodelAnimator.play_pose(animationKey)) || undefined;
const characterTrack =
(animationType === "Animation" ? this.characterAnimator.play_animation(animationKey) : this.characterAnimator.play_pose(animationKey)) || undefined;
this.tracks.viewmodel[animationKey] = viewmodelTrack;
this.tracks.character[animationKey] = characterTrack;
return viewmodelTrack;
};
stopAnimation = (animationKey: string): void => {
this.viewmodelAnimator.stop_animation(animationKey);
this.characterAnimator.stop_animation(animationKey);
this.tracks.viewmodel[animationKey] = undefined;
this.tracks.character[animationKey] = undefined;
};
🤖 Prompt for AI Agents
In src/client/controllers/animation.ts around lines 30 to 51, playAnimation
currently returns only the viewmodel track and never persists running tracks, so
getAnimationTrack always returns undefined; update playAnimation to save the
produced viewmodelTrack and characterTrack into this.tracks (keyed by
animationKey) after creating them, and update stopAnimation to remove/clear the
entry for animationKey (and still call stop_animation on both animators) so
tracks are persisted while running and cleared when stopped.

getAnimationTrack = (
animationKey: string,
scope: "Character" | "Viewmodel" | "Both" = "Both",
): CanimTracks | undefined | [character: CanimTracks | undefined, viewmodel: CanimTracks | undefined] => {
const characterAnimationTrack = this.tracks.character[animationKey];
const viewmodelAnimationTrack = this.tracks.viewmodel[animationKey];

if (scope === "Character") return characterAnimationTrack;
if (scope === "Viewmodel") return viewmodelAnimationTrack;
if (scope === "Both") return [characterAnimationTrack, viewmodelAnimationTrack];
};

assignViewmodel = (viewmodel: Viewmodel) => {
this.viewmodelAnimator.assign_model(viewmodel);
};

onCharacterAdded(character: Model): void {
this.characterAnimator.assign_model(character);
}

onInit(): void {
const globalChecksum = {
current: 0,
target: 0,
};

configs.forEach((itemConfig: ItemConfig, itemName: string) => {
const amountOfAnimations = getObjectSize(itemConfig.animations);
globalChecksum.target += amountOfAnimations;

task.spawn(() => {
const loadedTracks: { [animationName in string]: { track: CanimTracks; trackType: "Animation" | "Pose"; rebased: boolean } } = {};

for (const [animationName, animationProperties] of pairs(itemConfig.animations)) {
const key = `${itemName}/${animationName}`;
const id = `rbxassetid://${animationProperties.id}`;
const animationType = animationProperties.type;
const priority = animationProperties.priority;
const looped = animationProperties.looped || false;
const rebased = animationProperties.rebased ? animationProperties.rebased : true;
const fadeTime = animationProperties.fadeTime !== undefined ? animationProperties.fadeTime : 0;
const weights = animationProperties.weights;

let animation: CanimTrack | CanimPose;

if (animationType === "Animation") {
animation = this.viewmodelAnimator.load_animation(key, priority, id);
this.characterAnimator.load_animation(key, priority, id);
} else {
animation = this.viewmodelAnimator.load_pose(key, priority, id);
this.characterAnimator.load_pose(key, priority, id);
}

animation.finished_loading.Wait();
globalChecksum.current += 1;

animation.looped = looped;
// eslint-disable-next-line camelcase
animation.fade_time = fadeTime;

if (weights !== undefined) {
for (const [bone, weight] of pairs(weights)) {
animation.bone_weights[bone] = weight;
}
}

loadedTracks[animationName] = { track: animation, trackType: animationType, rebased };
Comment on lines +95 to +118
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Configure character tracks the same as viewmodel tracks.
We drop the track returned by characterAnimator.load_*, so the character-side animations never get their looped, fade_time, or bone_weights configured and may still be loading when we mark the checksum complete. Character idles/runs will therefore stop immediately or blend incorrectly.

Apply this diff:

-					let animation: CanimTrack | CanimPose;
-
-					if (animationType === "Animation") {
-						animation = this.viewmodelAnimator.load_animation(key, priority, id);
-						this.characterAnimator.load_animation(key, priority, id);
-					} else {
-						animation = this.viewmodelAnimator.load_pose(key, priority, id);
-						this.characterAnimator.load_pose(key, priority, id);
-					}
-
-					animation.finished_loading.Wait();
+					let viewmodelAnimation: CanimTracks;
+					let characterAnimation: CanimTracks;
+
+					if (animationType === "Animation") {
+						viewmodelAnimation = this.viewmodelAnimator.load_animation(key, priority, id);
+						characterAnimation = this.characterAnimator.load_animation(key, priority, id);
+					} else {
+						viewmodelAnimation = this.viewmodelAnimator.load_pose(key, priority, id);
+						characterAnimation = this.characterAnimator.load_pose(key, priority, id);
+					}
+
+					viewmodelAnimation.finished_loading.Wait();
+					characterAnimation.finished_loading.Wait();
 					globalChecksum.current += 1;
 
-					animation.looped = looped;
-					// eslint-disable-next-line camelcase
-					animation.fade_time = fadeTime;
+					viewmodelAnimation.looped = looped;
+					characterAnimation.looped = looped;
+					// eslint-disable-next-line camelcase
+					viewmodelAnimation.fade_time = fadeTime;
+					// eslint-disable-next-line camelcase
+					characterAnimation.fade_time = fadeTime;
 
 					if (weights !== undefined) {
 						for (const [bone, weight] of pairs(weights)) {
-							animation.bone_weights[bone] = weight;
+							viewmodelAnimation.bone_weights[bone] = weight;
+							characterAnimation.bone_weights[bone] = weight;
 						}
 					}
 
-					loadedTracks[animationName] = { track: animation, trackType: animationType, rebased };
+					loadedTracks[animationName] = { track: viewmodelAnimation, trackType: animationType, rebased };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/client/controllers/animation.ts around lines 95-118, the code only
captures and configures the viewmodel animation while calling
characterAnimator.load_* without keeping its return value, so the character
track never gets looped, fade_time or bone_weights configured and may still be
loading when the checksum is bumped; fix this by capturing the character
animation into a separate variable (e.g. characterAnimation) from
characterAnimator.load_animation/load_pose, wait on both
animation.finished_loading and characterAnimation.finished_loading before
incrementing globalChecksum.current, and apply looped, fade_time and any
bone_weights to both the viewmodel animation and the characterAnimation (and
store/configure both as needed).

this.typeLookup[key] = animationType;

log("verbose", `(${globalChecksum.current}/${globalChecksum.target}) animation ${animationName} for item ${itemName} sucesfully loaded`);
}

// const idle = loadedTracks.idle;
// if (idle) {
// const idleTrack = idle.track as CanimPose;
// for (const [animationName, { track, trackType, rebased }] of pairs(loadedTracks)) {
// print(`${animationName} rebased: ${rebased}`);
// if (trackType !== "Animation" || !rebased) continue;
// // eslint-disable-next-line camelcase
// (track as CanimTrack).rebase_target = idleTrack;
// }
// }
});
});

while (globalChecksum.current < globalChecksum.target) RunService.Heartbeat.Wait();
}

onRender(dt: number): void {
this.viewmodelAnimator.update(dt);
this.characterAnimator.update(dt);

Shobnode.display_node(1231251241, new UDim2(0, 0, 0.8, 0), this.viewmodelAnimator.debug);
}
}
57 changes: 54 additions & 3 deletions src/client/controllers/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Controller, OnStart } from "@flamework/core";
import { OnPreCameraRender, OnPostCameraRender, OnCameraRender } from "./core";
import { OnCharacterAdded } from "./core";
import { Players, UserInputService, Workspace } from "@rbxts/services";
import { lerp } from "shared/utilities/number_utility";
import { inverseLerp, lerp } from "shared/utilities/number_utility";
import { log } from "shared/log_message";

const userGameSettings = UserSettings().GetService("UserGameSettings");

type Modifiers = { [modifierName in string]: Modifier | undefined };
type FOVModifiers = { [modifierName in string]: FOVModifier | undefined };
type SensitivityModifiers = { [modifierName in string]: SensitivityModifier | undefined };

export class Modifier {
static modifiers: Modifiers = {};
Expand Down Expand Up @@ -71,6 +72,56 @@ export class Modifier {
};
}

export class SensitivityModifier {
static modifiers: SensitivityModifiers = {};

static create = (name: string): SensitivityModifier => {
if (!SensitivityModifier.modifiers[name]) SensitivityModifier.modifiers[name] = new SensitivityModifier(name);
return SensitivityModifier.modifiers[name] as SensitivityModifier;
};

static getMultiplier = (): number => {
let lowestPercentage = 100;

for (const [_, modifierObject] of pairs(SensitivityModifier.modifiers)) {
const modifierPercent = modifierObject.getPercent();
if (modifierPercent < lowestPercentage) lowestPercentage = modifierPercent;
}

if (lowestPercentage > 100 || lowestPercentage < 0) throw `Incorrect percentage value!`;

return lowestPercentage / 100;
};

private percent = 100;
private destroyed = false;

private constructor(private name: string) {}

public getPercent = (): number => {
if (this.destroyed) throw `Attempt to get percent of modifier after it was destroyed`;

return this.percent;
};

public setPercent = (newPercent: number) => {
if (this.destroyed) throw `Attempt to set percent of modifier after it was destroyed`;

if (newPercent > 100 || newPercent < 0) {
log("warning", `Attempt to set an incorrect percentage for modifier ${this.name}`);
return;
}

this.percent = newPercent;
};

public destroy = (): void => {
this.setPercent(0);
SensitivityModifier.modifiers[this.name] = undefined;
this.destroyed = true;
};
}

export class FOVModifier {
static modifiers: FOVModifiers = {};

Expand Down Expand Up @@ -198,8 +249,8 @@ export class Camera implements OnPreCameraRender, OnPostCameraRender, OnCameraRe
const mouseSensitivity = userGameSettings.MouseSensitivity;
userGameSettings.RotationType = Enum.RotationType.CameraRelative;

this.rotationAngles.x += math.rad(correctedMouseDelta.X * mouseSensitivity * -1);
this.rotationAngles.y += math.rad(correctedMouseDelta.Y * mouseSensitivity * -1);
this.rotationAngles.x += math.rad(correctedMouseDelta.X * SensitivityModifier.getMultiplier() * mouseSensitivity * -1);
this.rotationAngles.y += math.rad(correctedMouseDelta.Y * SensitivityModifier.getMultiplier() * mouseSensitivity * -1);
this.rotationAngles.y = math.clamp(this.rotationAngles.y, math.rad(-75), math.rad(75));
}

Expand Down
5 changes: 3 additions & 2 deletions src/client/controllers/freelook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { Camera, Modifier } from "./camera";
import { Input } from "./input";
import { UserInputService } from "@rbxts/services";
import { OnCharacterAdded } from "./core";
import { smoothClamp } from "shared/utilities/number_utility";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused import.

The smoothClamp import is added but never used in the code.

Apply this diff to remove the unused import:

-import { smoothClamp } from "shared/utilities/number_utility";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { smoothClamp } from "shared/utilities/number_utility";
🤖 Prompt for AI Agents
In src/client/controllers/freelook.ts around line 6, the file imports
smoothClamp from "shared/utilities/number_utility" but never uses it; remove
that unused import from the import list so the file no longer imports
smoothClamp (and reformat the remaining imports if necessary).


@Controller({})
export class Freelook implements OnStart, OnTick, OnCharacterAdded {
static xAxisMax = 1;
static yAxisMax = 1;
static yAxisMax = 0.4;

private freelookModifier = Modifier.create("freelook", false, 2.5);
private freelookActive = false;
Expand All @@ -30,7 +31,7 @@ export class Freelook implements OnStart, OnTick, OnCharacterAdded {

onTick(dt: number): void {
if (this.freelookActive) {
const mouseDelta = UserInputService.GetMouseDelta().div(300);
const mouseDelta = UserInputService.GetMouseDelta().div(270);
this.freelookOffset = this.freelookOffset.add(mouseDelta.mul(-1));
} else {
this.freelookOffset = this.freelookOffset.Lerp(Vector2.zero, 0.1);
Expand Down
17 changes: 15 additions & 2 deletions src/client/controllers/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export class Items implements OnInit, OnStart, OnRender, OnCharacterAdded, OnRun
private currentItemSlot: number | undefined;
private character: Model | undefined;

private started = false;

constructor(private input: Input) {}

private equip(slot: number) {
Expand Down Expand Up @@ -97,6 +99,14 @@ export class Items implements OnInit, OnStart, OnRender, OnCharacterAdded, OnRun
this.selectSlot(slot);
});
});

if (this.character) {
if (this.inventory.size() > 0) {
this.selectSlot(0);
}
}

this.started = true;
}

onRender(dt: number): void {
Expand All @@ -121,8 +131,11 @@ export class Items implements OnInit, OnStart, OnRender, OnCharacterAdded, OnRun
onCharacterAdded(character: Model): void {
this.character = character;
if (this.currentItemObject) this.currentItemObject.character = character;
if (this.inventory.size() > 0) {
this.selectSlot(0);

if (this.started) {
if (this.inventory.size() > 0) {
this.selectSlot(0);
}
}
}

Expand Down
25 changes: 20 additions & 5 deletions src/client/controllers/movement.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Controller, OnStart, OnRender, OnTick, Modding } from "@flamework/core";
import { OnCharacterAdded } from "./core";
import { Players, Workspace } from "@rbxts/services";
import { Players } from "@rbxts/services";
import { Input } from "./input";
import { isCharacterGrounded } from "shared/utilities/character_utility";
import State from "shared/state";
import { setTimeout } from "@rbxts/set-timeout";
import { Camera, SensitivityModifier } from "./camera";

interface ControlModule {
Enable: (ControlModule: ControlModule, Enabled: boolean) => void;
Expand Down Expand Up @@ -32,7 +33,7 @@ export interface OnWalkingChanged {
}

@Controller({})
export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick, OnFallChanged, OnRunningChanged {
static localPlayer = Players.LocalPlayer;
static playerScripts = Movement.localPlayer.WaitForChild("PlayerScripts");
static playerModule = Movement.playerScripts.WaitForChild("PlayerModule");
Expand All @@ -51,7 +52,6 @@ export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
[Enum.KeyCode.LeftShift, "sprint"],
]);

private camera: Camera = Workspace.CurrentCamera as Camera;
private moveVector: Vector3 = Vector3.zero;
private lastMoveVector: Vector3 = this.moveVector;
private lastVelocity = 0;
Expand All @@ -66,6 +66,9 @@ export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
private fallChangedListeners = new Set<OnFallChanged>();
private lastFreefallStartTick: number | undefined;

private airSensitivityModifier = SensitivityModifier.create("jump");
private runSensitivityModifier = SensitivityModifier.create("run");

static speedConstant = {
crouch: 6,
walk: 12,
Expand All @@ -82,7 +85,7 @@ export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
run: 50,
};

constructor(private input: Input) {}
constructor(private input: Input, private cameraController: Camera) {}

onCharacterAdded(character: Model) {
this.character = character;
Expand Down Expand Up @@ -164,7 +167,10 @@ export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
this.lastMoveVector = this.moveVector;

if (input.Magnitude <= 0) return;
const y = this.camera.CFrame.ToOrientation()[1];

const rawCameraCFrame = this.cameraController.getRawCFrame();

const y = rawCameraCFrame.ToOrientation()[1];
const cameraOrientation = CFrame.Angles(0, y, 0);
const forwardPoint = cameraOrientation.mul(new CFrame(input));
const orientation = new CFrame(Vector3.zero, forwardPoint.Position);
Expand Down Expand Up @@ -217,9 +223,18 @@ export class Movement implements OnCharacterAdded, OnStart, OnRender, OnTick {
return this.humanoid.GetState() === Enum.HumanoidStateType.FallingDown;
}

onFallChanged(state: boolean): void {
this.airSensitivityModifier.setPercent(state ? 20 : 100);
}

onRunningChanged(runningState: boolean): void {
this.runSensitivityModifier.setPercent(runningState ? 35 : 100);
}

onStart(): void {
const runningChangedListeners = new Set<OnRunningChanged>();
const walkingChangedListeners = new Set<OnWalkingChanged>();
this.fallChangedListeners = new Set<OnFallChanged>();

Modding.onListenerAdded<OnJump>((object) => this.jumpListeners.add(object));
Modding.onListenerRemoved<OnJump>((object) => this.jumpListeners.delete(object));
Expand Down
Loading
Loading