My first ever professional game: CastleDonia

Dear Fellow Devs,

Recently, I have worked on a game like most of you out there. The game is inspired by Stickman:legacy and Field of Battle. A strategic game basically.

I wanna showcase an early iteration of the game with the core mechanics done.

The entities are spawned in with OOP and module scripts mostly. The scripts will be in this post.

Gyazos:
https://gyazo.com/a7149f189d9ad588f342704d3e88ccb7 (Placing)
https://gyazo.com/24157f2f91fe2fcf5c8c1fec6c8a0fcd(Archer and Dummy Interaction)
https://gyazo.com/a0e1e6744420b8263051e7d0ad539b28(Dummy v Dummy)
https://gyazo.com/6bbf6d078ee6eb4b5a457b48661033c7(Dummy v wall)

The dummy v dummy interaction needs work thou!

Scripts:

EntityEngine(Variables are messy because of debugging and the import tables):

local ENGINE = {}
ENGINE.__index = ENGINE

--[[-
=============== THIS ENGINE IS MADE BY PLANET8ATER56 OTHERWISE KNOWN AS RESPECTEDCOW=====================
DO NOT COPY THIS ENGINE OR ACCESS THIS ENGINE WITHOUT PERMISSION.

TO GET PERMISSION CONTACT RespectedCow#5338 via Discord.

--//HOW THIS ENGINE WORKS//--
I'M LAZY TO UPDATE

-]]

--//Services//--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local pathfinding = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")
local PhysService = game:GetService("PhysicsService")
PhysService:CreateCollisionGroup("t")
PhysService:CollisionGroupSetCollidable("t","t",false)

--//Variables//--
local EntityData = require(script.Parent.EntityData)
local EntityAnimator = require(script.Parent.EntityAnimator)
local newTroopEvent = script.NewTroop

--//Functions//--
function NoCollide(model)
	for k,v in pairs(model:GetChildren()) do
		if v:IsA"BasePart" then
			PhysService:SetPartCollisionGroup(v,"t")
		end
	end
end

local function findTarget(Troop, enemyTeam, maxDist, entityType)
	local myRoot = Troop.PrimaryPart
	local target = nil
	local potentialTargets = {}
	local seeTargets = {}
	for i,v in ipairs(game.Workspace[enemyTeam..entityType.."Troops"]:GetChildren()) do
		local human = v:FindFirstChild("Humanoid")
		local torso = v:FindFirstChild("Torso") or v:FindFirstChild("HumanoidRootPart")
		if human and torso and v.Name ~= script.Parent.Name then
			if (myRoot.Position - torso.Position).magnitude < maxDist and human.Health > 0 then
				table.insert(potentialTargets,torso)
			end
		end
	end
	if #potentialTargets > 0 then
		for i,v in ipairs(potentialTargets) do
			table.insert(seeTargets, v)
		end
	end
	if #seeTargets > 0 then
		for i,v in ipairs(seeTargets) do
			if (myRoot.Position - v.Position).magnitude < maxDist then
				target = v
			end
		end
	end
	return target
end

function ENGINE:OffensiveStart()
	--//VARIABLES//--
	print(self.entityTeam.Name)
	local entityTeamWall = game.Workspace.Map:FindFirstChild(self.entityTeam.Name):FindFirstChild(self.entityTeam.Name.."Wall")
	local target
	local CURRENT_STATUS
	if self.entityEnemy.dataFolder.WallDestroyed == true then
	  	target = entityTeamWall.Parent.Parent[self.entityEnemy.Team.Name].EndPoint
	else
		target = entityTeamWall.Parent.Parent[self.entityEnemy.Team.Name].WallTarget
	end
	
	--//CREATION//--
	print(self.entityModel)
	local newClone = self.entityModel:Clone()
	newClone.Name = self.entityTeam.Name.."Dummy"
	newClone.Parent = game.Workspace[self.entityTeam.Name.."OffensiveTroops"]
	newClone.PrimaryPart.CFrame = entityTeamWall.StartingPoint.CFrame
	newClone:SetPrimaryPartCFrame(CFrame.lookAt( -- Make the npc look at the opposit team's wall targets
		newClone.PrimaryPart.Position,
		target.Position * Vector3.new(1, 0, 1) +
			newClone.PrimaryPart.Position * Vector3.new(0, 1, 0)
		))
	local humanoid = newClone.Humanoid
	
	local datatableParameters = {-- FOR GOING INTO FUNCTIONS
		damage = self.damage,
		attackrate = self.attackrate,
		maxDist = self.maxDist,
		range = self.range,
		speed = self.speed,
		troopDamage = self.troopDamage,
		health = self.health,
		defense = self.defense,
		
		--//EVENTS//--
		Attack = self.attackEvent,
		Enabled = self.enabledEvent,
		Death = self.Dead,
		BehaviorChanged = self.BehaviorChanged
	}
	
	--//SET THE TROOP'S HUMANOID STATS TO THE CORRECT VALUES
	newClone.Humanoid.WalkSpeed = self.speed
	newClone.Humanoid.Health = self.health
	
	--//ACTIVATION//--
	self.BehaviorChanged.Event:Connect(function(behavior)
		CURRENT_STATUS = behavior
	end)
	
	newTroopEvent:Fire(self, newClone)
	
	NoCollide(newClone) -- PREVENT LAG
	
	--ENGINE:CreateOppositTroop(enemyTroopImportData, self.entityOwner, "Dummy") -- TESTING THE FUNCTION
	
	newClone.PrimaryPart:SetNetworkOwner(nil) -- PREVENT LAG
	
	EntityAnimator:Animate(newClone)
	
	ENGINE:OffensiveStartBehaviour(self, newClone, datatableParameters, target, self.entityEnemy.Team.Name, self.entityType) -- ORGANIZING
end

function ENGINE:DefensiveStart()
	--//VARIABLES//--
	local newClone = self.entityModel:Clone()
	local targetFolder = game.Workspace[self.entityEnemy.Team.Name.."OffensiveTroops"]
	local testingPartPosition = game.Workspace.TestingPart
	local GameEnded = game.ServerStorage.Values.GameEnded
	
	--//ORGANIZING//--
	newClone.Parent = game.Workspace[self.entityTeam.Name.."DefensiveTroops"]
	newClone.PrimaryPart.CFrame = self.placeCFrame
	
	local datatableParameters = {-- FOR GOING INTO FUNCTIONS
		damage = self.damage,
		attackrate = self.attackrate,
		maxDist = self.maxDist,
		range = self.range,
		speed = self.speed,
		troopDamage = self.troopDamage,
		health = self.health,
		defense = self.defense,

		--//EVENTS//--
		Attack = self.attackEvent,
		Enabled = self.enabledEvent,
		Death = self.Dead,
		BehaviorChanged = self.BehaviorChanged
	}
	
	--//ACTIVATION//--
	EntityAnimator:Animate(newClone)
	
	NoCollide(newClone)
	
	newClone.PrimaryPart.Anchored = true
	
	while GameEnded.Value do
		local target = findTarget(newClone, self.entityEnemy.Team.Name, self.maxDist, "Offensive")
		if target ~= nil and target.Parent.Humanoid.Health > 0 then
			
			ENGINE:DefensiveAttack(newClone, target, datatableParameters)
			wait(self.attackrate)
		end
		wait()
	end
end

--//Behaviour//--
function ENGINE:Attack(self, Troop, target, dataParametertable)
	--//DEFINING DATA//--
	local speed = dataParametertable.speed
	local defense = dataParametertable.defense
	local range = dataParametertable.range
	local attackrate = dataParametertable.attackrate
	local damage = dataParametertable.damage
	local troopDamage = dataParametertable.troopDamage
	local maxDist = dataParametertable.maxDist
	local health = dataParametertable.health
	local attackType
	
	--//RUN THE CHECKS//--
	if target.Parent:FindFirstChild("Humanoid") == nil then -- TWO CHECKS
		if (Troop.PrimaryPart.Position - target.Position).magnitude < range then
			local attack = Troop.Humanoid:LoadAnimation(Troop.Configuration.Animations.Attack) 
			attackType = "ENTITY"
			
			self.Attack:Fire(self, Troop, target, attackType)
			attack:Play()
			target.Health.Value = target.Health.Value - damage
		end
		
	else
		if (Troop.PrimaryPart.Position - target.Position).magnitude < range then
			local attack = Troop.Humanoid:LoadAnimation(Troop.Configuration.Animations.Attack) 
			attackType = "ENVIRONMENT"
			
			self.Attack:Fire(self, Troop, target, attackType)
			attack:Play()
			target.Parent.Humanoid:TakeDamage(troopDamage)
		end
	end
end

function ENGINE:PATHFIND(newClone, targetPosition)
	local TroopPath = pathfinding:CreatePath()
	TroopPath:ComputeAsync(newClone.PrimaryPart.Position,targetPosition)
	local wayPoints = TroopPath:GetWaypoints()

	if TroopPath.Status == Enum.PathStatus.Success then
		for i,wayPoint in pairs(wayPoints) do
			newClone.Humanoid:MoveTo(wayPoint.Position)

			if wayPoint.Action == Enum.PathWaypointAction.Jump then
				newClone.Humanoid:ChangeState(Enum.HumanoidStateType.Jumping)
			end

			newClone.Humanoid.MoveToFinished:Wait()
		end
	else
		newClone.Humanoid:MoveTo(newClone.PrimaryPart.Position)
	end
end

function ENGINE:DefensiveAttack(newClone, target, dataParametertable)
	--//DEFINING DATA//--
	local speed = dataParametertable.speed
	local defense = dataParametertable.defense
	local range = dataParametertable.range
	local attackrate = dataParametertable.attackrate
	local damage = dataParametertable.damage
	local troopDamage = dataParametertable.troopDamage
	local maxDist = dataParametertable.maxDist
	local health = dataParametertable.health
	
	if (target.Position - newClone.PrimaryPart.Position).magnitude < range then
		newClone:SetPrimaryPartCFrame(CFrame.lookAt( -- MAKE THE CLONE LOOK AT THE ENEMY/TARGET
				newClone.PrimaryPart.Position,
				target.Position * Vector3.new(1, 0, 1) +
					newClone.PrimaryPart.Position * Vector3.new(0, 1, 0)
			))
		local animation = newClone.Humanoid:LoadAnimation(newClone.Configuration.Animations.Attack)
		animation:Play()
		
		target.Parent.Humanoid:TakeDamage(troopDamage)
	end
end

function ENGINE:OffensiveStartBehaviour(self, newClone, dataParametertable, mainTarget, enemyTeam, entityType)
	--//DEFINING DATA//--
	local speed = dataParametertable.speed
	local defense = dataParametertable.defense
	local range = dataParametertable.range
	local attackrate = dataParametertable.attackrate
	local damage = dataParametertable.damage
	local troopDamage = dataParametertable.troopDamage
	local maxDist = dataParametertable.maxDist
	local health = dataParametertable.health
	
	--//VARIABLES//--
	local IsAttacking = false
	local gameEnded = game.ServerStorage.Values.GameEnded
	local targetFolder = game.Workspace[enemyTeam.."OffensiveTroops"]
	
	while gameEnded.Value do
		local target = findTarget(newClone, enemyTeam, maxDist, entityType)
		if newClone.Humanoid.Health <= 1 then
			newClone:Destroy()
			break
		end
		if target ~= nil then
			if (target.Position - newClone.PrimaryPart.Position).magnitude < range then
				newClone.Humanoid:MoveTo(newClone.PrimaryPart.Position)
				self.BehaviorChanged:Fire("ATTACK", target, newClone)
				ENGINE:Attack(self, newClone, target, dataParametertable)
				IsAttacking = true
				wait(attackrate)
			else
				newClone.Humanoid:MoveTo(target.Position+Vector3.new(0, 0, -2))
			end
		else
			IsAttacking = false
			
			spawn(function()
				if IsAttacking == false then
					newClone.Humanoid:MoveTo(mainTarget.Position)
				end
			end)
			if (newClone.PrimaryPart.Position - mainTarget.Position).magnitude < range then
				if mainTarget.Health.Value ~= 0 then
					ENGINE:Attack(self, newClone, mainTarget, dataParametertable)
				end 
				wait(attackrate)
			end
		end
		wait()
	end
end

--//CONSTRUCTOR//--
function ENGINE.new(name, lvl, entitytype, team, player, enemy, troopFile, placeCframe)
	--//Events//--
	print(enemy)
	local BehaviourChangedEvent = Instance.new("BindableEvent")
	local DeadEvent = Instance.new("BindableEvent")
	local enabledEvent = Instance.new("BindableEvent")
	local AttackEvent = Instance.new("BindableEvent")
	
	-- Create the table
	local newEntityDataTable = setmetatable(
		{
			--//Properties//--
			entityName = name,
			entityModel = ReplicatedStorage[troopFile]:FindFirstChild(name),
			entityType = entitytype,
			entityTeam = team,
			entityOwner = player,
			entityEnemy = enemy,
			
			placeCFrame = placeCframe,
			
			attackrate = EntityData[name]["Lvl"..lvl].attackrate,
			damage = EntityData[name]["Lvl"..lvl].damage,
			speed = EntityData[name]["Lvl"..lvl].speed,
			health = EntityData[name]["Lvl"..lvl].health,
			defense = EntityData[name]["Lvl"..lvl].defense,
			range = EntityData[name]["Lvl"..lvl].range,
			maxDist = EntityData[name]["Lvl"..lvl].maxDist,
			troopDamage = EntityData[name]["Lvl"..lvl].troopDamage,
			
			--//Events//--
			BehaviorChanged = BehaviourChangedEvent,
			Dead = DeadEvent.Event,
			enableEvent = enabledEvent.Event,
			Attack = AttackEvent
		},
		ENGINE
	)
	
	return newEntityDataTable
end

--//INSTRUCTIONS//--


return ENGINE

Building Module:

local SYSTEM = {}

--//VARIABLES//--
local towerFolder = game.ReplicatedStorage:WaitForChild("PlaceClones") -- Default is defensive
local CreateEntityEvent = game.ReplicatedStorage:WaitForChild("Remotes").Events.CreateEntity

--//SERVICES//--
local RunService = game:GetService("RunService")
local PhysService = game:GetService("PhysicsService")

local function GetTouchingParts(part)
	local connection = part.Touched:Connect(function() end)
	local results = part:GetTouchingParts()
	connection:Disconnect()
	return results
end

function NoCollide(model)
	for k,v in pairs(model:GetChildren()) do
		if v:IsA"BasePart" or v.Name == "Collider" then
			PhysService:SetPartCollisionGroup(v,"t")
		end
	end
end

function DistanceFromOtherTowers(tower, player)
	local defensiveFolder = game.Workspace[player.Team.Name.."DefensiveTroops"]
	local hasChild = false
	local canPlace = false
	
	local collideTroop = {}
	
	for i,troop in pairs(defensiveFolder:GetChildren()) do
		hasChild = true
		if troop:IsA("Model") then
			if (tower.PrimaryPart.Position - troop.PrimaryPart.Position).magnitude < 6 then
				table.insert(collideTroop, troop)
			end
		end
	end
	if hasChild and #collideTroop == 0 then
		return true
	end
	if hasChild == false and canPlace == false then
		return true
	end
end

function SYSTEM:PlaceTower(player, tower, mouse)
	if player.dataFolder.IsPlacing.Value == true then return end
	player.dataFolder.IsPlacing.Value = true
	
	-- CLONE THE TOWER FROM REPLICATED STORAGE FIRST
	local newClone = towerFolder:FindFirstChild(tower):Clone()
	newClone.Parent = game.Workspace.PlaceFolder
	local isPlaced = false
	local canPlace = false
	local terminate = false
	
	player.PlayerGui.LocalGUI.HealthIndictor.Enabled = false
	
	NoCollide(newClone)
	
	assert(mouse ~= nil, "BUILDING SYSTEM: MOUSE IS INVALID")
	assert(newClone ~= nil, "BUILDING SYSTEM: CANNOT CLONE SOMETHING THAT DOESN'T EXIST!")
	
	mouse.Button1Down:Connect(function()
		if canPlace == true and isPlaced == false then
			isPlaced = true
			local placeCFrame = newClone.PrimaryPart.CFrame
			newClone:Destroy()
			player.dataFolder.IsPlacing.Value = false
			CreateEntityEvent:FireServer("Archer", 1, "Defensive", placeCFrame)
			player.PlayerGui.LocalGUI.HealthIndictor.Enabled = true
			terminate = true
		end
	end)
	
	mouse.Button2Down:Connect(function()
		newClone:Destroy()
		isPlaced = false
		canPlace = false
		player.dataFolder.IsPlacing.Value = false
		player.PlayerGui.LocalGUI.HealthIndictor.Enabled = true
		terminate = true
	end)
	
	-- GET THE PLAYER'S MOUSE POSITION EVERYTIME THE SCREEN RENDERS
	RunService.RenderStepped:Connect(function()
		if isPlaced == false and terminate == false then
			local newcframe = mouse.Hit -- GET DA POSITION
			newClone.HumanoidRootPart.CFrame = CFrame.new(newcframe.X, 24, newcframe.Z)
			newClone.HumanoidRootPart.Orientation = Vector3.new(0,0,0)
			newClone.Collider.Transparency = 0.5
			
			if mouse.Target.Name == "PlacePart" and DistanceFromOtherTowers(newClone, player) and mouse.Target.Parent.Name == player.Team.Name then
				newClone.Collider.BrickColor = BrickColor.new("Sage green")
				canPlace = true
			else
				newClone.Collider.BrickColor = BrickColor.new("Really red")
				canPlace = false
			end
		end
		wait()
	end)
end

return SYSTEM

Animator Module(Helpful for some people):

-- LAZY TO COMMENT LMAO
local module = {}

local vectorZero = Vector3.new(0, 0, 0) 
local animatedBeings = {}

local function ifIdle(humanoid, NPC)
	if NPC.HumanoidRootPart == nil then return end
	if  NPC.HumanoidRootPart.Velocity.X >= -5 and NPC.HumanoidRootPart.Velocity.X <= 5 and NPC.HumanoidRootPart.Velocity.Z >= -5 and NPC.HumanoidRootPart.Velocity.Z <= 5 then
		return true
	end
end

local constantAnimation = function(v)
	if v == nil then return end
	if v:GetState() == Enum.HumanoidStateType.Dead then
		coroutine.yield()
	end

	local WalkAnimation = v:LoadAnimation(v.Parent.Configuration.Animations.Walk)
	local IdleAnimation = v:LoadAnimation(v.Parent.Configuration.Animations.Idle)

	game:GetService("RunService").Heartbeat:Connect(function()
		if v.Parent then
			if ifIdle(v, v.Parent) then
				WalkAnimation:Stop()
				if IdleAnimation.IsPlaying == false then
					IdleAnimation:Play()
				end
			else
				IdleAnimation:Stop()
				if WalkAnimation.IsPlaying == false then
					WalkAnimation:Play()
					IdleAnimation:AdjustSpeed(v.WalkSpeed)
				end
			end
		end

	end)
end


function module:Animate(NPC)
	local humanoid = NPC.Humanoid
	spawn(function()
		constantAnimation(humanoid)
	end)
end

return module

What do you think? I will work on the gold mining and currency system today! After I watch some youtube videos of course…

Side note: I know this is out of topic but this is a side note so maybe this is ok. I am hiring a builder that can make characters and maps for around 20 dollars ish and also around 20 percent. You will have to work for the entire game. Half up front and half after the game is at beta or the important stuff are done.
Have to show work.

Anyways, thank you for reading this post. I think this game idea might work. But who knows. I wonder how I’ll even advertise it…

Yours truly,
RespectedCow/Planet8ater56

I really need 1000 Robux to change my name. Tell me if this is in the wrong category. Good luck!

Edit: I did the GoldMine system. It works well. But can’t fit in a 7 secs gyazo.

Edit: did the Gui.

Discord Server: Official CastleDonia Server The server is done now. Need more members pls join.

9 Likes

Looks great so far! It really reminds me of those old nostalgic Tower-Defense like games! Can’t wait to play it in the future! :open_mouth:

2 Likes