Menu
CoddyTech

Les éliminations rapportent des points

Leçon 22 sur 25 du cours Jeu Roblox : Arène de combat de Coddy.

Vaincre un bot devrait compter. Quand un Humanoid meurt, rien n’indique qui l’a éliminé, alors hurt mémorise le dernier joueur qui a frappé chaque Humanoid. Le coup ou le balayage d’un joueur transmet ce joueur comme attaquant ; le coup d’un bot n’en transmet aucun :

if attacker then
	lastHit[humanoid] = attacker
end

À la mort du bot (Died), le dernier joueur à l’avoir frappé obtient un Kill dans le classement : un Folder nommé leaderstats dans le joueur, contenant un IntValue.

challenge icon

Défi

Difficile

Comptez une élimination pour la personne qui bat un robot.

  1. Quand un joueur rejoint la partie, ajoutez un Folder leaderstats contenant un IntValue Kills.
  2. Ajoutez local lastHit = {} ; hurt(humanoid, amount, attacker) enregistre lastHit[humanoid] = attacker ; swing et slam transmettent player.
  3. À la mort d’un robot (Died), ajoutez 1 au compteur Kills de ce joueur s’il est toujours là ; effacez l’entrée.

Appuyez sur « Jouer » et battez un robot : le compteur Kills augmente dans le classement. Ensuite, cliquez sur « Vérifier ».

Essayez vous-même

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local Debris = game:GetService("Debris")
local PathfindingService = game:GetService("PathfindingService")
local Settings = require(game.ReplicatedStorage.Settings)

local remotes = game.ReplicatedStorage.Remotes
local bots = workspace.Bots
local lastSwing = {} -- player -> moment de son dernier coup d’épée
local combo = {} -- player -> { coups consécutifs, moment où le dernier a porté }

-- Les animations d’épée propres à Roblox
local slash = Instance.new("Animation")
slash.AnimationId = "rbxassetid://522635514"
local lunge = Instance.new("Animation")
lunge.AnimationId = "rbxassetid://522638767"

print("Swords deal " .. Settings.SwordDamage .. " damage")

-- Un nombre qui flotte brièvement au-dessus de la personne touchée
local function damageNumber(character, amount)
	local root = character:FindFirstChild("HumanoidRootPart")
	if not root then
		return
	end
	local tag = Instance.new("BillboardGui")
	tag.Name = "Damage"
	tag.Size = UDim2.fromOffset(60, 30)
	tag.StudsOffset = Vector3.new(0, 4.5, 0)
	tag.Adornee = root
	local text = Instance.new("TextLabel")
	text.Size = UDim2.fromScale(1, 1)
	text.BackgroundTransparency = 1
	text.TextColor3 = Color3.fromRGB(255, 220, 80)
	text.TextScaled = true
	text.Text = "-" .. math.round(amount)
	text.Parent = tag
	tag.Parent = character
	Debris:AddItem(tag, 0.8)
end

-- Tous les coups portés dans le jeu passent par ici
local function hurt(humanoid, amount)
	if humanoid.Health <= 0 then
		return
	end
	local character = humanoid.Parent
	-- une parade absorbe la majeure partie du coup
	if character:GetAttribute("Blocking") then
		amount *= Settings.BlockMultiplier
	end
	humanoid:TakeDamage(amount)
	damageNumber(character, amount)
end

-- Un étourdissement immobilise quelqu’un un instant
local function stun(humanoid)
	local character = humanoid.Parent
	if character:GetAttribute("Stunned") then
		return
	end
	character:SetAttribute("Stunned", true)
	print(character.Name .. " is stunned")
	local speed = humanoid.WalkSpeed
	humanoid.WalkSpeed = 0
	task.delay(Settings.StunTime, function()
		if character.Parent then
			humanoid.WalkSpeed = speed
			character:SetAttribute("Stunned", nil)
		end
	end)
end

-- Un coup d’épée : l’animation, puis tous ceux qui se trouvent dans la zone devant vous sont touchés
local function swing(player)
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = time()
	if lastSwing[player] and now - lastSwing[player] < Settings.SwingCooldown then
		return
	end
	lastSwing[player] = now
	-- trois coups d’épée consécutifs forment un combo ; le troisième est une estocade
	local c = combo[player]
	if c and now - c.last <= Settings.ComboWindow then
		c.count = c.count % 3 + 1
	else
		c = { count = 1 }
		combo[player] = c
	end
	c.last = now
	local finisher = c.count == 3
	character.Humanoid.Animator:LoadAnimation(finisher and lunge or slash):Play()
	-- la zone devant vous, jamais votre propre corps
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local box = root.CFrame * CFrame.new(0, 0, -3)
	local hit = {} -- chaque humanoid une seule fois par coup d’épée
	for _, part in workspace:GetPartBoundsInBox(box, Settings.HitboxSize, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		if humanoid and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.ComboDamage[c.count])
			if finisher then
				stun(humanoid)
			end
		end
	end
end

-- Les capacités, par nom : ce que chacune fait
local abilities = {}

-- Le personnage d'un joueur est déplacé par son propre appareil ; le dash s'effectue donc sur celui-ci
function abilities.Dash(player)
	remotes.Ability:FireClient(player, "Dash")
end

-- Tous ceux qui vous entourent sont touchés et projetés loin de vous
function abilities.Slam(player, character, root)
	local params = OverlapParams.new()
	params.FilterDescendantsInstances = { character }
	local hit = {}
	for _, part in workspace:GetPartBoundsInRadius(root.Position, Settings.SlamRadius, params) do
		local humanoid = part.Parent:FindFirstChildOfClass("Humanoid")
		local theirRoot = part.Parent:FindFirstChild("HumanoidRootPart")
		if humanoid and theirRoot and not hit[humanoid] then
			hit[humanoid] = true
			hurt(humanoid, Settings.SlamDamage)
			local away = (theirRoot.Position - root.Position) * Vector3.new(1, 0, 1)
			if away.Magnitude > 0 then
				away = away.Unit
			end
			theirRoot:ApplyImpulse((away * Settings.SlamKnockback + Vector3.new(0, 40, 0)) * theirRoot.AssemblyMass)
		end
	end
end

function abilities.Block(player, character)
	character:SetAttribute("Blocking", true)
	task.delay(Settings.BlockTime, function()
		if character.Parent then
			character:SetAttribute("Blocking", nil)
		end
	end)
end

-- Le client demande une capacité par son nom. Le serveur vérifie qu'il s'agit bien d'une
-- capacité existante, que le joueur est en vie et que son temps de recharge est écoulé. Le moment où elle sera de nouveau disponible
-- est stocké dans un attribut du joueur, pour que son écran puisse l'afficher.
remotes.Ability.OnServerEvent:Connect(function(player, name)
	if typeof(name) ~= "string" or not abilities[name] then
		return
	end
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or character.Humanoid.Health <= 0 then
		return
	end
	local now = workspace:GetServerTimeNow()
	if now < (player:GetAttribute(name .. "ReadyAt") or 0) then
		return
	end
	player:SetAttribute(name .. "ReadyAt", now + Settings.Cooldowns[name])
	abilities[name](player, character, root)
end)

-- Chaque joueur reçoit une épée à chaque nouvelle vie
Players.PlayerAdded:Connect(function(player)
	-- Leçon 22 : Score Kills vient ici
	player.CharacterAdded:Connect(function()
		local sword = ServerStorage.Sword:Clone()
		sword.Parent = player.Backpack
		sword.Activated:Connect(function()
			swing(player)
		end)
	end)
end)

-- Le mannequin d'entraînement : sa barre de vie suit son état de santé et, une fois vaincu, le
-- mannequin réapparaît après un instant sous forme d'une nouvelle copie au même endroit
local function setupDummy(dummy)
	local humanoid = dummy.Humanoid
	humanoid.HealthChanged:Connect(function(health)
		dummy.HealthBar.Back.Fill.Size = UDim2.fromScale(health / humanoid.MaxHealth, 1)
	end)
	humanoid.Died:Connect(function()
		local spot = dummy:GetPivot()
		task.wait(0.5)
		dummy:Destroy()
		task.wait(Settings.DummyRespawn)
		local fresh = ServerStorage.Dummy:Clone()
		fresh:PivotTo(spot)
		fresh.Parent = workspace
		setupDummy(fresh)
	end)
end
setupDummy(workspace.Dummy)

-- Le personnage vivant le plus proche qu'un bot peut voir, et sa distance
local function nearestCharacter(position)
	local best, bestDistance = nil, Settings.BotSight
	for _, player in Players:GetPlayers() do
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart")
		if root and character.Humanoid.Health > 0 then
			local distance = (root.Position - position).Magnitude
			if distance < bestDistance then
				best, bestDistance = character, distance
			end
		end
	end
	return best, bestDistance
end

-- Marcher vers un personnage : directement vers lui si rien ne bloque le passage, ou
-- vers le prochain point d'un chemin qui contourne l'obstacle
local function walkToward(bot, target, path)
	local root = bot.HumanoidRootPart
	local goal = target.HumanoidRootPart.Position
	local params = RaycastParams.new()
	params.FilterDescendantsInstances = { bot }
	local hit = workspace:Raycast(root.Position, goal - root.Position, params)
	if not hit or hit.Instance:IsDescendantOf(target) then
		bot.Humanoid:MoveTo(goal)
		return
	end
	path:ComputeAsync(root.Position, goal)
	local points = path:GetWaypoints()
	if path.Status == Enum.PathStatus.Success and points[2] then
		bot.Humanoid:MoveTo(points[2].Position)
	end
end

-- Le cerveau d’un bot : toutes les quarts de seconde, il observe les alentours et prend une décision
local function runBot(bot)
	local humanoid = bot.Humanoid
	local root = bot.HumanoidRootPart
	local lastAttack = 0
	local path = PathfindingService:CreatePath()
	while bot.Parent and humanoid.Health > 0 do
		local target, distance = nearestCharacter(root.Position)
		local state
		if bot:GetAttribute("Stunned") then
			state = "Stunned"
		elseif humanoid.Health < humanoid.MaxHealth * Settings.RetreatAt then
			state = "Retreat"
		elseif not target then
			state = "Idle"
		elseif distance <= Settings.BotRange then
			state = "Attack"
		else
			state = "Chase"
		end
		bot:SetAttribute("State", state)
		if state == "Chase" then
			walkToward(bot, target, path)
		elseif state == "Attack" then
			humanoid:MoveTo(root.Position)
			if time() - lastAttack >= Settings.BotAttackCooldown then
				lastAttack = time()
				humanoid.Animator:LoadAnimation(slash):Play()
				hurt(target.Humanoid, Settings.BotDamage)
			end
		elseif state == "Retreat" and target then
			local away = (root.Position - target.HumanoidRootPart.Position) * Vector3.new(1, 0, 1)
			if away.Magnitude > 0 then
				humanoid:MoveTo(root.Position + away.Unit * 12)
			end
		else
			humanoid:MoveTo(root.Position)
		end
		task.wait(0.25)
	end
end

-- Un nouveau bot sur la plateforme rouge, avec ses caractéristiques et une barre de vie
local function spawnBot()
	local bot = ServerStorage.Bot:Clone()
	bot:PivotTo(workspace.BotSpawn.CFrame + Vector3.new(0, 3, 0))
	bot.Humanoid.MaxHealth = Settings.BotHealth
	bot.Humanoid.Health = Settings.BotHealth
	bot.Humanoid.WalkSpeed = Settings.BotSpeed
	bot.Parent = bots
	-- Garder la physique du bot sur le serveur pour que le recul du slam le déplace
	bot.HumanoidRootPart:SetNetworkOwner(nil)
	bot.Humanoid.HealthChanged:Connect(function(health)
		bot.HealthBar.Back.Fill.Size = UDim2.fromScale(health / bot.Humanoid.MaxHealth, 1)
	end)
	bot.Humanoid.Died:Connect(function()
		task.wait(1)
		bot:Destroy()
		task.wait(Settings.BotRespawn)
		spawnBot()
	end)
	task.spawn(runBot, bot)
end

for _ = 1, Settings.Bots do
	spawnBot()
end

Toutes les leçons de Jeu Roblox : Arène de combat

Entraînez-vous par vous-même : Playground Roblox