Sword module with a messy constructor

Hi,

I am making a sword module to make it easy to make my type of swords… The module consists of type checking and public methods.

I am wondering if I can fix my messy .new constructor. It doesn’t look good in my opinion because of my findfirstchild(“animations”).

Is it possible to remove findfirstchild with ‘typechecking’?

I am very new to making classes

Sword Class::

--!strict

local Sword = {}
Sword.__index = Sword

local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--local RunService = game:GetService("RunService")

local Source = ReplicatedStorage.Source

local RaycastHitbox = require(Source.RaycastHitboxV4)
local Trove = require(Source.Packages.Trove)
local ContextActionUtility  = require(Source.ContextActionUtility)

--local LocalPlayer = Players.LocalPlayer

local ATTACK_IMG = "rbxassetid://5743593320"

-- constants
local Debounce = false
local hitSounds = 0

type tab = { 
	Damage: number,
	Yield: number,
	AttackTime: number,
	StartDelay: number,
	EndDelay: number,
	Combos: number,
	ComboReset: number,
	Holster: boolean, -- if holster or no...
	Special: boolean, -- if special attacks or no...
}

type self = {
	_sword: Tool,
	_animations: Folder,
	_settings: tab,
	_connections: Trove.ClassType,]
	_hitbox: RaycastHitbox.ClassType,
	_loadedAnimations: any,
	_loadedSounds: any,
	_combo: number,
	_lastClicked: any,
}

export type Sword = typeof(
	setmetatable({} :: self, {__index = Sword})
)

function Sword.new(sword: Tool, setting: tab, Player: Player): Sword
	local self = setmetatable({}, Sword)	
	local Animator = Player:FindFirstChild("Character"):FindFirstChild("Humanoid"):FindFirstChild("Animator"):: Animator

	self._sword = sword
	self._animations = sword:FindFirstChild("Animations"):: Folder
	self._settings = setting
	self._connections = Trove.new()
	self._hitbox = RaycastHitbox.new(self._sword:FindFirstChild("Handle"))

	-- constants
	self._loadedAnimations = {}
	self._loadedSounds = {}
	self._combo = 0
	self._lastClicked = tick()

	local Animations = self._sword:FindFirstChild("Animations"):: Folder
	local Parameters = RaycastParams.new()

	Parameters.FilterDescendantsInstances = {Player.Character}

	self._loadedAnimations.Equip = Animator:LoadAnimation(Animations:FindFirstChild("Equip"):: Animation)
	self._loadedAnimations.Idle = Animator:LoadAnimation(Animations:FindFirstChild("Idle"):: Animation)
	self._loadedAnimations.Swing1 = Animator:LoadAnimation(Animations:FindFirstChild("Swing1"):: Animation)
	self._loadedAnimations.Swing2 = Animator:LoadAnimation(Animations:FindFirstChild("Swing2"):: Animation)
	self._loadedAnimations.Swing3 = Animator:LoadAnimation(Animations:FindFirstChild("Swing3"):: Animation)

	for _,v in ipairs(self._sword:FindFirstChild("Handle"):GetChildren()) do
		local hitString = "Hit"..tostring(_)

		if self._sword:FindFirstChild("Handle"):FindFirstChild(hitString) then
			self._loadedSounds[hitString] = self._sword:FindFirstChild("Handle"):FindFirstChild(hitString):: Sound
			hitSounds += 1
		end
	end

	self._loadedSounds.Equip = self._sword:FindFirstChild("Handle"):FindFirstChild("Equip"):: Sound
	self._loadedSounds.Swing1 = self._sword:FindFirstChild("Handle"):FindFirstChild("Swing1"):: Sound
	self._loadedSounds.Swing2 = self._sword:FindFirstChild("Handle"):FindFirstChild("Swing2"):: Sound
	self._loadedSounds.Swing3 = self._sword:FindFirstChild("Handle"):FindFirstChild("Swing3"):: Sound

	self._hitbox.RaycastParams = Parameters

	self._connections:Connect(self._sword.Equipped, function(mouse: Mouse) 
		self:Equip()
		self:Bind(self._sword.Name, Enum.UserInputType.MouseButton1)
	end)

	self._connections:Connect(self._sword.Unequipped, function(mouse: Mouse) 
		self:Unequip()
		self:Unbind(self._sword.Name)
	end)

	self._hitbox.OnHit:Connect(function(hit, humanoid, results)
		if humanoid.Health ~= 0 then
			local RandomHit = math.random(0,hitSounds)
			humanoid:TakeDamage(self._settings.Damage)

			if RandomHit == 0 and hitSounds ~= 0 then
				self._loadedSounds.Hit1:Play()
			else
				self._loadedSounds["Hit"..RandomHit]:Play()
			end
		end
	end)

	self._hitbox.Visualizer = true

	return self
end

-- methods

function Sword:Equip()
	self._loadedSounds.Equip:Play()

	self._loadedAnimations.Equip:Play()
	self._loadedAnimations.Idle:Play()
end

function Sword:Unequip()
	if self._loadedAnimations.Idle.IsPlaying then
		self._loadedAnimations.Idle:Stop()
	end

	if self._loadedAnimations.Equip.IsPlaying then
		self._loadedAnimations.Equip:Stop()
	end
end

function Sword:Attack(InputState) 
	if InputState ~= Enum.UserInputState.Begin or Debounce then
		return
	end

	if tick() - self._lastClicked >= self._settings.ComboReset then -- combo reset
		self._combo = 0 
	end

	self._lastClicked = tick() 

	Debounce = true

	if self._combo < self._settings.Combos then
		self._combo += 1
	else
		self._combo = 1
	end

	local swingString = "Swing"..tostring(self._combo)
	print(self._combo, swingString)

	self._loadedAnimations[swingString]:Play()

	task.wait(self._settings.StartDelay:: number)

	self._loadedSounds[swingString]:Play()
	self._hitbox:HitStart()

	task.wait(self._settings.Yield:: number)

	self._hitbox:HitStop()

	task.wait(self._settings.EndDelay:: number)

	Debounce = false
end

function Sword:Holster()
	
end

function Sword:Special()
	
end

function Sword:Bind(ActionName: string, Input)
	ContextActionUtility:BindAction(ActionName,function(actionName,inputState) 
		self:Attack(inputState)
	end, true,Input)

	ContextActionUtility:SetImage(ActionName,ATTACK_IMG)
end

function Sword:Unbind(ActionName: string)
	ContextActionUtility:UnbindAction(ActionName)
end

function Sword:Destroy()
	self._connections:Destroy()
end

return Sword

Also… if you see any bad practices with the public methods, it won’t hurt to help!!!

--Load animations
local LoadedAnimations = {"Equip", "Idle", "Swing1", "Swing2", "Swing3"}
for _, anim in ipairs(LoadedAnimations) do
	self._loadedAnimations[anim] = Animator:LoadAnimation(Animations:FindFirstChild(anim):: Animation)
end

--Load sounds
local LoadedSounds = {"Equip", "Swing1", "Swing2", "Swing3"}
local SwordHandle = self._sword:FindFirstChild("Handle")
for _, sound in ipairs(LoadedSounds) do
	self._loadedSounds[sound] = SwordHandle:FindFirstChild(sound):: Sound
end

If you don’t like this then try:

local function LoadAnimation(Name: string)
	self._loadedAnimations[Name] = Animator:LoadAnimation(Animations:FindFirstChild(Name):: Animation)
end
LoadAnimation("Equip")
LoadAnimation("Idle")
LoadAnimation("Swing1")
LoadAnimation("Swing2")
LoadAnimation("Swing3")

You can also replace FindFirstChild() with indexing (e.g. Sword.Handle or Sword["Handle"]) because you’re not doing anything if it returns nil.

2 Likes

What you’d probably be looking for in this case is Intersection types (Type checking - Luau)

By using this definition:

local Animations = self._sword:FindFirstChild("Animations") :: Folder & {[string]: Animation}

You should be fine to index directly:

local Animations = self._sword:FindFirstChild("Animations") :: Folder & {[string]: Animation}

self._loadedAnimations.Equip = Animator:LoadAnimation(Animations["Equip"])
self._loadedAnimations.Idle = Animator:LoadAnimation(Animations["Idle"])
self._loadedAnimations.Swing1 = Animator:LoadAnimation(Animations["Swing1"])
self._loadedAnimations.Swing2 = Animator:LoadAnimation(Animations["Swing2"])
self._loadedAnimations.Swing3 = Animator:LoadAnimation(Animations["Swing3"])

You should also probably define a swordTool type if you have a lot of things to index
For instance:

type swordTool = Tool & {
	Animations: Folder & {[string]:Animation},
	Handle: BasePart & {[string]: Sound},
}

function Sword.new(sword: swordTool, setting: tab, Player: Player): Sword
	local self = setmetatable({}, Sword)	
	
	self._sword = sword
	self._animations = sword.Animations
	self._settings = setting
	self._connections = Trove.new()
	self._hitbox = RaycastHitbox.new(self._sword.Handle)

    ...

	local Animations = self._sword.Animations

	self._loadedAnimations.Equip = Animator:LoadAnimation(Animations["Equip"])
	self._loadedAnimations.Idle = Animator:LoadAnimation(Animations["Idle"])
	self._loadedAnimations.Swing1 = Animator:LoadAnimation(Animations["Swing1"])
	self._loadedAnimations.Swing2 = Animator:LoadAnimation(Animations["Swing2"])
	self._loadedAnimations.Swing3 = Animator:LoadAnimation(Animations["Swing3"])
end

Anyways hope this helps, I generally stray away from using strict typechecking because I find that more often than not it ends up being a nuisance. If you still do want to use strict typechecking, I would recommend creating a module with a ton of base classes like player or character to reduce the tons of repetitive dumping casting

type Character = Model & {
	Head: BasePart?,
	Humanoid: Humanoid & {Animator: Animator},
	HumanoidRootPart: BasePart?
}
local Animator = Player:FindFirstChild("Character"):FindFirstChild("Humanoid"):FindFirstChild("Animator"):: Animator
--> Becomes 
local Animator = (Player.Character :: Character).Humanoid.Animator;
1 Like

You’re right, it does become a nuisance. But on the bright side, you can spot errors early and your code looks better, but that’s my opinion.

Thank you for both of you replying. I appreciate the help.

Can you give me a example of a base class that I would use?

I am fairly new to strict typechecking and i’m trying to get into it. But I do have a CharacterWrapper class. It gets and loads the character in a efficient way for me.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.