<?xml version="1.0" encoding="utf-8"?><roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4"><Item class="ReplicatedFirst" referent="RBX2"><Properties><string name="Name">ReplicatedFirst</string></Properties><Item class="LocalScript" referent="RBX1"><Properties><string name="Name">LoadingScreen</string><ProtectedString name="Source"><![CDATA[--[[
	THE LAST SAMURAI — cinematic loading screen (ReplicatedFirst).
	The web-preview design rebuilt in native Roblox UI:
	black lacquer, ink-bleed sun, ensō ring, vertical kanji, staggered title,
	and the signature loader — a katana drawn from its saya, where the
	revealed blade IS the progress bar. Ends with a katana-cut transition.
]]

local ReplicatedFirst = game:GetService("ReplicatedFirst")
local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local ContentProvider = game:GetService("ContentProvider")
local UserInputService = game:GetService("UserInputService")
local RunService = game:GetService("RunService")

ReplicatedFirst:RemoveDefaultLoadingScreen()

local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

local LACQUER = Color3.fromRGB(11, 10, 13)
local WASHI = Color3.fromRGB(233, 226, 210)
local SHU = Color3.fromRGB(179, 32, 46)
local KIN = Color3.fromRGB(217, 164, 91)

local function make(cls, props)
	local inst = Instance.new(cls)
	local parent
	for k, v in pairs(props) do
		if k == "Parent" then
			parent = v
		else
			inst[k] = v
		end
	end
	if parent then
		inst.Parent = parent
	end
	return inst
end

-- ------------------------------------------------------------- build

local gui = make("ScreenGui", {
	Name = "TLSLoading", IgnoreGuiInset = true, DisplayOrder = 100,
	ResetOnSpawn = false, Parent = playerGui,
})
local rootFrame = make("Frame", {
	BackgroundColor3 = LACQUER, BorderSizePixel = 0,
	Size = UDim2.new(1, 0, 1, 0), Parent = gui,
})

-- ink-bleed sun: layered soft circles
local sunLayers = {}
for i = 1, 3 do
	local s = 220 + i * 90
	sunLayers[i] = make("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.4, 0),
		Size = UDim2.new(0, s, 0, s), BackgroundColor3 = SHU,
		BackgroundTransparency = 1, BorderSizePixel = 0, Parent = rootFrame,
	})
	make("UICorner", { CornerRadius = UDim.new(1, 0), Parent = sunLayers[i] })
end

-- ensō ring
local enso = make("Frame", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.4, 0),
	Size = UDim2.new(0, 430, 0, 430), BackgroundTransparency = 1, Parent = rootFrame,
})
make("UICorner", { CornerRadius = UDim.new(1, 0), Parent = enso })
local ensoStroke = make("UIStroke", {
	Color = WASHI, Thickness = 4, Transparency = 1, Parent = enso,
})

-- 侍 watermark
local mark = make("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.45, 0),
	Size = UDim2.new(0, 700, 0, 700), BackgroundTransparency = 1,
	Font = Enum.Font.Antique, Text = "侍", TextSize = 560,
	TextColor3 = WASHI, TextTransparency = 1, Parent = rootFrame,
})

-- vertical kanji column (right side)
local KANJI = { "最", "後", "ノ", "侍" }
local kanjiLabels = {}
for i, ch in ipairs(KANJI) do
	kanjiLabels[i] = make("TextLabel", {
		AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -34, 0.24, (i - 1) * 62),
		Size = UDim2.new(0, 60, 0, 58), BackgroundTransparency = 1,
		Font = Enum.Font.Antique, Text = ch, TextSize = 46,
		TextColor3 = WASHI, TextTransparency = 1, Parent = rootFrame,
	})
end

-- title, letter by letter
local TITLE = "THE LAST SAMURAI"
local titleRow = make("Frame", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.4, 0),
	Size = UDim2.new(0, 760, 0, 64), BackgroundTransparency = 1, Parent = rootFrame,
})
make("UIListLayout", {
	FillDirection = Enum.FillDirection.Horizontal,
	HorizontalAlignment = Enum.HorizontalAlignment.Center,
	Padding = UDim.new(0, 8), Parent = titleRow,
})
local letters = {}
for i = 1, #TITLE do
	local ch = TITLE:sub(i, i)
	letters[i] = make("TextLabel", {
		Size = UDim2.new(0, ch == " " and 16 or 30, 1, 0), BackgroundTransparency = 1,
		Font = Enum.Font.Garamond, Text = ch, TextSize = 46,
		TextColor3 = WASHI, TextTransparency = 1, LayoutOrder = i, Parent = titleRow,
	})
end

local subtitle = make("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.4, 46),
	Size = UDim2.new(0, 600, 0, 20), BackgroundTransparency = 1,
	Font = Enum.Font.Garamond, Text = "—   A   T A L E   O F   S T E E L   A N D   W I N D   —",
	TextSize = 14, TextColor3 = KIN, TextTransparency = 1, Parent = rootFrame,
})

-- ------------------------------------------------------------- katana loader

local loader = make("Frame", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.66, 0),
	Size = UDim2.new(0, 640, 0, 60), BackgroundTransparency = 1, Parent = rootFrame,
})
-- kashira + tsuka (handle with gold diamonds)
make("Frame", {
	Position = UDim2.new(0, 0, 0.5, -8), Size = UDim2.new(0, 8, 0, 16),
	BackgroundColor3 = Color3.fromRGB(36, 29, 22), BorderSizePixel = 0, Parent = loader,
})
local tsuka = make("Frame", {
	Position = UDim2.new(0, 10, 0.5, -7), Size = UDim2.new(0, 66, 0, 14),
	BackgroundColor3 = Color3.fromRGB(23, 20, 26), BorderSizePixel = 0, Parent = loader,
})
for i = 1, 5 do
	local dia = make("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0, i * 11, 0.5, 0),
		Size = UDim2.new(0, 6, 0, 6), BackgroundColor3 = KIN,
		BackgroundTransparency = 0.35, BorderSizePixel = 0, Parent = tsuka,
	})
	dia.Rotation = 45
end
-- tsuba
local tsuba = make("Frame", {
	AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0, 82, 0.5, 0),
	Size = UDim2.new(0, 10, 0, 26), BackgroundColor3 = Color3.fromRGB(36, 29, 22),
	BorderSizePixel = 0, Parent = loader,
})
make("UICorner", { CornerRadius = UDim.new(1, 0), Parent = tsuba })
make("UIStroke", { Color = KIN, Thickness = 1, Transparency = 0.3, Parent = tsuba })
-- ghost blade (outline of what's still sheathed)
make("Frame", {
	Position = UDim2.new(0, 90, 0.5, -4), Size = UDim2.new(0, 540, 0, 8),
	BackgroundColor3 = WASHI, BackgroundTransparency = 0.94, BorderSizePixel = 0, Parent = loader,
})
-- revealed blade (the progress bar)
local bladeClip = make("Frame", {
	Position = UDim2.new(0, 90, 0.5, -5), Size = UDim2.new(0, 0, 0, 10),
	BackgroundTransparency = 1, ClipsDescendants = true, BorderSizePixel = 0, Parent = loader,
})
local blade = make("Frame", {
	Size = UDim2.new(0, 540, 0, 10), BackgroundColor3 = Color3.fromRGB(212, 216, 224),
	BorderSizePixel = 0, Parent = bladeClip,
})
local bladeGrad = make("UIGradient", {
	Color = ColorSequence.new({
		ColorSequenceKeypoint.new(0, Color3.fromRGB(190, 196, 206)),
		ColorSequenceKeypoint.new(0.5, Color3.fromRGB(244, 244, 240)),
		ColorSequenceKeypoint.new(1, Color3.fromRGB(212, 216, 224)),
	}),
	Parent = blade,
})
-- hamon line
make("Frame", {
	Position = UDim2.new(0, 0, 1, -3), Size = UDim2.new(1, 0, 0, 1),
	BackgroundColor3 = Color3.new(1, 1, 1), BackgroundTransparency = 0.4,
	BorderSizePixel = 0, Parent = blade,
})

-- stage + percent row
local stage = make("TextLabel", {
	Position = UDim2.new(0, 90, 1, -14), Size = UDim2.new(0, 400, 0, 16),
	BackgroundTransparency = 1, Font = Enum.Font.Garamond,
	Text = "AWAKENING THE WIND   ·   風を起こす", TextSize = 13,
	TextColor3 = WASHI, TextTransparency = 0.3,
	TextXAlignment = Enum.TextXAlignment.Left, Parent = loader,
})
local pct = make("TextLabel", {
	AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 1, -20),
	Size = UDim2.new(0, 100, 0, 26), BackgroundTransparency = 1,
	Font = Enum.Font.Garamond, Text = "0 %", TextSize = 24,
	TextColor3 = WASHI, TextXAlignment = Enum.TextXAlignment.Right, Parent = loader,
})

-- tips
local TIPS = {
	"A perfect parry costs nothing. Hesitation costs everything.",
	"Blocking raises your posture. When posture breaks — you break.",
	"Heavy strikes shatter a raised guard. Never stand and hold.",
	"The dash carries you through steel untouched, for a single breath.",
	"Every blade here is equal. Only the hand that holds it differs.",
	"Water stance widens the parry window. Wind stance quickens the feet.",
	"When their guard shatters, press E. End it with honor.",
	"The blade remembers what the hand has forgotten — wind through the bamboo.",
}
local tip = make("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.new(0.5, 0, 0.8, 0),
	Size = UDim2.new(0.7, 0, 0, 40), BackgroundTransparency = 1,
	Font = Enum.Font.Garamond, Text = "", TextSize = 15, TextWrapped = true,
	TextColor3 = WASHI, TextTransparency = 1, Parent = rootFrame,
})

local ready = make("TextLabel", {
	AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.new(0.5, 0, 0.88, 0),
	Size = UDim2.new(0.8, 0, 0, 24), BackgroundTransparency = 1,
	Font = Enum.Font.Garamond, Text = "刀を抜け   —   P R E S S   A N Y   K E Y   T O   D R A W   Y O U R   B L A D E",
	TextSize = 16, TextColor3 = WASHI, TextTransparency = 1, Parent = rootFrame,
})

-- ------------------------------------------------------------- intro animation

local function tw(inst, ti, goal)
	TweenService:Create(inst, ti, goal):Play()
end

task.spawn(function()
	task.wait(0.4)
	tw(ensoStroke, TweenInfo.new(2.2, Enum.EasingStyle.Sine), { Transparency = 0.84 })
	tw(sunLayers[1], TweenInfo.new(3, Enum.EasingStyle.Sine), { BackgroundTransparency = 0.12 })
	tw(sunLayers[2], TweenInfo.new(3.4, Enum.EasingStyle.Sine), { BackgroundTransparency = 0.78 })
	tw(sunLayers[3], TweenInfo.new(3.8, Enum.EasingStyle.Sine), { BackgroundTransparency = 0.92 })
	tw(mark, TweenInfo.new(4), { TextTransparency = 0.955 })
	task.wait(1)
	for i, lbl in ipairs(letters) do
		task.delay(i * 0.07, function()
			tw(lbl, TweenInfo.new(0.9, Enum.EasingStyle.Quart), { TextTransparency = 0 })
		end)
	end
	for i, lbl in ipairs(kanjiLabels) do
		task.delay(0.6 + i * 0.3, function()
			tw(lbl, TweenInfo.new(1.2), { TextTransparency = 0.18 })
		end)
	end
	task.wait(1.4)
	tw(subtitle, TweenInfo.new(1.4), { TextTransparency = 0.1 })
end)

-- tips rotator
task.spawn(function()
	local i = 0
	while gui.Parent do
		i = i + 1
		tip.Text = "心得 — " .. TIPS[(i - 1) % #TIPS + 1]
		tw(tip, TweenInfo.new(0.7), { TextTransparency = 0.15 })
		task.wait(4)
		tw(tip, TweenInfo.new(0.7), { TextTransparency = 1 })
		task.wait(0.8)
	end
end)

-- ------------------------------------------------------------- progress

local STAGES = {
	{ "AWAKENING THE WIND", "風を起こす" },
	{ "FOLDING STEEL — 1,000 LAYERS", "鋼を鍛える" },
	{ "PAINTING THE INK WORLD", "墨の世界を描く" },
	{ "BINDING THE FORMS", "型を結ぶ" },
	{ "SCATTERING CHERRY BLOSSOMS", "桜を散らす" },
	{ "TEACHING THE BLADE BUSHIDŌ", "武士道を教える" },
	{ "SHARPENING THE FINAL EDGE", "刃を研ぐ" },
}

local progress = 0
local shown = 0
local done = false

task.spawn(function()
	-- real work happens alongside the theatre
	task.spawn(function()
		pcall(function()
			ContentProvider:PreloadAsync({ playerGui })
		end)
	end)
	local t0 = os.clock()
	while progress < 100 do
		local step = 3 + math.random() * 11
		progress = math.min(100, progress + step)
		task.wait(0.25 + math.random() * 0.55)
		-- never finish before the world exists and the intro breathes
		if progress >= 100 then
			while (not game:IsLoaded() or os.clock() - t0 < 6.5) do
				task.wait(0.2)
			end
		end
	end
	done = true
	stage.Text = "THE BLADE IS DRAWN   ·   準備完了"
	tw(ready, TweenInfo.new(1), { TextTransparency = 0.2 })
	task.spawn(function()
		while done and gui.Parent do
			tw(ready, TweenInfo.new(0.9), { TextTransparency = 0.65 })
			task.wait(0.95)
			tw(ready, TweenInfo.new(0.9), { TextTransparency = 0.1 })
			task.wait(0.95)
		end
	end)
end)

local hb
hb = RunService.RenderStepped:Connect(function(dt)
	shown = shown + (progress - shown) * math.min(1, dt * 4)
	pct.Text = ("%d %%"):format(math.floor(shown))
	bladeClip.Size = UDim2.new(0, math.floor(540 * shown / 100), 0, 10)
	bladeGrad.Offset = Vector2.new((os.clock() * 0.4) % 1.5 - 0.75, 0)
	if not done then
		local idx = math.min(#STAGES, math.floor(shown / 100 * #STAGES) + 1)
		stage.Text = STAGES[idx][1] .. "   ·   " .. STAGES[idx][2]
	end
end)

-- ------------------------------------------------------------- katana-cut exit

local exiting = false
local function exit()
	if exiting or not done then return end
	exiting = true

	local shing = make("Sound", {
		SoundId = "rbxasset://sounds/unsheath.wav", Volume = 1, PlaybackSpeed = 1.2, Parent = gui,
	})
	shing:Play()

	-- white slash flash
	local flash = make("Frame", {
		AnchorPoint = Vector2.new(0.5, 0.5), Position = UDim2.new(0.5, 0, 0.5, 0),
		Size = UDim2.new(2, 0, 0, 6), Rotation = -14,
		BackgroundColor3 = Color3.new(1, 1, 1), BorderSizePixel = 0, Parent = gui,
	})
	tw(flash, TweenInfo.new(0.12, Enum.EasingStyle.Quad), { Size = UDim2.new(2, 0, 0, 90), BackgroundTransparency = 0.2 })
	task.delay(0.12, function()
		tw(flash, TweenInfo.new(0.3), { BackgroundTransparency = 1 })
	end)

	-- two halves fly apart
	rootFrame.BackgroundTransparency = 1
	local top = make("Frame", {
		AnchorPoint = Vector2.new(0.5, 1), Position = UDim2.new(0.5, 0, 0.52, 0),
		Size = UDim2.new(2.2, 0, 1.2, 0), Rotation = -14,
		BackgroundColor3 = LACQUER, BorderSizePixel = 0, ZIndex = 0, Parent = gui,
	})
	local bottom = make("Frame", {
		AnchorPoint = Vector2.new(0.5, 0), Position = UDim2.new(0.5, 0, 0.52, 0),
		Size = UDim2.new(2.2, 0, 1.2, 0), Rotation = -14,
		BackgroundColor3 = LACQUER, BorderSizePixel = 0, ZIndex = 0, Parent = gui,
	})
	-- hide the composed content instantly; the halves now represent the screen
	for _, child in ipairs(rootFrame:GetChildren()) do
		if child:IsA("GuiObject") then
			child.Visible = false
		end
	end
	local cutInfo = TweenInfo.new(0.9, Enum.EasingStyle.Quart, Enum.EasingDirection.In)
	tw(top, cutInfo, { Position = UDim2.new(0.62, 0, -0.9, 0), Rotation = -11 })
	tw(bottom, cutInfo, { Position = UDim2.new(0.38, 0, 1.94, 0), Rotation = -17 })
	task.delay(1.1, function()
		hb:Disconnect()
		gui:Destroy()
	end)
end

UserInputService.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.Keyboard
		or input.UserInputType == Enum.UserInputType.MouseButton1
		or input.UserInputType == Enum.UserInputType.Touch
		or input.UserInputType == Enum.UserInputType.Gamepad1 then
		exit()
	end
end)
]]></ProtectedString></Properties></Item></Item><Item class="ReplicatedStorage" referent="RBX10"><Properties><string name="Name">ReplicatedStorage</string></Properties><Item class="Folder" referent="RBX9"><Properties><string name="Name">Shared</string></Properties><Item class="ModuleScript" referent="RBX3"><Properties><string name="Name">AnimPoses</string><ProtectedString name="Source"><![CDATA[--[[
	Procedural animation clips for THE LAST SAMURAI.

	Poses are authored in CHARACTER SPACE, per abstract joint, in degrees:
	  x = pitch (positive tips the part's rest axis toward character-forward)
	  y = yaw   (positive turns toward the character's left)
	  z = roll
	  py = vertical translation offset in studs (Root only — crouch/kneel)

	Abstract joints: Root, Neck, RS (right shoulder), LS, RH (right hip), LH.
	The AnimationController conjugates these with each rig's cached base C0s,
	so the same data drives R6 players, R15 players and NPC rigs identically.

	Clip fields:
	  dur     — seconds
	  keys    — { { t = 0..1, pose = {...} }, ... }  (t is normalized)
	  trail   — { from, to } normalized window where the katana trail is enabled
	  loop    — repeats until replaced
	  hold    — freeze on the final key
]]

local AnimPoses = {}

local function K(t, pose)
	return { t = t, pose = pose }
end

AnimPoses.Guard = {
	dur = 2.6, loop = true,
	keys = {
		K(0.0, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
		K(0.5, { Root = { y = 14, py = -0.06 }, Neck = { y = -10 }, RS = { x = 46, y = -14 }, LS = { x = 33, y = 26 } }),
		K(1.0, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Sprint = {
	dur = 1, loop = true, upperOnly = true,
	keys = {
		K(0, { Root = { x = 10 }, RS = { x = -34, y = -18 }, LS = { x = 20, y = 14 } }),
		K(1, { Root = { x = 10 }, RS = { x = -34, y = -18 }, LS = { x = 20, y = 14 } }),
	},
}

AnimPoses.Slash1 = { -- fast horizontal, right to left
	dur = 0.42,
	trail = { 0.28, 0.62 },
	keys = {
		K(0.00, { Root = { y = 42, x = -6 },  Neck = { y = -24 }, RS = { x = 96, y = -55, z = 20 }, LS = { x = 40, y = 30 } }),
		K(0.34, { Root = { y = 34, x = -4 },  Neck = { y = -20 }, RS = { x = 100, y = -62, z = 22 }, LS = { x = 42, y = 30 } }),
		K(0.55, { Root = { y = -38, x = 14 }, Neck = { y = 18 },  RS = { x = 78, y = 48, z = -12 }, LS = { x = 30, y = 12 } }),
		K(0.78, { Root = { y = -30, x = 10 }, Neck = { y = 14 },  RS = { x = 70, y = 40 },          LS = { x = 30, y = 16 } }),
		K(1.00, { Root = { y = 14 },          Neck = { y = -10 }, RS = { x = 42, y = -14 },         LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Slash2 = { -- reverse cut, left to right
	dur = 0.42,
	trail = { 0.28, 0.62 },
	keys = {
		K(0.00, { Root = { y = -36, x = -4 }, Neck = { y = 18 },  RS = { x = 82, y = 46, z = -16 }, LS = { x = 26, y = 8 } }),
		K(0.34, { Root = { y = -40, x = -2 }, Neck = { y = 20 },  RS = { x = 86, y = 52, z = -18 }, LS = { x = 26, y = 8 } }),
		K(0.55, { Root = { y = 40, x = 14 },  Neck = { y = -22 }, RS = { x = 92, y = -58, z = 18 }, LS = { x = 40, y = 30 } }),
		K(0.78, { Root = { y = 30, x = 10 },  Neck = { y = -16 }, RS = { x = 84, y = -48, z = 14 }, LS = { x = 38, y = 28 } }),
		K(1.00, { Root = { y = 14 },          Neck = { y = -10 }, RS = { x = 42, y = -14 },         LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Slash3 = { -- diagonal from high right
	dur = 0.45,
	trail = { 0.3, 0.64 },
	keys = {
		K(0.00, { Root = { y = 26, x = -12 }, Neck = { y = -14, x = -8 }, RS = { x = 150, y = -30, z = 14 }, LS = { x = 50, y = 34 } }),
		K(0.36, { Root = { y = 28, x = -14 }, Neck = { y = -14, x = -8 }, RS = { x = 156, y = -34, z = 16 }, LS = { x = 52, y = 34 } }),
		K(0.58, { Root = { y = -24, x = 22 }, Neck = { y = 10, x = 10 },  RS = { x = 38, y = 30, z = -10 },  LS = { x = 24, y = 12 } }),
		K(0.80, { Root = { y = -18, x = 16 }, Neck = { y = 8 },           RS = { x = 40, y = 24 },           LS = { x = 26, y = 16 } }),
		K(1.00, { Root = { y = 14 },          Neck = { y = -10 },         RS = { x = 42, y = -14 },          LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Slash4 = { -- finisher: full overhead smash
	dur = 0.62,
	trail = { 0.34, 0.66 },
	keys = {
		K(0.00, { Root = { x = -18, y = 8 },  Neck = { x = -12 }, RS = { x = 172, y = -8 },  LS = { x = 150, y = 14 } }),
		K(0.38, { Root = { x = -22, y = 8, py = -0.15 }, Neck = { x = -14 }, RS = { x = 178, y = -8 }, LS = { x = 156, y = 14 } }),
		K(0.56, { Root = { x = 34, y = -6, py = -0.3 },  Neck = { x = 16 },  RS = { x = 26, y = 4 },   LS = { x = 20, y = 10 } }),
		K(0.80, { Root = { x = 26, y = -4, py = -0.2 },  Neck = { x = 12 },  RS = { x = 30, y = 2 },   LS = { x = 24, y = 12 } }),
		K(1.00, { Root = { y = 14 },          Neck = { y = -10 }, RS = { x = 42, y = -14 },  LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.HeavyCharge = {
	dur = 1.2, hold = true,
	keys = {
		K(0.0, { Root = { y = 14 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
		K(0.3, { Root = { x = -14, y = 10 }, Neck = { x = -10 }, RS = { x = 164, y = -12 }, LS = { x = 140, y = 12 } }),
		K(1.0, { Root = { x = -16, y = 10, py = -0.12 }, Neck = { x = -12 }, RS = { x = 170, y = -12 }, LS = { x = 146, y = 12 } }),
	},
}

AnimPoses.HeavyRelease = {
	dur = 0.55,
	trail = { 0.1, 0.5 },
	keys = {
		K(0.00, { Root = { x = -16, y = 10 }, Neck = { x = -12 }, RS = { x = 170, y = -12 }, LS = { x = 146, y = 12 } }),
		K(0.40, { Root = { x = 38, y = -8, py = -0.4 }, Neck = { x = 18 }, RS = { x = 20, y = 6 }, LS = { x = 16, y = 10 } }),
		K(0.70, { Root = { x = 30, y = -6, py = -0.25 }, Neck = { x = 14 }, RS = { x = 26, y = 4 }, LS = { x = 20, y = 12 } }),
		K(1.00, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.DashAttack = { -- lunging thrust
	dur = 0.45,
	trail = { 0.2, 0.5 },
	keys = {
		K(0.00, { Root = { y = 30, x = 6 }, RS = { x = 52, y = -30 }, LS = { x = 36, y = 28 } }),
		K(0.34, { Root = { y = -34, x = 22, py = -0.3 }, Neck = { y = 16 }, RS = { x = 88, y = 8 }, LS = { x = 20, y = 20 } }),
		K(0.66, { Root = { y = -28, x = 18, py = -0.2 }, Neck = { y = 12 }, RS = { x = 84, y = 6 }, LS = { x = 22, y = 20 } }),
		K(1.00, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Block = {
	dur = 0.14, hold = true,
	keys = {
		K(0, { Root = { y = 14 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
		K(1, { Root = { y = 20, x = 4, py = -0.1 }, Neck = { y = -12 }, RS = { x = 84, y = -34, z = 42 }, LS = { x = 70, y = 38 } }),
	},
}

AnimPoses.ParryFlash = { -- the deflect itself: blade snaps outward
	dur = 0.3,
	keys = {
		K(0.0, { Root = { y = 20, x = 4 }, RS = { x = 84, y = -34, z = 42 }, LS = { x = 70, y = 38 } }),
		K(0.3, { Root = { y = -10, x = -4 }, RS = { x = 108, y = 30, z = -20 }, LS = { x = 60, y = 20 } }),
		K(1.0, { Root = { y = 20, x = 4 }, RS = { x = 84, y = -34, z = 42 }, LS = { x = 70, y = 38 } }),
	},
}

AnimPoses.Stagger = {
	dur = 0.45,
	keys = {
		K(0.0, { Root = { x = -26, y = 16 }, Neck = { x = -16 }, RS = { x = -10, y = -30 }, LS = { x = -6, y = 24 } }),
		K(0.5, { Root = { x = -30, y = 20, py = -0.1 }, Neck = { x = -18 }, RS = { x = -14, y = -34 }, LS = { x = -8, y = 26 } }),
		K(1.0, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.Hurt = {
	dur = 0.24,
	keys = {
		K(0.0, { Root = { x = -16, y = -6 }, Neck = { x = -12 }, RS = { x = 28, y = -10 } }),
		K(1.0, { Root = { y = 14 }, Neck = { y = -10 }, RS = { x = 42, y = -14 }, LS = { x = 30, y = 26 } }),
	},
}

AnimPoses.GuardBroken = {
	dur = 1.0, loop = true,
	keys = {
		K(0.0, { Root = { x = -28, py = -0.5 }, Neck = { x = -22 }, RS = { x = -24, y = -36 }, LS = { x = -20, y = 32 } }),
		K(0.5, { Root = { x = -24, y = 6, py = -0.55 }, Neck = { x = -20 }, RS = { x = -20, y = -32 }, LS = { x = -16, y = 30 } }),
		K(1.0, { Root = { x = -28, py = -0.5 }, Neck = { x = -22 }, RS = { x = -24, y = -36 }, LS = { x = -20, y = 32 } }),
	},
}

AnimPoses.Dash = {
	dur = 0.24, hold = true,
	keys = {
		K(0, { Root = { x = 30, py = -0.7 }, Neck = { x = 6 }, RS = { x = 10, y = -20 }, LS = { x = -20, y = 20 } }),
		K(1, { Root = { x = 30, py = -0.7 }, Neck = { x = 6 }, RS = { x = 10, y = -20 }, LS = { x = -20, y = 20 } }),
	},
}

AnimPoses.ExecuteAttacker = {
	dur = 1.6, hold = true,
	trail = { 0.18, 0.34 },
	keys = {
		K(0.00, { Root = { y = 30, x = 4 }, RS = { x = 60, y = -30 }, LS = { x = 40, y = 28 } }),
		K(0.22, { Root = { y = -38, x = 26, py = -0.5 }, Neck = { y = 16 }, RS = { x = 92, y = 10 }, LS = { x = 18, y = 18 } }),
		K(0.45, { Root = { y = -20, x = 10, py = -0.2 }, RS = { x = 60, y = 0 }, LS = { x = 24, y = 20 } }),
		K(0.80, { Root = { y = 8, x = 4 }, Neck = { x = 4 }, RS = { x = 20, y = -30, z = 30 }, LS = { x = 30, y = 30 } }),
		K(1.00, { Root = { y = 8, x = 4 }, Neck = { x = 4 }, RS = { x = 20, y = -30, z = 30 }, LS = { x = 30, y = 30 } }),
	},
}

AnimPoses.ExecuteVictim = { -- kneel, then fall
	dur = 1.6, hold = true,
	keys = {
		K(0.00, { Root = { x = 10 } }),
		K(0.25, { Root = { x = 16, py = -1.5 }, Neck = { x = 22 }, RH = { x = -96 }, LH = { x = -96 }, RS = { x = 8 }, LS = { x = 8 } }),
		K(0.85, { Root = { x = 16, py = -1.5 }, Neck = { x = 26 }, RH = { x = -96 }, LH = { x = -96 } }),
		K(1.00, { Root = { x = 70, py = -2.2 }, Neck = { x = 30 }, RH = { x = -96 }, LH = { x = -96 } }),
	},
}

AnimPoses.Death = {
	dur = 0.8, hold = true,
	keys = {
		K(0.0, { Root = { x = -10 } }),
		K(1.0, { Root = { x = -74, py = -2.4 }, Neck = { x = -20 }, RS = { x = -30, y = -40 }, LS = { x = -26, y = 36 } }),
	},
}

-- Map combat attack keys to clips
AnimPoses.AttackClips = {
	L1 = "Slash1", L2 = "Slash2", L3 = "Slash3", L4 = "Slash4",
	Heavy = "HeavyRelease", Dash = "DashAttack",
}

return AnimPoses
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX4"><Properties><string name="Name">Config</string><ProtectedString name="Source"><![CDATA[--[[
	THE LAST SAMURAI — master tuning table.
	Single source of truth for combat, stances, weather and progression.
	Server is authoritative over everything in Combat/Attacks/Stances.
]]

local Config = {}

Config.Combat = {
	MaxHP = 100,
	MaxPosture = 100,
	MaxStamina = 100,
	MaxUlt = 100,

	ParryWindow = 0.16,          -- seconds after block start that count as a perfect parry
	ParryPostureToAttacker = 24, -- posture the attacker eats when parried
	ParryPostureRefund = 15,     -- posture the defender sheds on a parry
	ParryUltGain = 16,
	BlockChip = 0.15,            -- fraction of damage that leaks through a block
	BlockStaminaCost = 6,        -- flat stamina per blocked hit
	GuardBreakStun = 1.8,
	PostureAfterBreak = 45,
	PostureDecayDelay = 1.1,
	PostureDecayRate = 9,        -- per second once decay starts

	StaminaRegen = 22,           -- per second
	StaminaRegenBlocking = 9,
	SprintDrain = 8,             -- per second
	SprintMinStamina = 5,

	BackstabMult = 1.35,
	HitUltGainAttacker = 5,
	HitUltGainVictim = 3,

	ExecuteHPThreshold = 15,     -- or posture broken
	ExecuteRange = 12,
	ExecuteDuration = 1.6,

	DashCost = 18,
	DashCooldown = 0.9,
	DashDuration = 0.22,
	DashSpeed = 85,              -- studs/second while dashing
	DashIFrames = 0.20,

	WalkSpeed = 12,
	SprintSpeed = 22,
	BlockWalkSpeed = 6,
	StaggerDuration = 0.45,
	HurtStun = 0.22,

	ComboChainWindow = 0.55,     -- seconds after an attack ends in which the combo continues
	HeavyChargeMax = 1.2,
	HeavyChargeBonus = 0.4,      -- max +40% damage at full charge

	RespawnTime = 4,
}

-- windup = seconds from press to the blade connecting (the parry read window)
-- recover = seconds after the hit before you can act again
Config.Attacks = {
	L1 =    { damage = 7,  posture = 9,  range = 10, angle = 75, windup = 0.18, recover = 0.24, stamina = 5, nextKey = "L2", knock = 8 },
	L2 =    { damage = 7,  posture = 9,  range = 10, angle = 75, windup = 0.18, recover = 0.24, stamina = 5, nextKey = "L3", knock = 8 },
	L3 =    { damage = 8,  posture = 10, range = 10.5, angle = 75, windup = 0.19, recover = 0.26, stamina = 5, nextKey = "L4", knock = 10 },
	L4 =    { damage = 12, posture = 15, range = 11, angle = 80, windup = 0.24, recover = 0.42, stamina = 7, knock = 26, finisher = true },
	Heavy = { damage = 16, posture = 34, range = 11.5, angle = 80, windup = 0.30, recover = 0.48, stamina = 22, knock = 30, guardBreak = true },
	Dash =  { damage = 9,  posture = 8,  range = 12, angle = 60, windup = 0.14, recover = 0.30, stamina = 6, knock = 12 },
	UltHit ={ damage = 9,  posture = 12, range = 14, angle = 360, windup = 0,   recover = 0,    stamina = 0, knock = 6 },
}

Config.Ult = {
	Hits = 3,
	Interval = 0.28,
	Range = 22,
	Cost = 100,
}

-- Every stance is a sidegrade: speed / damage / posture pressure / parry forgiveness.
Config.Stances = {
	{ id = "Stone", kanji = "岩", desc = "Unbreakable guard",  speed = 0.88, damage = 1.06, posture = 1.35, parryWindow = 1.0,  blockCost = 0.55 },
	{ id = "Wind",  kanji = "風", desc = "Swift feet",         speed = 1.22, damage = 0.82, posture = 0.90, parryWindow = 1.0,  blockCost = 1.0 },
	{ id = "Water", kanji = "水", desc = "Precise parries",    speed = 1.00, damage = 1.00, posture = 1.00, parryWindow = 1.55, blockCost = 0.85 },
	{ id = "Flame", kanji = "炎", desc = "All or nothing",     speed = 1.06, damage = 1.28, posture = 1.10, parryWindow = 0.80, blockCost = 1.4 },
}

Config.Weather = {
	{ id = "BloodSunset", kanji = "夕焼け", name = "BLOOD SUNSET",
		clock = 17.6, ambient = Color3.fromRGB(96, 48, 44), outdoorAmbient = Color3.fromRGB(140, 74, 58),
		fogColor = Color3.fromRGB(155, 74, 52), fogEnd = 900, haze = 1.8, density = 0.34,
		atmoColor = Color3.fromRGB(199, 111, 80), atmoDecay = Color3.fromRGB(92, 34, 42),
		rain = false, snow = false, petals = true, fireflies = false, brightness = 2.4 },
	{ id = "FullMoon", kanji = "満月", name = "FULL MOON",
		clock = 0, ambient = Color3.fromRGB(28, 34, 52), outdoorAmbient = Color3.fromRGB(48, 60, 92),
		fogColor = Color3.fromRGB(16, 22, 38), fogEnd = 700, haze = 1.2, density = 0.3,
		atmoColor = Color3.fromRGB(70, 86, 120), atmoDecay = Color3.fromRGB(18, 24, 44),
		rain = false, snow = false, petals = false, fireflies = true, brightness = 1.2 },
	{ id = "Storm", kanji = "雷雨", name = "STORM",
		clock = 15, ambient = Color3.fromRGB(44, 50, 58), outdoorAmbient = Color3.fromRGB(70, 80, 92),
		fogColor = Color3.fromRGB(52, 60, 70), fogEnd = 420, haze = 2.6, density = 0.45,
		atmoColor = Color3.fromRGB(110, 122, 136), atmoDecay = Color3.fromRGB(40, 48, 58),
		rain = true, snow = false, petals = false, fireflies = false, brightness = 1.4, thunder = true },
	{ id = "FirstSnow", kanji = "初雪", name = "FIRST SNOW",
		clock = 14.5, ambient = Color3.fromRGB(78, 86, 102), outdoorAmbient = Color3.fromRGB(120, 130, 150),
		fogColor = Color3.fromRGB(140, 148, 164), fogEnd = 520, haze = 2.2, density = 0.4,
		atmoColor = Color3.fromRGB(168, 176, 192), atmoDecay = Color3.fromRGB(90, 100, 120),
		rain = false, snow = true, petals = false, fireflies = false, brightness = 1.8 },
	{ id = "GhostFog", kanji = "霧", name = "GHOST FOG",
		clock = 6.4, ambient = Color3.fromRGB(70, 70, 66), outdoorAmbient = Color3.fromRGB(110, 108, 100),
		fogColor = Color3.fromRGB(120, 118, 110), fogEnd = 220, haze = 3.4, density = 0.58,
		atmoColor = Color3.fromRGB(150, 148, 138), atmoDecay = Color3.fromRGB(84, 82, 76),
		rain = false, snow = false, petals = true, fireflies = false, brightness = 1.5 },
}
Config.WeatherCycleSeconds = 240

Config.AI = {
	Dummy = { hp = 200, aggro = 0, parryChance = 0, reaction = 1 },
	Ronin = {
		{ id = "Initiate", hp = 100, aggro = 0.45, parryChance = 0.10, blockChance = 0.35, reaction = 0.34, comboMax = 2, engageRange = 60 },
		{ id = "Ronin",    hp = 100, aggro = 0.68, parryChance = 0.28, blockChance = 0.55, reaction = 0.22, comboMax = 3, engageRange = 70 },
		{ id = "Shogun",   hp = 110, aggro = 0.85, parryChance = 0.50, blockChance = 0.70, reaction = 0.15, comboMax = 4, engageRange = 80 },
	},
	Names = {
		{ "紅風", "Kenji of the Red Wind" },
		{ "夜叉", "The Yasha of Three Rivers" },
		{ "無音", "Muon, the Soundless" },
		{ "鬼火", "Onibi the Lantern-Eater" },
		{ "枯山", "Kareyama the Withered Peak" },
	},
}

Config.Rewards = {
	KillCoins = 50, KillXP = 100,
	ParryCoins = 2,
	ExecuteBonusCoins = 25,
	WinCoins = 150,
	XPPerLevel = 500, -- level n requires n * XPPerLevel total
}

Config.Palette = {
	Lacquer = Color3.fromRGB(11, 10, 13),
	Washi = Color3.fromRGB(233, 226, 210),
	Shu = Color3.fromRGB(179, 32, 46),
	Kin = Color3.fromRGB(217, 164, 91),
	Steel = Color3.fromRGB(126, 143, 166),
	Crimson = Color3.fromRGB(140, 47, 57),
}

return Config
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX5"><Properties><string name="Name">KatanaBuilder</string><ProtectedString name="Source"><![CDATA[--[[
	Builds a katana entirely from parts — no meshes, no uploaded assets.
	Built along +Y: kashira at the bottom, kissaki (tip) at the top.
	The server attaches it to a character's right hand with a Motor6D named "Grip";
	that Motor replicates, so every client sees the blade and its Trail.
]]

local Util = require(script.Parent:WaitForChild("Util"))

local KatanaBuilder = {}

local function p(props)
	props.Anchored = false
	props.CanCollide = false
	props.CanQuery = false
	props.CanTouch = false
	props.Massless = true
	return Util.make("Part", props)
end

function KatanaBuilder.build()
	local model = Instance.new("Model")
	model.Name = "Katana"

	-- tsuka (handle) — this is the root the Grip motor drives
	local handle = p({
		Name = "Handle", Size = Vector3.new(0.22, 1.1, 0.22),
		Color = Color3.fromRGB(23, 20, 26), Material = Enum.Material.Fabric,
		Parent = model,
	})

	-- kashira (pommel cap)
	local kashira = p({
		Name = "Kashira", Size = Vector3.new(0.26, 0.12, 0.26),
		Color = Color3.fromRGB(217, 164, 91), Material = Enum.Material.Metal,
		Parent = model,
	})
	Util.make("WeldConstraint", { Part0 = handle, Part1 = kashira, Parent = kashira })
	kashira.CFrame = handle.CFrame * CFrame.new(0, -0.58, 0)

	-- tsuba (guard)
	local tsuba = p({
		Name = "Tsuba", Shape = Enum.PartType.Cylinder, Size = Vector3.new(0.08, 0.62, 0.62),
		Color = Color3.fromRGB(36, 29, 22), Material = Enum.Material.Metal,
		Parent = model,
	})
	tsuba.CFrame = handle.CFrame * CFrame.new(0, 0.58, 0) * CFrame.Angles(0, 0, math.rad(90))
	Util.make("WeldConstraint", { Part0 = handle, Part1 = tsuba, Parent = tsuba })

	-- blade
	local blade = p({
		Name = "Blade", Size = Vector3.new(0.14, 3.3, 0.34),
		Color = Color3.fromRGB(212, 216, 224), Material = Enum.Material.Metal,
		Reflectance = 0.28,
		Parent = model,
	})
	blade.CFrame = handle.CFrame * CFrame.new(0, 0.62 + 1.65, 0)
	Util.make("WeldConstraint", { Part0 = handle, Part1 = blade, Parent = blade })

	-- kissaki (tip)
	local tip = Util.make("WedgePart", {
		Name = "Kissaki", Size = Vector3.new(0.14, 0.5, 0.34),
		Color = Color3.fromRGB(220, 224, 232), Material = Enum.Material.Metal,
		Reflectance = 0.3, Anchored = false, CanCollide = false, CanQuery = false,
		CanTouch = false, Massless = true, Parent = model,
	})
	tip.CFrame = blade.CFrame * CFrame.new(0, 1.65 + 0.25, 0) * CFrame.Angles(math.rad(180), 0, 0)
	Util.make("WeldConstraint", { Part0 = blade, Part1 = tip, Parent = tip })

	-- hamon line (subtle temper line along the edge)
	local hamon = p({
		Name = "Hamon", Size = Vector3.new(0.15, 3.3, 0.05),
		Color = Color3.fromRGB(244, 244, 240), Material = Enum.Material.SmoothPlastic,
		Transparency = 0.35,
		Parent = model,
	})
	hamon.CFrame = blade.CFrame * CFrame.new(0, 0, 0.14)
	Util.make("WeldConstraint", { Part0 = blade, Part1 = hamon, Parent = hamon })

	-- trail attachments: base and tip of the blade
	local a0 = Util.make("Attachment", { Name = "TrailA", Position = Vector3.new(0, -1.6, 0), Parent = blade })
	local a1 = Util.make("Attachment", { Name = "TrailB", Position = Vector3.new(0, 1.9, 0), Parent = blade })

	-- thin, air-distortion style trail: white, short-lived, no neon
	Util.make("Trail", {
		Name = "SwingTrail",
		Attachment0 = a0, Attachment1 = a1,
		Lifetime = 0.14,
		MinLength = 0.05,
		FaceCamera = true,
		LightInfluence = 0,
		Color = ColorSequence.new(Color3.fromRGB(240, 242, 246)),
		Transparency = NumberSequence.new({
			NumberSequenceKeypoint.new(0, 0.55),
			NumberSequenceKeypoint.new(1, 1),
		}),
		WidthScale = NumberSequence.new({
			NumberSequenceKeypoint.new(0, 1),
			NumberSequenceKeypoint.new(1, 0.25),
		}),
		Enabled = false,
		Parent = blade,
	})

	model.PrimaryPart = handle
	return model
end

-- Attaches a katana to a character's right hand via Motor6D "Grip".
-- Works for R6 ("Right Arm") and R15 ("RightHand").
function KatanaBuilder.attach(character)
	local hand = character:FindFirstChild("Right Arm") or character:FindFirstChild("RightHand")
	if not hand then
		return nil
	end
	local existing = character:FindFirstChild("Katana")
	if existing then
		existing:Destroy()
	end
	local katana = KatanaBuilder.build()
	katana.Parent = character

	local isR6 = character:FindFirstChild("Right Arm") ~= nil
	local gripOffset
	if isR6 then
		-- hand is at the bottom of the arm; blade up (+Y) rotated to face forward
		gripOffset = CFrame.new(0, -1.05, 0) * CFrame.Angles(math.rad(-90), 0, 0)
	else
		gripOffset = CFrame.new(0, -0.15, 0) * CFrame.Angles(math.rad(-90), 0, 0)
	end

	Util.make("Motor6D", {
		Name = "Grip",
		Part0 = hand,
		Part1 = katana.PrimaryPart,
		C0 = gripOffset,
		Parent = hand,
	})
	return katana
end

return KatanaBuilder
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX6"><Properties><string name="Name">Net</string><ProtectedString name="Source"><![CDATA[--[[
	Networking layer. The server creates every remote once; clients wait for them.

	Remotes:
	  Action   (Client -> Server)  combat intents { a = "light" | "heavy_start" | ... }
	  Combat   (Server -> All)     combat presentation events (drives anims/VFX/SFX everywhere)
	  State    (Server -> Player)  own vitals { hp, po, st, ult, stance }
	  Weather  (Server -> All)     { index = n } current weather preset
	  Announce (Server -> All/1)   { kanji, text, dur }
	  KillFeed (Server -> All)     { killer, victim, executed }
	  Actors   (Server -> All)     actor registry sync { id, model, name } / { id, gone = true }
]]

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

local REMOTE_NAMES = { "Action", "Combat", "State", "Weather", "Announce", "KillFeed", "Actors" }

local Net = {}

local function getFolder()
	if RunService:IsServer() then
		local folder = ReplicatedStorage:FindFirstChild("TLS_Remotes")
		if not folder then
			folder = Instance.new("Folder")
			folder.Name = "TLS_Remotes"
			folder.Parent = ReplicatedStorage
		end
		return folder
	end
	return ReplicatedStorage:WaitForChild("TLS_Remotes")
end

function Net.get()
	local folder = getFolder()
	local remotes = {}
	for _, name in ipairs(REMOTE_NAMES) do
		if RunService:IsServer() then
			local r = folder:FindFirstChild(name)
			if not r then
				r = Instance.new("RemoteEvent")
				r.Name = name
				r.Parent = folder
			end
			remotes[name] = r
		else
			remotes[name] = folder:WaitForChild(name)
		end
	end
	return remotes
end

return Net
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX7"><Properties><string name="Name">Sounds</string><ProtectedString name="Source"><![CDATA[--[[
	Sound map. Core combat audio uses rbxasset:// sounds that ship inside the
	Roblox engine itself, so they can never be moderated or fail to load.
	Ambience slots point at the same built-ins pitched into new roles; every
	entry can be swapped for a licensed catalog id ("rbxassetid://...") later
	without touching any other code.
]]

local Sounds = {}

Sounds.Map = {
	Slash =      { id = "rbxasset://sounds/swordslash.wav",  volume = 0.6, pitch = { 0.95, 1.1 } },
	SlashHeavy = { id = "rbxasset://sounds/swordslash.wav",  volume = 0.8, pitch = { 0.7, 0.8 } },
	Lunge =      { id = "rbxasset://sounds/swordlunge.wav",  volume = 0.7, pitch = { 0.95, 1.05 } },
	Unsheath =   { id = "rbxasset://sounds/unsheath.wav",    volume = 0.7, pitch = { 1, 1 } },
	Clash =      { id = "rbxasset://sounds/swordslash.wav",  volume = 0.9, pitch = { 1.35, 1.55 } },
	Parry =      { id = "rbxasset://sounds/unsheath.wav",    volume = 1,   pitch = { 1.5, 1.6 } },
	ParryRing =  { id = "rbxasset://sounds/electronicpingshort.wav", volume = 0.25, pitch = { 0.6, 0.65 } },
	Thud =       { id = "rbxasset://sounds/snap.mp3",        volume = 0.7, pitch = { 0.55, 0.7 } },
	GuardBreak = { id = "rbxasset://sounds/snap.mp3",        volume = 1,   pitch = { 0.4, 0.45 } },
	Dash =       { id = "rbxasset://sounds/swordlunge.wav",  volume = 0.4, pitch = { 1.3, 1.45 } },
	Bell =       { id = "rbxasset://sounds/electronicpingshort.wav", volume = 0.4, pitch = { 0.32, 0.34 } },
	Execute =    { id = "rbxasset://sounds/unsheath.wav",    volume = 1,   pitch = { 0.8, 0.85 } },
	UIClick =    { id = "rbxasset://sounds/button.wav",      volume = 0.5, pitch = { 1, 1 } },
}

-- Looping ambience (wind through bamboo faked with pitched-down rocket whoosh
-- would be unreliable across engine versions, so ambience loops are optional:
-- drop catalog ids in here and the SoundController picks them up automatically).
Sounds.Ambience = {
	Wind = { id = "", volume = 0.25 },
	Rain = { id = "", volume = 0.5 },
	NightInsects = { id = "", volume = 0.3 },
}

return Sounds
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX8"><Properties><string name="Name">Util</string><ProtectedString name="Source"><![CDATA[--[[
	Small shared utilities: Signal, math helpers, instance builder.
]]

local Util = {}

-- ---------- Signal (lightweight, connection objects with Disconnect) ----------
local Signal = {}
Signal.__index = Signal

function Signal.new()
	return setmetatable({ _handlers = {} }, Signal)
end

function Signal:Connect(fn)
	local handlers = self._handlers
	handlers[#handlers + 1] = fn
	local conn = {}
	function conn.Disconnect()
		for i = #handlers, 1, -1 do
			if handlers[i] == fn then
				table.remove(handlers, i)
				break
			end
		end
	end
	conn.disconnect = conn.Disconnect
	return conn
end

function Signal:Fire(...)
	for _, fn in ipairs(self._handlers) do
		task.spawn(fn, ...)
	end
end

Util.Signal = Signal

-- ---------- math ----------
function Util.lerp(a, b, t)
	return a + (b - a) * t
end

function Util.clamp(v, lo, hi)
	if v < lo then return lo end
	if v > hi then return hi end
	return v
end

function Util.ease(t)
	if t < 0.5 then
		return 2 * t * t
	end
	return 1 - math.pow(-2 * t + 2, 2) / 2
end

-- ---------- instance builder ----------
-- Util.make("Part", { Name = "X", Parent = ..., Size = ... , Children = {...} })
function Util.make(className, props)
	local inst = Instance.new(className)
	local parent = nil
	for k, v in pairs(props or {}) do
		if k == "Parent" then
			parent = v
		elseif k == "Children" then
			for _, child in ipairs(v) do
				child.Parent = inst
			end
		else
			inst[k] = v
		end
	end
	if parent then
		inst.Parent = parent
	end
	return inst
end

-- ---------- misc ----------
function Util.flatDirection(v)
	local flat = Vector3.new(v.X, 0, v.Z)
	if flat.Magnitude < 0.001 then
		return Vector3.new(0, 0, -1)
	end
	return flat.Unit
end

function Util.yawCFrame(position, lookAt)
	local dir = Util.flatDirection(lookAt - position)
	return CFrame.lookAt(position, position + dir)
end

function Util.retry(times, delaySeconds, fn, ...)
	local lastErr
	for _ = 1, times do
		local ok, res = pcall(fn, ...)
		if ok then
			return true, res
		end
		lastErr = res
		task.wait(delaySeconds)
	end
	return false, lastErr
end

return Util
]]></ProtectedString></Properties></Item></Item></Item><Item class="ServerScriptService" referent="RBX18"><Properties><string name="Name">ServerScriptService</string></Properties><Item class="Script" referent="RBX17"><Properties><string name="Name">Server</string><ProtectedString name="Source"><![CDATA[--[[
	THE LAST SAMURAI — server bootstrap.
	Order matters: remotes -> data -> combat -> world -> weather -> AI -> anticheat.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Net = require(Shared:WaitForChild("Net"))
local Config = require(Shared:WaitForChild("Config"))

local DataService = require(script:WaitForChild("DataService"))
local CombatService = require(script:WaitForChild("CombatService"))
local MapService = require(script:WaitForChild("MapService"))
local WeatherService = require(script:WaitForChild("WeatherService"))
local AIService = require(script:WaitForChild("AIService"))
local AntiCheat = require(script:WaitForChild("AntiCheat"))

local remotes = Net.get()

Players.RespawnTime = Config.Combat.RespawnTime

DataService.init()
CombatService.init(remotes, DataService)
MapService.build()
WeatherService.init(remotes, CombatService)
AIService.init(remotes, CombatService, MapService)
AntiCheat.init()

-- kill rewards
CombatService.ActorDied:Connect(function(victimId, killerId, executed)
	if killerId and killerId > 0 then
		local killer = Players:GetPlayerByUserId(killerId)
		if killer then
			local mult = WeatherService.isBloodMoon() and 2 or 1
			DataService.award(killer, {
				coins = Config.Rewards.KillCoins * mult,
				xp = Config.Rewards.KillXP * mult,
				kills = 1,
			})
		end
	end
end)

-- level-up fanfare
DataService.onLevelUp = function(player, newLevel)
	remotes.Announce:FireClient(player, {
		kanji = "昇段", text = ("LEVEL %d"):format(newLevel), dur = 3,
	})
end

-- keep the AI's fog handicap in sync with the weather
task.spawn(function()
	while true do
		AIService.setWeatherIndex(WeatherService.Index)
		task.wait(2)
	end
end)

-- simple chat commands: /weather cycles presets
Players.PlayerAdded:Connect(function(player)
	player.Chatted:Connect(function(msg)
		if msg:lower() == "/weather" then
			WeatherService.next()
		end
	end)
end)

print("[THE LAST SAMURAI] server ready — 始め!")
]]></ProtectedString></Properties><Item class="ModuleScript" referent="RBX11"><Properties><string name="Name">AIService</string><ProtectedString name="Source"><![CDATA[--[[
	AIService — ronin duelists and dojo training dummies.
	Rigs are R6 skeletons built entirely from parts (canonical Motor6D C0/C1s,
	so the shared procedural animation system drives them exactly like players).
	Brains read the same combat event stream players do: they block and parry
	REACTIVELY to real attack windups, with human reaction delays and error.
]]

local Workspace = game:GetService("Workspace")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))

local AIService = {}
local combatService
local brains = {} -- actorId -> brain

local INK = Color3.fromRGB(24, 19, 26)
local SKIN = Color3.fromRGB(216, 176, 141)
local STRAW = Color3.fromRGB(178, 152, 96)
local CRIMSON = Color3.fromRGB(140, 47, 57)

-- ------------------------------------------------------------- rig builder

local function limb(name, size, color, parent)
	return Util.make("Part", {
		Name = name, Size = size, Color = color,
		Material = Enum.Material.Fabric,
		Anchored = false, CanCollide = false,
		TopSurface = Enum.SurfaceType.Smooth, BottomSurface = Enum.SurfaceType.Smooth,
		Parent = parent,
	})
end

local function motor(name, p0, p1, c0, c1)
	return Util.make("Motor6D", { Name = name, Part0 = p0, Part1 = p1, C0 = c0, C1 = c1, Parent = p0 })
end

function AIService.buildRig(name, cframe)
	local model = Instance.new("Model")
	model.Name = name

	local hrp = Util.make("Part", {
		Name = "HumanoidRootPart", Size = Vector3.new(2, 2, 1),
		Transparency = 1, Anchored = false, CanCollide = true,
		Parent = model,
	})
	local torso = limb("Torso", Vector3.new(2, 2, 1), INK, model)
	local head = limb("Head", Vector3.new(2, 1, 1), SKIN, model)
	Util.make("SpecialMesh", { MeshType = Enum.MeshType.Head, Scale = Vector3.new(1.25, 1.25, 1.25), Parent = head })
	local ra = limb("Right Arm", Vector3.new(1, 2, 1), INK, model)
	local la = limb("Left Arm", Vector3.new(1, 2, 1), INK, model)
	local rl = limb("Right Leg", Vector3.new(1, 2, 1), INK, model)
	local ll = limb("Left Leg", Vector3.new(1, 2, 1), INK, model)

	-- canonical R6 joints (identical to a real player rig)
	local ang = CFrame.Angles
	motor("RootJoint", hrp, torso,
		CFrame.new(0, 0, 0) * ang(-math.pi / 2, 0, math.pi),
		CFrame.new(0, 0, 0) * ang(-math.pi / 2, 0, math.pi))
	motor("Neck", torso, head,
		CFrame.new(0, 1, 0) * ang(-math.pi / 2, 0, math.pi),
		CFrame.new(0, -0.5, 0) * ang(-math.pi / 2, 0, math.pi))
	motor("Right Shoulder", torso, ra,
		CFrame.new(1, 0.5, 0) * ang(0, math.pi / 2, 0),
		CFrame.new(-0.5, 0.5, 0) * ang(0, math.pi / 2, 0))
	motor("Left Shoulder", torso, la,
		CFrame.new(-1, 0.5, 0) * ang(0, -math.pi / 2, 0),
		CFrame.new(0.5, 0.5, 0) * ang(0, -math.pi / 2, 0))
	motor("Right Hip", torso, rl,
		CFrame.new(1, -1, 0) * ang(0, math.pi / 2, 0),
		CFrame.new(0.5, 1, 0) * ang(0, math.pi / 2, 0))
	motor("Left Hip", torso, ll,
		CFrame.new(-1, -1, 0) * ang(0, -math.pi / 2, 0),
		CFrame.new(-0.5, 1, 0) * ang(0, -math.pi / 2, 0))

	-- crimson obi sash
	local sash = limb("Sash", Vector3.new(2.06, 0.5, 1.06), CRIMSON, model)
	Util.make("WeldConstraint", { Part0 = torso, Part1 = sash, Parent = sash })
	sash.CFrame = torso.CFrame * CFrame.new(0, -0.55, 0)

	-- kasa (straw hat): three shrinking discs
	local hatSizes = { { 4.4, 0.3 }, { 3.2, 0.35 }, { 1.9, 0.4 } }
	for i, hs in ipairs(hatSizes) do
		local disc = Util.make("Part", {
			Name = "Kasa" .. i, Shape = Enum.PartType.Cylinder,
			Size = Vector3.new(hs[2], hs[1], hs[1]),
			Color = STRAW, Material = Enum.Material.Fabric,
			Anchored = false, CanCollide = false, Massless = true,
			Parent = model,
		})
		Util.make("WeldConstraint", { Part0 = head, Part1 = disc, Parent = disc })
		disc.CFrame = head.CFrame * CFrame.new(0, 0.42 + i * 0.22, 0) * CFrame.Angles(0, 0, math.rad(90))
	end

	local humanoid = Util.make("Humanoid", {
		RigType = Enum.HumanoidRigType.R6,
		Parent = model,
	})
	humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None

	model.PrimaryPart = hrp
	model:PivotTo(cframe)
	model.Parent = Workspace
	return model
end

-- ------------------------------------------------------------- brains

local function newBrain(actorId, spec, home)
	return {
		id = actorId, spec = spec, home = home,
		target = nil, nextThink = 0, nextAttack = 0,
		comboLeft = 0, blockRelease = 0, reactUntil = 0,
	}
end

local function findTarget(brain, actor)
	local best, bestDist = nil, brain.spec.engageRange or 60
	for id, other in pairs(combatService.Actors) do
		if id ~= brain.id and not other.dead and not other.isNPC and other.root then
			local d = (other.root.Position - actor.root.Position).Magnitude
			if d < bestDist then
				best, bestDist = other, d
			end
		end
	end
	return best
end

local function think(brain, actor, t)
	local spec = brain.spec
	if spec.aggro <= 0 then
		return -- training dummy: stands there, takes it, teaches nothing but truth
	end

	local target = brain.target
	if not target or target.dead or not target.root
		or (target.root.Position - actor.root.Position).Magnitude > spec.engageRange * 1.6 then
		brain.target = findTarget(brain, actor)
		target = brain.target
	end

	local humanoid = actor.humanoid
	if not target then
		-- patrol near home
		if math.random() < 0.3 then
			humanoid:MoveTo(brain.home + Vector3.new(math.random(-14, 14), 0, math.random(-14, 14)))
		end
		return
	end

	local myPos = actor.root.Position
	local dist = (target.root.Position - myPos).Magnitude

	-- release a held block
	if actor.blocking and t > brain.blockRelease then
		combatService.DoAction(brain.id, { a = "block", on = false })
	end

	-- low stamina: give ground
	if actor.stamina < 20 then
		local away = Util.flatDirection(myPos - target.root.Position)
		humanoid:MoveTo(myPos + away * 14)
		return
	end

	-- execution opportunity
	if (t < target.gbUntil or target.hp <= Config.Combat.ExecuteHPThreshold)
		and dist < Config.Combat.ExecuteRange - 2 and math.random() < 0.6 then
		combatService.DoAction(brain.id, { a = "execute" })
		return
	end

	if dist > 9 then
		-- close in; occasional dash approach
		humanoid:MoveTo(target.root.Position)
		if dist > 16 and dist < 34 and math.random() < 0.12 then
			combatService.DoAction(brain.id, { a = "dash", dir = Util.flatDirection(target.root.Position - myPos) })
			task.delay(0.25, function()
				if not actor.dead then
					combatService.DoAction(brain.id, { a = "dash_attack" })
				end
			end)
		end
	else
		-- in range: strafe a little, pick fights
		if math.random() < 0.35 then
			local right = actor.root.CFrame.RightVector
			humanoid:MoveTo(myPos + right * (math.random() < 0.5 and 6 or -6))
		end
		if t >= brain.nextAttack and not actor.blocking then
			if target.blockStreak >= 3 and math.random() < 0.7 then
				combatService.DoAction(brain.id, { a = "heavy_start" })
				task.delay(0.45 + math.random() * 0.4, function()
					if not actor.dead then
						combatService.DoAction(brain.id, { a = "heavy_release" })
					end
				end)
				brain.nextAttack = t + 2
			elseif math.random() < spec.aggro then
				brain.comboLeft = math.random(1, spec.comboMax)
				combatService.DoAction(brain.id, { a = "light" })
				brain.comboLeft = brain.comboLeft - 1
				brain.nextAttack = t + 0.55
			else
				brain.nextAttack = t + 0.35
			end
		elseif brain.comboLeft > 0 and actor.attack == nil and t >= actor.busyUntil then
			combatService.DoAction(brain.id, { a = "light" })
			brain.comboLeft = brain.comboLeft - 1
			brain.nextAttack = t + 0.55
		end
	end
end

-- react to enemy windups with a human delay
local function onAttackStarted(attackerId, key, windup)
	local attacker = combatService.getActor(attackerId)
	if not attacker then return end
	for id, brain in pairs(brains) do
		local actor = combatService.getActor(id)
		local spec = brain.spec
		if actor and not actor.dead and spec.aggro > 0 and brain.target == attacker then
			local t = os.clock()
			if t < brain.reactUntil then
				return
			end
			local dist = (attacker.root.Position - actor.root.Position).Magnitude
			local atkCfg = Config.Attacks[key]
			if atkCfg and dist < atkCfg.range + 6 then
				brain.reactUntil = t + spec.reaction * (1 + math.random())
				local roll = math.random()
				local fogged = false
				-- Ghost Fog dulls AI eyes (weather index 5)
				if AIService.WeatherIndex == 5 then
					fogged = math.random() < 0.4
				end
				if not fogged and roll < spec.parryChance then
					-- time the block so it lands inside the parry window
					local delay = math.max(0, windup - Config.Combat.ParryWindow * 0.5
						- spec.reaction * 0.3 * math.random())
					task.delay(delay, function()
						if not actor.dead then
							combatService.DoAction(id, { a = "block", on = true })
							brain.blockRelease = os.clock() + 0.35
						end
					end)
				elseif not fogged and roll < spec.parryChance + spec.blockChance then
					task.delay(spec.reaction * math.random(), function()
						if not actor.dead then
							combatService.DoAction(id, { a = "block", on = true })
							brain.blockRelease = os.clock() + 0.4 + math.random() * 0.4
						end
					end)
				elseif roll > 0.85 then
					combatService.DoAction(id, { a = "dash",
						dir = Util.flatDirection(actor.root.Position - attacker.root.Position) })
				end
			end
		end
	end
end

-- ------------------------------------------------------------- spawning

local function spawnNPC(spec, position, displayName, isDummy)
	local model = AIService.buildRig(displayName, CFrame.new(position))
	local id = combatService.registerActor(model, {
		npcName = displayName,
		hp = spec.hp,
	})
	if not id then
		model:Destroy()
		return
	end
	local brain = newBrain(id, spec, position)
	brain.isDummy = isDummy
	brains[id] = brain
	return id
end

local function respawnLater(brain, oldModel, name, spec)
	task.delay(3.5, function()
		if oldModel then
			oldModel:Destroy()
		end
	end)
	task.delay(brain.isDummy and 2.5 or 7, function()
		spawnNPC(spec, brain.home, name, brain.isDummy)
	end)
end

function AIService.init(remoteTable, combat, mapService)
	combatService = combat
	AIService.WeatherIndex = 1

	combat.AttackStarted:Connect(onAttackStarted)

	combat.ActorDied:Connect(function(victimId)
		local brain = brains[victimId]
		if brain then
			brains[victimId] = nil
			local actor = combat.getActor(victimId)
			local model = actor and actor.model or nil
			local name = actor and actor.npcName or "浪人 Ronin"
			combat.unregister(victimId)
			respawnLater(brain, model, name, brain.spec)
		end
	end)

	-- dojo dummies
	for i, pos in ipairs(mapService.DummySpots or {}) do
		spawnNPC(Config.AI.Dummy, pos, "木人 Training Dummy " .. i, true)
	end
	-- ronin duelists
	for _, spot in ipairs(mapService.RoninSpots or {}) do
		local spec = Config.AI.Ronin[spot.diff] or Config.AI.Ronin[2]
		local nm = Config.AI.Names[math.random(#Config.AI.Names)]
		spawnNPC(spec, spot.pos, nm[1] .. " " .. nm[2], false)
	end

	-- brain tick
	RunService.Heartbeat:Connect(function()
		local t = os.clock()
		for id, brain in pairs(brains) do
			if t >= brain.nextThink then
				brain.nextThink = t + 0.15 + math.random() * 0.1
				local actor = combatService.getActor(id)
				if actor and not actor.dead and not actor.cine
					and t >= actor.gbUntil and t >= actor.staggerUntil then
					think(brain, actor, t)
				end
			end
		end
	end)
end

function AIService.setWeatherIndex(i)
	AIService.WeatherIndex = i
end

return AIService
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX12"><Properties><string name="Name">AntiCheat</string><ProtectedString name="Source"><![CDATA[--[[
	AntiCheat — conservative server-side movement validation.
	All damage, cooldowns, stamina and posture already live on the server
	(CombatService), so the only client-trusted surface is movement.
	Speed/teleport violations rubber-band the player back; repeated hard
	violations are logged. No instant kicks — false positives are worse
	than a laggy cheater.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))

local AntiCheat = {}

local tracked = {} -- player -> { lastPos, strikes, lastCheck }

-- generous ceiling: sprint speed * stance bonus + dash burst + physics slack
local MAX_SPEED = Config.Combat.DashSpeed + Config.Combat.SprintSpeed * 1.3 + 20

function AntiCheat.init()
	Players.PlayerAdded:Connect(function(player)
		player.CharacterAdded:Connect(function(character)
			local root = character:WaitForChild("HumanoidRootPart", 10)
			if root then
				tracked[player] = { lastPos = root.Position, strikes = 0, lastCheck = os.clock() }
			end
		end)
	end)
	Players.PlayerRemoving:Connect(function(player)
		tracked[player] = nil
	end)

	local accum = 0
	RunService.Heartbeat:Connect(function(dt)
		accum = accum + dt
		if accum < 0.5 then return end
		accum = 0
		local t = os.clock()
		for player, rec in pairs(tracked) do
			local char = player.Character
			local root = char and char:FindFirstChild("HumanoidRootPart")
			if root then
				local elapsed = t - rec.lastCheck
				if elapsed > 0.1 then
					local dist = (root.Position - rec.lastPos).Magnitude
					local speed = dist / elapsed
					if speed > MAX_SPEED then
						rec.strikes = rec.strikes + 1
						if rec.strikes >= 3 then
							-- rubber-band
							root.CFrame = CFrame.new(rec.lastPos + Vector3.new(0, 2, 0))
							warn(("[TLS AntiCheat] %s speed violation (%.0f studs/s)"):format(player.Name, speed))
							rec.strikes = 0
						end
					else
						rec.strikes = math.max(0, rec.strikes - 1)
						rec.lastPos = root.Position
					end
					rec.lastCheck = t
				end
			end
		end
	end)
end

return AntiCheat
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX13"><Properties><string name="Name">CombatService</string><ProtectedString name="Source"><![CDATA[--[[
	CombatService — the authoritative heart of THE LAST SAMURAI.

	Every combat decision (damage, posture, parry timing, stamina, cooldowns,
	executions, ults) is made HERE, on the server. Clients only send intents
	through the Action remote and render the Combat event stream.

	Actors are players AND NPCs behind one interface, so PvP and PvE share
	one honest rule set.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))
local KatanaBuilder = require(Shared:WaitForChild("KatanaBuilder"))

local C = Config.Combat

local CombatService = {}
CombatService.Actors = {}          -- id -> actor
CombatService.ActorDied = Util.Signal.new()    -- (victimId, killerId, executed)
CombatService.AttackStarted = Util.Signal.new()-- (attackerId, key, windup)

local remotes
local nextNpcId = -1000
local bloodMoon = false

-- ------------------------------------------------------------------ helpers

local function now()
	return os.clock()
end

local function broadcast(payload)
	remotes.Combat:FireAllClients(payload)
end

local function stanceOf(actor)
	return Config.Stances[actor.stance] or Config.Stances[3]
end

local function actorPos(actor)
	return actor.root and actor.root.Position or Vector3.zero
end

local function actorLook(actor)
	return actor.root and Util.flatDirection(actor.root.CFrame.LookVector) or Vector3.new(0, 0, -1)
end

local function canAct(actor)
	local t = now()
	return not actor.dead and not actor.cine
		and t >= actor.staggerUntil and t >= actor.gbUntil
		and t >= actor.hurtUntil and t >= actor.busyUntil
		and not actor.charging
end

local function setWalkSpeed(actor)
	if not actor.humanoid then return end
	local stance = stanceOf(actor)
	local speed = C.WalkSpeed * stance.speed
	if actor.blocking then
		speed = C.BlockWalkSpeed
	elseif actor.sprinting then
		speed = C.SprintSpeed * stance.speed
	end
	if actor.dead or actor.cine or now() < actor.gbUntil then
		speed = 0
	end
	actor.humanoid.WalkSpeed = speed
end

local function mirrorHealth(actor)
	if actor.humanoid and actor.humanoid.Health > 0 then
		actor.humanoid.Health = math.max(actor.hp, 0.001)
	end
end

-- ------------------------------------------------------------------ registry

function CombatService.registerActor(model, opts)
	opts = opts or {}
	local humanoid = model:FindFirstChildOfClass("Humanoid")
	local root = model:FindFirstChild("HumanoidRootPart")
	if not humanoid or not root then
		return nil
	end

	local id
	if opts.player then
		id = opts.player.UserId
	else
		nextNpcId = nextNpcId - 1
		id = nextNpcId
	end

	local maxHP = opts.hp or C.MaxHP
	humanoid.MaxHealth = maxHP
	humanoid.Health = maxHP
	humanoid.BreakJointsOnDeath = false

	local actor = {
		id = id,
		model = model,
		humanoid = humanoid,
		root = root,
		player = opts.player,
		isNPC = opts.player == nil,
		npcName = opts.npcName,
		maxHP = maxHP,

		hp = maxHP,
		posture = 0,
		stamina = C.MaxStamina,
		ult = 0,
		stance = 3, -- Water

		blocking = false, blockStart = 0, blockStreak = 0,
		attack = nil, busyUntil = 0,
		combo = { lastKey = nil, chainUntil = 0 },
		charging = nil,
		staggerUntil = 0, gbUntil = 0, hurtUntil = 0, iframesUntil = 0,
		cds = { dash = 0 },
		sprinting = false,
		dead = false, cine = false,
		lastPostureGain = 0,
		stats = { parries = 0, maxCombo = 0, comboNow = 0 },
	}
	CombatService.Actors[id] = actor
	setWalkSpeed(actor)

	KatanaBuilder.attach(model)

	humanoid.Died:Connect(function()
		if not actor.dead then
			CombatService.kill(actor, nil, false)
		end
	end)

	remotes.Actors:FireAllClients({
		id = id, model = model,
		npc = actor.isNPC, npcName = opts.npcName,
	})
	return id, actor
end

function CombatService.unregister(id)
	local actor = CombatService.Actors[id]
	if actor then
		CombatService.Actors[id] = nil
		remotes.Actors:FireAllClients({ id = id, gone = true })
	end
end

function CombatService.getActor(id)
	return CombatService.Actors[id]
end

function CombatService.syncActorsTo(player)
	for id, actor in pairs(CombatService.Actors) do
		if actor.model and actor.model.Parent then
			remotes.Actors:FireClient(player, {
				id = id, model = actor.model,
				npc = actor.isNPC, npcName = actor.npcName,
			})
		end
	end
end

-- ------------------------------------------------------------------ damage core

local function postureDamage(victim, amount)
	victim.posture = Util.clamp(victim.posture + amount, 0, C.MaxPosture)
	victim.lastPostureGain = now()
	if victim.posture >= C.MaxPosture and now() >= victim.gbUntil then
		victim.gbUntil = now() + C.GuardBreakStun
		victim.blocking = false
		victim.attack = nil
		victim.charging = nil
		setWalkSpeed(victim)
		broadcast({ e = "guardbreak", id = victim.id, pos = actorPos(victim) })
		task.delay(C.GuardBreakStun, function()
			if not victim.dead and victim.posture >= C.MaxPosture then
				victim.posture = C.PostureAfterBreak
			end
		end)
	end
end

function CombatService.kill(victim, killer, executed)
	if victim.dead then return end
	victim.dead = true
	victim.blocking = false
	victim.attack = nil
	victim.hp = 0
	setWalkSpeed(victim)
	broadcast({
		e = "death", id = victim.id,
		kid = killer and killer.id or nil,
		executed = executed or false,
		pos = actorPos(victim),
	})
	remotes.KillFeed:FireAllClients({
		killer = killer and (killer.player and killer.player.DisplayName or killer.npcName or "?") or nil,
		victim = victim.player and victim.player.DisplayName or victim.npcName or "?",
		executed = executed or false,
	})
	CombatService.ActorDied:Fire(victim.id, killer and killer.id or nil, executed or false)
	if victim.humanoid then
		victim.humanoid.Health = 0
	end
	if victim.player then
		CombatService.unregister(victim.id)
	end
end

local function applyHit(attacker, victim, atk, mult)
	local t = now()
	local aStance = stanceOf(attacker)
	local vStance = stanceOf(victim)
	local pos = actorPos(victim) + Vector3.new(0, 1.5, 0)

	if t < victim.iframesUntil then
		broadcast({ e = "evade", id = victim.id, pos = pos })
		return
	end

	local toVictim = Util.flatDirection(actorPos(victim) - actorPos(attacker))
	local victimFacingAttacker = actorLook(victim):Dot(-toVictim) > 0.1
	local backstab = actorLook(victim):Dot(toVictim) > 0.35

	-- BLOCK / PARRY -----------------------------------------------------
	if victim.blocking and victimFacingAttacker then
		local window = C.ParryWindow * vStance.parryWindow
		if t - victim.blockStart <= window then
			-- PERFECT PARRY
			victim.posture = math.max(0, victim.posture - C.ParryPostureRefund)
			victim.ult = math.min(C.MaxUlt, victim.ult + C.ParryUltGain)
			victim.stats.parries = victim.stats.parries + 1
			attacker.attack = nil
			attacker.charging = nil
			attacker.staggerUntil = t + C.StaggerDuration + 0.25
			attacker.busyUntil = attacker.staggerUntil
			postureDamage(attacker, C.ParryPostureToAttacker)
			broadcast({ e = "parry", id = victim.id, aid = attacker.id, pos = pos })
			if victim.player then
				local DataService = CombatService.DataService
				if DataService then
					DataService.award(victim.player, { coins = Config.Rewards.ParryCoins })
				end
			end
			return
		end
		-- normal block
		local pDmg = atk.posture * aStance.posture * vStance.blockCost
		victim.stamina = math.max(0, victim.stamina - C.BlockStaminaCost)
		victim.hp = math.max(1, victim.hp - atk.damage * C.BlockChip)
		victim.blockStreak = victim.blockStreak + 1
		mirrorHealth(victim)
		if atk.guardBreak then
			postureDamage(victim, C.MaxPosture)
		else
			postureDamage(victim, pDmg)
		end
		broadcast({
			e = "hit", id = attacker.id, tid = victim.id,
			blocked = true, dmg = 0, pos = pos,
			guardBreak = atk.guardBreak or false,
		})
		return
	end

	-- CLEAN HIT ----------------------------------------------------------
	local dmg = atk.damage * aStance.damage * (mult or 1)
	if backstab then
		dmg = dmg * C.BackstabMult
	end
	if bloodMoon then
		dmg = dmg * 1.1
	end
	victim.hp = victim.hp - dmg
	victim.blockStreak = 0
	victim.hurtUntil = t + C.HurtStun
	postureDamage(victim, atk.posture * 0.45)
	victim.ult = math.min(C.MaxUlt, victim.ult + C.HitUltGainVictim)
	attacker.ult = math.min(C.MaxUlt, attacker.ult + C.HitUltGainAttacker)
	attacker.stats.comboNow = attacker.stats.comboNow + 1
	attacker.stats.maxCombo = math.max(attacker.stats.maxCombo, attacker.stats.comboNow)
	mirrorHealth(victim)

	-- physics knockback only for server-owned rigs (NPCs); players get none
	if victim.isNPC and victim.root then
		victim.root.AssemblyLinearVelocity = toVictim * (atk.knock * 2) + Vector3.new(0, 8, 0)
	end

	broadcast({
		e = "hit", id = attacker.id, tid = victim.id,
		dmg = math.floor(dmg + 0.5), pos = pos,
		backstab = backstab, heavy = atk.guardBreak or atk.finisher or false,
	})

	if victim.hp <= 0 then
		CombatService.kill(victim, attacker, false)
	end
end

local function resolveAttack(attacker, key, atk, mult)
	if attacker.dead or attacker.cine then return end
	local t = now()
	if t < attacker.staggerUntil or t < attacker.gbUntil then return end

	local aPos = actorPos(attacker)
	local look = actorLook(attacker)
	local cosLimit = math.cos(math.rad((atk.angle or 75) * 0.5 + 20))

	for _, victim in pairs(CombatService.Actors) do
		if victim ~= attacker and not victim.dead and not victim.cine and victim.root then
			local offset = actorPos(victim) - aPos
			local dist = Vector3.new(offset.X, 0, offset.Z).Magnitude
			if dist <= atk.range + 3 and math.abs(offset.Y) < 8 then
				local dir = Util.flatDirection(offset)
				if atk.angle >= 360 or look:Dot(dir) >= cosLimit then
					applyHit(attacker, victim, atk, mult)
				end
			end
		end
	end
end

local function beginAttack(actor, key, mult)
	local atk = Config.Attacks[key]
	if not atk then return end
	if actor.stamina < atk.stamina then return end
	actor.stamina = actor.stamina - atk.stamina
	if actor.blocking then
		actor.blocking = false
		broadcast({ e = "block", id = actor.id, on = false })
	end
	setWalkSpeed(actor)

	local token = {}
	actor.attack = { key = key, token = token, startedAt = now() }
	actor.busyUntil = now() + atk.windup + atk.recover
	actor.combo.lastKey = key
	actor.combo.chainUntil = actor.busyUntil + C.ComboChainWindow

	broadcast({ e = "attack", id = actor.id, key = key })
	CombatService.AttackStarted:Fire(actor.id, key, atk.windup)

	task.delay(atk.windup, function()
		if actor.attack and actor.attack.token == token then
			resolveAttack(actor, key, atk, mult)
			actor.attack = nil
		end
	end)
end

-- ------------------------------------------------------------------ actions

function CombatService.DoAction(id, action)
	local actor = CombatService.Actors[id]
	if not actor or actor.dead or actor.cine then return end
	local a = action.a
	local t = now()

	if a == "light" then
		-- allow the press slightly early: treat as buffered if recovery is almost done
		if not canAct(actor) and actor.attack == nil and not actor.charging
			and t >= actor.busyUntil - 0.1 and t >= actor.staggerUntil
			and t >= actor.gbUntil and t >= actor.hurtUntil then
			task.delay(math.max(0, actor.busyUntil - t), function()
				CombatService.DoAction(actor.id, { a = "light" })
			end)
			return
		end
		if canAct(actor) then
			local chainCfg = nil
			if actor.combo.lastKey and t <= actor.combo.chainUntil then
				chainCfg = Config.Attacks[actor.combo.lastKey]
			end
			if chainCfg and chainCfg.nextKey then
				beginAttack(actor, chainCfg.nextKey, 1)
			else
				beginAttack(actor, "L1", 1)
			end
		end

	elseif a == "heavy_start" then
		if canAct(actor) and actor.stamina >= Config.Attacks.Heavy.stamina then
			actor.charging = t
			broadcast({ e = "charge", id = actor.id })
		end

	elseif a == "heavy_release" then
		if actor.charging then
			local held = Util.clamp(t - actor.charging, 0, C.HeavyChargeMax)
			actor.charging = nil
			local mult = 1 + C.HeavyChargeBonus * (held / C.HeavyChargeMax)
			beginAttack(actor, "Heavy", mult)
		end

	elseif a == "dash_attack" then
		if canAct(actor) then
			beginAttack(actor, "Dash", 1)
		end

	elseif a == "block" then
		if action.on then
			if canAct(actor) then
				actor.blocking = true
				actor.blockStart = t
				setWalkSpeed(actor)
				broadcast({ e = "block", id = actor.id, on = true })
			end
		else
			if actor.blocking then
				actor.blocking = false
				setWalkSpeed(actor)
				broadcast({ e = "block", id = actor.id, on = false })
			end
		end

	elseif a == "dash" then
		if t >= actor.cds.dash and actor.stamina >= C.DashCost
			and not actor.blocking and t >= actor.gbUntil and t >= actor.staggerUntil then
			actor.cds.dash = t + C.DashCooldown
			actor.stamina = actor.stamina - C.DashCost
			actor.iframesUntil = t + C.DashIFrames
			broadcast({ e = "dash", id = actor.id })
			-- NPCs get their dash motion from the server
			if actor.isNPC and actor.root then
				local dir = action.dir or actorLook(actor)
				if typeof(dir) == "Vector3" and dir.Magnitude > 0 then
					dir = Util.flatDirection(dir)
					actor.root.AssemblyLinearVelocity = dir * C.DashSpeed
				end
			end
		end

	elseif a == "sprint" then
		actor.sprinting = action.on == true and actor.stamina > C.SprintMinStamina
		setWalkSpeed(actor)

	elseif a == "stance" then
		local i = tonumber(action.i)
		if i and i >= 1 and i <= #Config.Stances and canAct(actor) then
			actor.stance = i
			setWalkSpeed(actor)
			broadcast({ e = "stance", id = actor.id, i = i })
		end

	elseif a == "execute" then
		CombatService.tryExecute(actor)

	elseif a == "ult" then
		CombatService.tryUlt(actor)
	end
end

-- ------------------------------------------------------------------ execution

function CombatService.tryExecute(attacker)
	if not canAct(attacker) then return end
	local t = now()
	local aPos = actorPos(attacker)
	local best, bestDist = nil, C.ExecuteRange
	for _, victim in pairs(CombatService.Actors) do
		if victim ~= attacker and not victim.dead and not victim.cine then
			local vulnerable = t < victim.gbUntil or victim.hp <= C.ExecuteHPThreshold
			if vulnerable then
				local d = (actorPos(victim) - aPos).Magnitude
				if d < bestDist then
					best, bestDist = victim, d
				end
			end
		end
	end
	if not best then return end

	attacker.cine = true
	best.cine = true
	setWalkSpeed(attacker)
	setWalkSpeed(best)

	-- place the attacker behind the victim, both anchored for the cinematic
	local vcf = best.root.CFrame
	local behind = vcf.Position + vcf.LookVector * -3
	attacker.root.CFrame = CFrame.lookAt(behind, Vector3.new(vcf.Position.X, behind.Y, vcf.Position.Z))
	attacker.root.Anchored = true
	best.root.Anchored = true

	broadcast({ e = "execute", id = attacker.id, tid = best.id, pos = actorPos(best) })

	task.delay(C.ExecuteDuration, function()
		attacker.root.Anchored = false
		if best.root then
			best.root.Anchored = false
		end
		attacker.cine = false
		best.cine = false
		if not best.dead then
			CombatService.kill(best, attacker, true)
		end
		if attacker.player and CombatService.DataService then
			CombatService.DataService.award(attacker.player, { coins = Config.Rewards.ExecuteBonusCoins })
		end
	end)
end

function CombatService.tryUlt(actor)
	if not canAct(actor) or actor.ult < Config.Ult.Cost then return end
	actor.ult = 0
	actor.busyUntil = now() + Config.Ult.Hits * Config.Ult.Interval + 0.3
	broadcast({ e = "ult", id = actor.id, pos = actorPos(actor) })
	for i = 1, Config.Ult.Hits do
		task.delay(i * Config.Ult.Interval, function()
			if actor.dead then return end
			resolveAttack(actor, "UltHit", Config.Attacks.UltHit, 1)
		end)
	end
end

function CombatService.setBloodMoon(on)
	bloodMoon = on
end

-- ------------------------------------------------------------------ character lifecycle

function CombatService.setupCharacter(player, character)
	local humanoid = character:WaitForChild("Humanoid", 10)
	if not humanoid then return end
	character:WaitForChild("HumanoidRootPart", 10)

	-- kill the default health regen script
	local regen = character:FindFirstChild("Health")
	if regen then
		regen:Destroy()
	end

	CombatService.registerActor(character, { player = player })

	character.AncestryChanged:Connect(function(_, parent)
		if parent == nil then
			CombatService.unregister(player.UserId)
		end
	end)
end

-- ------------------------------------------------------------------ heartbeat

local syncAccum = 0
local function heartbeat(dt)
	local t = now()
	syncAccum = syncAccum + dt
	local doSync = syncAccum >= 0.1
	if doSync then
		syncAccum = 0
	end

	for _, actor in pairs(CombatService.Actors) do
		if not actor.dead then
			-- stamina
			local regen = actor.blocking and C.StaminaRegenBlocking or C.StaminaRegen
			if actor.sprinting then
				actor.stamina = actor.stamina - C.SprintDrain * dt
				if actor.stamina <= C.SprintMinStamina then
					actor.sprinting = false
					setWalkSpeed(actor)
				end
			elseif not actor.charging and actor.attack == nil then
				actor.stamina = Util.clamp(actor.stamina + regen * dt, 0, C.MaxStamina)
			end
			-- posture decay
			if t - actor.lastPostureGain > C.PostureDecayDelay and t >= actor.gbUntil then
				actor.posture = math.max(0, actor.posture - C.PostureDecayRate * dt)
			end
			-- auto-release an over-held heavy
			if actor.charging and t - actor.charging > C.HeavyChargeMax + 0.1 then
				CombatService.DoAction(actor.id, { a = "heavy_release" })
			end
			-- combo counter reset
			if actor.stats.comboNow > 0 and actor.attack == nil and t > actor.combo.chainUntil then
				actor.stats.comboNow = 0
			end
			-- state sync to owning player
			if doSync and actor.player then
				remotes.State:FireClient(actor.player, {
					hp = actor.hp, po = actor.posture,
					st = actor.stamina, ult = actor.ult,
					stance = actor.stance,
				})
			end
		end
	end
end

-- ------------------------------------------------------------------ init

function CombatService.init(remoteTable, dataService)
	remotes = remoteTable
	CombatService.DataService = dataService

	remotes.Action.OnServerEvent:Connect(function(player, action)
		if typeof(action) ~= "table" or typeof(action.a) ~= "string" then
			return
		end
		CombatService.DoAction(player.UserId, action)
	end)

	RunService.Heartbeat:Connect(heartbeat)

	Players.PlayerAdded:Connect(function(player)
		player.CharacterAdded:Connect(function(character)
			CombatService.setupCharacter(player, character)
		end)
		task.defer(function()
			CombatService.syncActorsTo(player)
		end)
		if player.Character then
			CombatService.setupCharacter(player, player.Character)
		end
	end)
	Players.PlayerRemoving:Connect(function(player)
		CombatService.unregister(player.UserId)
	end)
end

return CombatService
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX14"><Properties><string name="Name">DataService</string><ProtectedString name="Source"><![CDATA[--[[
	DataService — coins, XP, level, kills, wins, parries.
	UpdateAsync merges, autosave every 2 minutes, BindToClose flush.
	Fails soft in Studio when API access is off.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))

local DataService = {}
local sessions = {} -- userId -> data
local store

local DEFAULT = { coins = 0, xp = 0, level = 1, kills = 0, wins = 0, parries = 0 }

local function deepCopy(t)
	local out = {}
	for k, v in pairs(t) do
		out[k] = v
	end
	return out
end

local function makeLeaderstats(player, data)
	local ls = Instance.new("Folder")
	ls.Name = "leaderstats"
	for _, def in ipairs({ { "Kills", data.kills }, { "Coins", data.coins }, { "Level", data.level } }) do
		local v = Instance.new("IntValue")
		v.Name = def[1]
		v.Value = def[2]
		v.Parent = ls
	end
	ls.Parent = player
end

local function syncLeaderstats(player, data)
	local ls = player:FindFirstChild("leaderstats")
	if not ls then return end
	ls.Kills.Value = data.kills
	ls.Coins.Value = data.coins
	ls.Level.Value = data.level
end

local function load(player)
	local data = deepCopy(DEFAULT)
	if store then
		local ok, saved = Util.retry(3, 1, function()
			return store:GetAsync("u_" .. player.UserId)
		end)
		if ok and type(saved) == "table" then
			for k, v in pairs(saved) do
				data[k] = v
			end
		end
	end
	sessions[player.UserId] = data
	makeLeaderstats(player, data)
end

local function save(player)
	local data = sessions[player.UserId]
	if not data or not store then return end
	Util.retry(3, 2, function()
		store:UpdateAsync("u_" .. player.UserId, function()
			return data
		end)
		return true
	end)
end

function DataService.get(player)
	return sessions[player.UserId]
end

function DataService.award(player, gains)
	local data = sessions[player.UserId]
	if not data then return end
	data.coins = data.coins + (gains.coins or 0)
	data.xp = data.xp + (gains.xp or 0)
	data.kills = data.kills + (gains.kills or 0)
	data.wins = data.wins + (gains.wins or 0)
	data.parries = data.parries + (gains.parries or 0)
	-- level-up check
	local needed = data.level * Config.Rewards.XPPerLevel
	while data.xp >= needed do
		data.xp = data.xp - needed
		data.level = data.level + 1
		needed = data.level * Config.Rewards.XPPerLevel
		if DataService.onLevelUp then
			DataService.onLevelUp(player, data.level)
		end
	end
	syncLeaderstats(player, data)
end

function DataService.init()
	local ok, s = pcall(function()
		return DataStoreService:GetDataStore("TLS_v1")
	end)
	if ok then
		store = s
	else
		warn("[TLS] DataStore unavailable (Studio without API access?) — progress will not persist")
	end

	Players.PlayerAdded:Connect(load)
	for _, p in ipairs(Players:GetPlayers()) do
		task.spawn(load, p)
	end
	Players.PlayerRemoving:Connect(function(player)
		save(player)
		sessions[player.UserId] = nil
	end)

	-- autosave
	task.spawn(function()
		while true do
			task.wait(120)
			for _, p in ipairs(Players:GetPlayers()) do
				task.spawn(save, p)
			end
		end
	end)

	if not RunService:IsStudio() then
		game:BindToClose(function()
			for _, p in ipairs(Players:GetPlayers()) do
				save(p)
			end
			task.wait(2)
		end)
	end
end

return DataService
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX15"><Properties><string name="Name">MapService</string><ProtectedString name="Source"><![CDATA[--[[
	MapService — builds the whole world from parts and terrain at server start.
	Flat grass plain, stone duel arena, torii gates, three-tier pagoda, dojo
	with training yard, cherry trees that shed petals, bamboo groves, stone
	lanterns, a koi pond, and a ring of ink-wash mountains on the horizon.
]]

local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Util = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Util"))

local MapService = {}

local COL = {
	Vermillion = Color3.fromRGB(146, 24, 28),
	DarkBeam = Color3.fromRGB(38, 24, 20),
	Roof = Color3.fromRGB(34, 36, 44),
	RoofTrim = Color3.fromRGB(217, 164, 91),
	Stone = Color3.fromRGB(92, 92, 96),
	StoneDark = Color3.fromRGB(64, 64, 70),
	Wood = Color3.fromRGB(86, 60, 42),
	WoodDark = Color3.fromRGB(52, 36, 26),
	Paper = Color3.fromRGB(226, 218, 196),
	Bamboo = Color3.fromRGB(106, 140, 78),
	Blossom = Color3.fromRGB(232, 178, 194),
	BlossomDeep = Color3.fromRGB(214, 148, 170),
	Trunk = Color3.fromRGB(58, 44, 40),
	Mountain = Color3.fromRGB(36, 30, 40),
}

local map

local function part(props)
	props.Anchored = true
	props.Parent = props.Parent or map
	if props.CanCollide == nil then
		props.CanCollide = true
	end
	props.TopSurface = Enum.SurfaceType.Smooth
	props.BottomSurface = Enum.SurfaceType.Smooth
	return Util.make("Part", props)
end

local function wedge(props)
	props.Anchored = true
	props.Parent = props.Parent or map
	if props.CanCollide == nil then
		props.CanCollide = true
	end
	return Util.make("WedgePart", props)
end

-- four wedge skirts + flat cap = one pagoda/dojo roof tier
local function roof(cx, y, cz, w, d, h, overhang, color)
	local ow, od = w + overhang * 2, d + overhang * 2
	wedge({ Size = Vector3.new(ow, h, od / 2), Color = color, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, y + h / 2, cz - od / 4) * CFrame.Angles(0, math.rad(180), 0) })
	wedge({ Size = Vector3.new(ow, h, od / 2), Color = color, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, y + h / 2, cz + od / 4) })
	wedge({ Size = Vector3.new(od, h, ow / 2), Color = color, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx - ow / 4, y + h / 2, cz) * CFrame.Angles(0, math.rad(90), 0) })
	wedge({ Size = Vector3.new(od, h, ow / 2), Color = color, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx + ow / 4, y + h / 2, cz) * CFrame.Angles(0, math.rad(-90), 0) })
	part({ Size = Vector3.new(w * 0.4, h * 0.5, d * 0.4), Color = COL.RoofTrim,
		Material = Enum.Material.Metal, CFrame = CFrame.new(cx, y + h + h * 0.25, cz) })
end

local function torii(cx, cz, rotY, scale)
	scale = scale or 1
	local cf = CFrame.new(cx, 0, cz) * CFrame.Angles(0, math.rad(rotY), 0)
	local h, spread = 22 * scale, 9 * scale
	for side = -1, 1, 2 do
		part({ Size = Vector3.new(2.2 * scale, h, 2.2 * scale), Color = COL.Vermillion,
			Material = Enum.Material.SmoothPlastic,
			CFrame = cf * CFrame.new(side * spread, h / 2, 0) * CFrame.Angles(0, 0, math.rad(-side * 2)) })
		part({ Size = Vector3.new(3.4 * scale, 1 * scale, 3.4 * scale), Color = COL.StoneDark,
			Material = Enum.Material.Slate, CFrame = cf * CFrame.new(side * spread, 0.5, 0) })
	end
	-- nuki (lower beam)
	part({ Size = Vector3.new(spread * 2 + 5 * scale, 1.6 * scale, 1.6 * scale), Color = COL.Vermillion,
		Material = Enum.Material.SmoothPlastic, CFrame = cf * CFrame.new(0, h - 5 * scale, 0) })
	-- kasagi (top beam, curved illusion via 3 segments)
	local topY = h + 0.6 * scale
	part({ Size = Vector3.new(spread * 2 + 9 * scale, 1.8 * scale, 2.6 * scale), Color = COL.DarkBeam,
		Material = Enum.Material.Wood, CFrame = cf * CFrame.new(0, topY, 0) })
	for side = -1, 1, 2 do
		part({ Size = Vector3.new(4.4 * scale, 1.7 * scale, 2.6 * scale), Color = COL.DarkBeam,
			Material = Enum.Material.Wood,
			CFrame = cf * CFrame.new(side * (spread + 4.6 * scale), topY + 0.7 * scale, 0)
				* CFrame.Angles(0, 0, math.rad(side * 9)) })
	end
	part({ Size = Vector3.new(2 * scale, 3.2 * scale, 1.2 * scale), Color = COL.Paper,
		Material = Enum.Material.SmoothPlastic, CFrame = cf * CFrame.new(0, h - 3 * scale, 0) })
end

local function stoneLantern(cx, cz)
	local base = part({ Size = Vector3.new(2, 1, 2), Color = COL.Stone, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, 0.5, cz) })
	part({ Size = Vector3.new(0.9, 2.4, 0.9), Color = COL.Stone, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, 2.2, cz) })
	local house = part({ Size = Vector3.new(1.8, 1.4, 1.8), Color = COL.StoneDark, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, 4.1, cz) })
	part({ Size = Vector3.new(2.4, 0.7, 2.4), Color = COL.Stone, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, 5.15, cz) })
	local core = part({ Size = Vector3.new(1, 1, 1), Color = Color3.fromRGB(255, 190, 110),
		Material = Enum.Material.Neon, Transparency = 0.25, CanCollide = false,
		CFrame = CFrame.new(cx, 4.1, cz) })
	Util.make("PointLight", {
		Color = Color3.fromRGB(255, 178, 96), Range = 16, Brightness = 1.1,
		Shadows = true, Parent = core,
	})
	return base, house
end

local function cherryTree(cx, cz, scale)
	scale = scale or 1
	local lean = math.rad(math.random(-8, 8))
	part({ Size = Vector3.new(1.6 * scale, 9 * scale, 1.6 * scale), Color = COL.Trunk,
		Material = Enum.Material.Wood,
		CFrame = CFrame.new(cx, 4.5 * scale, cz) * CFrame.Angles(lean, 0, lean) })
	part({ Size = Vector3.new(1.1 * scale, 6 * scale, 1.1 * scale), Color = COL.Trunk,
		Material = Enum.Material.Wood,
		CFrame = CFrame.new(cx, 9 * scale, cz) * CFrame.Angles(lean * 2.4, 0, lean * 1.6) })
	local canopyY = 12 * scale
	for i = 1, 4 do
		local off = CFrame.new(
			math.random(-40, 40) / 10 * scale,
			canopyY + math.random(-15, 20) / 10 * scale,
			math.random(-40, 40) / 10 * scale)
		local ball = part({
			Shape = Enum.PartType.Ball,
			Size = Vector3.new(1, 1, 1) * (5.5 + math.random() * 3) * scale,
			Color = i % 2 == 0 and COL.Blossom or COL.BlossomDeep,
			Material = Enum.Material.Grass, CanCollide = false,
			CFrame = CFrame.new(cx, 0, cz) * off,
		})
		if i == 1 then
			-- petals drift from the canopy — replicated ambient emitter
			Util.make("ParticleEmitter", {
				Rate = 6,
				Lifetime = NumberRange.new(4, 7),
				Speed = NumberRange.new(1, 3),
				SpreadAngle = Vector2.new(180, 180),
				Acceleration = Vector3.new(1.2, -2.4, 0.6),
				Rotation = NumberRange.new(0, 360),
				RotSpeed = NumberRange.new(-90, 90),
				Size = NumberSequence.new(0.22),
				Transparency = NumberSequence.new({
					NumberSequenceKeypoint.new(0, 0.15),
					NumberSequenceKeypoint.new(0.85, 0.3),
					NumberSequenceKeypoint.new(1, 1),
				}),
				Color = ColorSequence.new(COL.Blossom, COL.BlossomDeep),
				Parent = ball,
			})
		end
	end
end

local function bambooGrove(cx, cz, count, radius)
	for _ = 1, count do
		local a = math.random() * math.pi * 2
		local r = math.random() * radius
		local x, z = cx + math.cos(a) * r, cz + math.sin(a) * r
		local h = 18 + math.random() * 14
		part({
			Shape = Enum.PartType.Cylinder,
			Size = Vector3.new(h, 0.9, 0.9),
			Color = COL.Bamboo, Material = Enum.Material.SmoothPlastic,
			CFrame = CFrame.new(x, h / 2, z) * CFrame.Angles(0, 0, math.rad(90))
				* CFrame.Angles(math.rad(math.random(-4, 4)), 0, math.rad(math.random(-4, 4))),
			CanCollide = true,
		})
		part({
			Shape = Enum.PartType.Ball, Size = Vector3.new(3.4, 2.2, 3.4),
			Color = Color3.fromRGB(88, 120, 64), Material = Enum.Material.Grass,
			CanCollide = false, CFrame = CFrame.new(x, h + 0.5, z),
		})
	end
end

local function pagoda(cx, cz)
	local widths = { 26, 20, 14 }
	local y = 0
	part({ Size = Vector3.new(32, 2, 32), Color = COL.Stone, Material = Enum.Material.Slate,
		CFrame = CFrame.new(cx, 1, cz) })
	y = 2
	for tier = 1, 3 do
		local w = widths[tier]
		local wallH = 9 - tier
		part({ Size = Vector3.new(w, wallH, w), Color = COL.Paper, Material = Enum.Material.SmoothPlastic,
			CFrame = CFrame.new(cx, y + wallH / 2, cz) })
		for sx = -1, 1, 2 do
			for sz = -1, 1, 2 do
				part({ Size = Vector3.new(1.4, wallH, 1.4), Color = COL.Vermillion,
					Material = Enum.Material.SmoothPlastic,
					CFrame = CFrame.new(cx + sx * (w / 2 - 0.7), y + wallH / 2, cz + sz * (w / 2 - 0.7)) })
			end
		end
		y = y + wallH
		roof(cx, y, cz, w, w, 3.2, 4, COL.Roof)
		y = y + 3.2 + 1.6
	end
	part({ Shape = Enum.PartType.Cylinder, Size = Vector3.new(7, 0.8, 0.8), Color = COL.RoofTrim,
		Material = Enum.Material.Metal, CFrame = CFrame.new(cx, y + 3, cz) * CFrame.Angles(0, 0, math.rad(90)) })
	-- ground floor doorway
	part({ Size = Vector3.new(6, 7, 1), Color = COL.WoodDark, Material = Enum.Material.Wood,
		CFrame = CFrame.new(cx, 5.5, cz - widths[1] / 2) })
end

local function dojo(cx, cz)
	local w, d, wallH = 56, 36, 11
	-- raised floor + engawa porch
	part({ Size = Vector3.new(w + 8, 2, d + 8), Color = COL.WoodDark, Material = Enum.Material.WoodPlanks,
		CFrame = CFrame.new(cx, 1, cz) })
	part({ Size = Vector3.new(10, 1, 6), Color = COL.Wood, Material = Enum.Material.WoodPlanks,
		CFrame = CFrame.new(cx, 0.5, cz + d / 2 + 6) })
	-- walls (front wall has a wide door gap)
	local function wall(sizeV, cf)
		part({ Size = sizeV, Color = COL.Paper, Material = Enum.Material.SmoothPlastic, CFrame = cf })
	end
	wall(Vector3.new(w, wallH, 1.4), CFrame.new(cx, 2 + wallH / 2, cz - d / 2))              -- back
	wall(Vector3.new(1.4, wallH, d), CFrame.new(cx - w / 2, 2 + wallH / 2, cz))              -- left
	wall(Vector3.new(1.4, wallH, d), CFrame.new(cx + w / 2, 2 + wallH / 2, cz))              -- right
	wall(Vector3.new(w / 2 - 7, wallH, 1.4), CFrame.new(cx - (w / 4 + 3.5), 2 + wallH / 2, cz + d / 2))
	wall(Vector3.new(w / 2 - 7, wallH, 1.4), CFrame.new(cx + (w / 4 + 3.5), 2 + wallH / 2, cz + d / 2))
	wall(Vector3.new(14, 3, 1.4), CFrame.new(cx, 2 + wallH - 1.5, cz + d / 2))               -- lintel
	-- timber frame lines
	for i = -2, 2 do
		part({ Size = Vector3.new(1, wallH, 1.6), Color = COL.DarkBeam, Material = Enum.Material.Wood,
			CFrame = CFrame.new(cx + i * (w / 5), 2 + wallH / 2, cz - d / 2) })
	end
	for sx = -1, 1, 2 do
		for sz = -1, 1, 2 do
			part({ Size = Vector3.new(1.8, wallH, 1.8), Color = COL.DarkBeam, Material = Enum.Material.Wood,
				CFrame = CFrame.new(cx + sx * w / 2, 2 + wallH / 2, cz + sz * d / 2) })
		end
	end
	roof(cx, 2 + wallH, cz, w, d, 5, 5, COL.Roof)
	-- name board
	part({ Size = Vector3.new(10, 2.4, 0.6), Color = COL.DarkBeam, Material = Enum.Material.Wood,
		CFrame = CFrame.new(cx, 2 + wallH - 0.4, cz + d / 2 + 1.1) })
end

local function arena()
	-- stone circle
	part({ Shape = Enum.PartType.Cylinder, Size = Vector3.new(1.2, 96, 96), Color = COL.Stone,
		Material = Enum.Material.Slate, CFrame = CFrame.new(0, 0.6, 0) * CFrame.Angles(0, 0, math.rad(90)) })
	part({ Shape = Enum.PartType.Cylinder, Size = Vector3.new(0.4, 76, 76), Color = COL.StoneDark,
		Material = Enum.Material.Slate, CFrame = CFrame.new(0, 1.35, 0) * CFrame.Angles(0, 0, math.rad(90)) })
	part({ Shape = Enum.PartType.Cylinder, Size = Vector3.new(0.4, 12, 12), Color = COL.Vermillion,
		Material = Enum.Material.Slate, CFrame = CFrame.new(0, 1.45, 0) * CFrame.Angles(0, 0, math.rad(90)) })
	-- lantern ring
	for i = 0, 7 do
		local a = i / 8 * math.pi * 2
		stoneLantern(math.cos(a) * 52, math.sin(a) * 52)
	end
end

local function mountains()
	for i = 0, 11 do
		local a = i / 12 * math.pi * 2
		local dist = 480 + math.random(-40, 60)
		local h = 180 + math.random(0, 140)
		local w = 260 + math.random(0, 160)
		wedge({
			Size = Vector3.new(w, h, w / 2),
			Color = COL.Mountain, Material = Enum.Material.Slate, CastShadow = false,
			CFrame = CFrame.new(math.cos(a) * dist, h / 2 - 6, math.sin(a) * dist)
				* CFrame.Angles(0, a + math.pi / 2 + math.rad(math.random(-20, 20)), 0),
		})
	end
end

local function spawns()
	for i = 0, 3 do
		local a = i / 4 * math.pi * 2 + math.pi / 4
		Util.make("SpawnLocation", {
			Size = Vector3.new(6, 1, 6), Transparency = 1, CanCollide = false, Anchored = true,
			Neutral = true, Duration = 0,
			Position = Vector3.new(math.cos(a) * 30, 1.8, math.sin(a) * 30),
			Parent = map,
		})
	end
end

local function barriers()
	local R = 300
	for i = 0, 3 do
		local a = i / 4 * math.pi * 2
		part({
			Size = Vector3.new(R * 2.2, 120, 4), Transparency = 1, CanCollide = true,
			CFrame = CFrame.new(math.cos(a) * R, 60, math.sin(a) * R)
				* CFrame.Angles(0, a + math.pi / 2, 0),
		})
	end
end

function MapService.build()
	map = Instance.new("Folder")
	map.Name = "TLSMap"
	map.Parent = Workspace

	-- terrain ground + pond
	local terrain = Workspace.Terrain
	terrain:FillBlock(CFrame.new(0, -9, 0), Vector3.new(1200, 18, 1200), Enum.Material.Grass)
	terrain:FillBlock(CFrame.new(130, -3.5, -120), Vector3.new(70, 10, 50), Enum.Material.Air)
	terrain:FillBlock(CFrame.new(130, -5, -120), Vector3.new(66, 6, 46), Enum.Material.Water)
	pcall(function()
		terrain.Decoration = true
	end)

	arena()
	torii(0, 78, 0, 1)
	torii(0, -78, 180, 1)
	dojo(-130, 20)
	pagoda(150, 90)
	cherryTree(60, -70, 1.3)
	cherryTree(90, -40, 1)
	cherryTree(-70, -90, 1.15)
	cherryTree(-40, 110, 0.9)
	cherryTree(115, -105, 1.2)
	bambooGrove(-150, -120, 26, 34)
	bambooGrove(200, -30, 20, 26)
	bambooGrove(-60, 160, 18, 24)
	-- lantern-lined path from arena to the dojo
	for i = 1, 4 do
		stoneLantern(-58 - i * 16, 6)
		part({ Size = Vector3.new(14, 0.6, 8), Color = COL.StoneDark, Material = Enum.Material.Slate,
			CFrame = CFrame.new(-58 - i * 16, 0.3, 14) })
	end
	-- scattered rocks
	for _ = 1, 14 do
		local a = math.random() * math.pi * 2
		local r = 90 + math.random() * 160
		part({
			Size = Vector3.new(2 + math.random() * 5, 1.5 + math.random() * 3, 2 + math.random() * 5),
			Color = COL.StoneDark, Material = Enum.Material.Slate,
			CFrame = CFrame.new(math.cos(a) * r, 1, math.sin(a) * r)
				* CFrame.Angles(math.random(), math.random() * 6, math.random()),
		})
	end
	mountains()
	spawns()
	barriers()

	-- dummy pads in the dojo yard (AIService parks the dummies here)
	MapService.DummySpots = {
		Vector3.new(-130, 3, 48), Vector3.new(-118, 3, 52), Vector3.new(-142, 3, 52),
	}
	MapService.RoninSpots = {
		{ pos = Vector3.new(0, 4, 55), diff = 2 },
		{ pos = Vector3.new(30, 4, -50), diff = 2 },
		{ pos = Vector3.new(150, 4, 70), diff = 3 },
	}
end

return MapService
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX16"><Properties><string name="Name">WeatherService</string><ProtectedString name="Source"><![CDATA[--[[
	WeatherService — five hand-tuned presets that reshape the entire mood:
	Blood Sunset, Full Moon, Storm, First Snow, Ghost Fog.
	Cycles automatically; every ~8 minutes a BLOOD MOON rises for 90 seconds
	(deeper night, +10% damage everywhere, doubled kill rewards).
]]

local Lighting = game:GetService("Lighting")
local TweenService = game:GetService("TweenService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))

local WeatherService = {}
WeatherService.Index = 1

local remotes, combatService
local atmosphere, bloom, colorCorrection, sunRays
local bloodMoonActive = false

local TWEEN = TweenInfo.new(4, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local function ensureEffects()
	atmosphere = Lighting:FindFirstChildOfClass("Atmosphere") or Util.make("Atmosphere", { Parent = Lighting })
	bloom = Lighting:FindFirstChild("TLSBloom") or Util.make("BloomEffect", {
		Name = "TLSBloom", Intensity = 0.4, Size = 32, Threshold = 1.6, Parent = Lighting })
	colorCorrection = Lighting:FindFirstChild("TLSColor") or Util.make("ColorCorrectionEffect", {
		Name = "TLSColor", Saturation = 0.02, Contrast = 0.08, Parent = Lighting })
	sunRays = Lighting:FindFirstChild("TLSRays") or Util.make("SunRaysEffect", {
		Name = "TLSRays", Intensity = 0.08, Spread = 0.6, Parent = Lighting })
end

local function applyPreset(preset, instant)
	local lightGoal = {
		Ambient = preset.ambient,
		OutdoorAmbient = preset.outdoorAmbient,
		FogColor = preset.fogColor,
		FogEnd = preset.fogEnd,
		Brightness = preset.brightness,
		ClockTime = preset.clock,
	}
	local atmoGoal = {
		Density = preset.density,
		Color = preset.atmoColor,
		Decay = preset.atmoDecay,
		Haze = preset.haze,
		Glare = preset.id == "BloodSunset" and 0.4 or 0.1,
		Offset = 0.4,
	}
	if instant then
		for k, v in pairs(lightGoal) do Lighting[k] = v end
		for k, v in pairs(atmoGoal) do atmosphere[k] = v end
	else
		TweenService:Create(Lighting, TWEEN, lightGoal):Play()
		TweenService:Create(atmosphere, TWEEN, atmoGoal):Play()
	end
	Lighting.GlobalShadows = true
	Lighting.EnvironmentDiffuseScale = 0.5
	Lighting.EnvironmentSpecularScale = 0.6
end

function WeatherService.set(index, announceIt)
	WeatherService.Index = ((index - 1) % #Config.Weather) + 1
	local preset = Config.Weather[WeatherService.Index]
	applyPreset(preset, false)
	remotes.Weather:FireAllClients({ index = WeatherService.Index, bloodMoon = bloodMoonActive })
	if announceIt then
		remotes.Announce:FireAllClients({ kanji = preset.kanji, text = preset.name, dur = 2.4 })
	end
end

function WeatherService.next()
	WeatherService.set(WeatherService.Index + 1, true)
end

local function bloodMoon()
	bloodMoonActive = true
	combatService.setBloodMoon(true)
	remotes.Announce:FireAllClients({ kanji = "血月", text = "BLOOD MOON — REWARDS DOUBLED", dur = 4 })
	remotes.Weather:FireAllClients({ index = WeatherService.Index, bloodMoon = true })
	local goal = {
		Ambient = Color3.fromRGB(70, 18, 26),
		OutdoorAmbient = Color3.fromRGB(110, 30, 40),
		FogColor = Color3.fromRGB(60, 10, 18),
		ClockTime = 0,
		Brightness = 1,
	}
	TweenService:Create(Lighting, TWEEN, goal):Play()
	TweenService:Create(atmosphere, TWEEN, {
		Color = Color3.fromRGB(150, 40, 50), Decay = Color3.fromRGB(60, 8, 16),
		Density = 0.42, Haze = 2.4,
	}):Play()
	task.wait(90)
	bloodMoonActive = false
	combatService.setBloodMoon(false)
	remotes.Announce:FireAllClients({ kanji = "夜明け", text = "THE MOON RELENTS", dur = 3 })
	WeatherService.set(WeatherService.Index, false)
end

function WeatherService.isBloodMoon()
	return bloodMoonActive
end

function WeatherService.init(remoteTable, combat)
	remotes = remoteTable
	combatService = combat
	ensureEffects()
	applyPreset(Config.Weather[1], true)
	remotes.Weather:FireAllClients({ index = 1, bloodMoon = false })

	-- weather cycle
	task.spawn(function()
		while true do
			task.wait(Config.WeatherCycleSeconds)
			if not bloodMoonActive then
				WeatherService.next()
			end
		end
	end)
	-- blood moon event
	task.spawn(function()
		while true do
			task.wait(480 + math.random(0, 120))
			bloodMoon()
		end
	end)
	-- storm thunder scheduling (clients render flash + sound)
	task.spawn(function()
		while true do
			task.wait(4 + math.random() * 7)
			if Config.Weather[WeatherService.Index].thunder and not bloodMoonActive then
				remotes.Weather:FireAllClients({ thunder = true })
			end
		end
	end)
end

return WeatherService
]]></ProtectedString></Properties></Item></Item></Item><Item class="StarterPlayer" referent="RBX28"><Properties><string name="Name">StarterPlayer</string></Properties><Item class="StarterPlayerScripts" referent="RBX19"><Properties><string name="Name">StarterPlayerScripts</string></Properties><Item class="LocalScript" referent="RBX27"><Properties><string name="Name">Client</string><ProtectedString name="Source"><![CDATA[--[[
	THE LAST SAMURAI — client bootstrap.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Net = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Net"))

local SoundController = require(script:WaitForChild("SoundController"))
local AnimationController = require(script:WaitForChild("AnimationController"))
local VFXController = require(script:WaitForChild("VFXController"))
local CameraController = require(script:WaitForChild("CameraController"))
local UIController = require(script:WaitForChild("UIController"))
local CombatController = require(script:WaitForChild("CombatController"))

local remotes = Net.get()

SoundController.init(remotes)
AnimationController.init(remotes)
VFXController.init(remotes)
CameraController.init(remotes)
UIController.init(remotes)
CombatController.init(remotes)
]]></ProtectedString></Properties><Item class="ModuleScript" referent="RBX20"><Properties><string name="Name">AnimationController</string><ProtectedString name="Source"><![CDATA[--[[
	AnimationController — procedural Motor6D combat animation for EVERY actor.

	Poses come from Shared/AnimPoses in character space. For each rig we cache
	base Motor6D C0s, then conjugate: C0' = T(pos+py) * R(characterSpace) * Rot(base).
	Rotations therefore behave identically on R6 players, R15 players and NPC
	rigs — no per-rig pose data.

	Layering: Roblox's default locomotion animations write Motor6D.Transform;
	we write C0. The two compose, which is exactly what we want — combat poses
	ride on top of walking. NPC rigs have no Animate script, so their legs get
	a procedural walk swing driven by velocity.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local AnimPoses = require(Shared:WaitForChild("AnimPoses"))
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))

local Bus = require(script.Parent:WaitForChild("ClientBus"))

local AnimationController = {}
local rigs = {} -- actorId -> rig

local JOINTS = { "Root", "Neck", "RS", "LS", "RH", "LH" }
local JOINT_NAMES = {
	Root = { "RootJoint", "Waist" },
	Neck = { "Neck" },
	RS = { "Right Shoulder", "RightShoulder" },
	LS = { "Left Shoulder", "LeftShoulder" },
	RH = { "Right Hip", "RightHip" },
	LH = { "Left Hip", "LeftHip" },
}

local FIELDS = { "x", "y", "z", "py" }

-- ------------------------------------------------------------- rig setup

local function findJoints(model)
	local found = {}
	for _, desc in ipairs(model:GetDescendants()) do
		if desc:IsA("Motor6D") then
			for abstract, names in pairs(JOINT_NAMES) do
				for _, n in ipairs(names) do
					if desc.Name == n and not found[abstract] then
						found[abstract] = { motor = desc, baseC0 = desc.C0 }
					end
				end
			end
		end
	end
	return found
end

local function newPoseState()
	local state = {}
	for _, j in ipairs(JOINTS) do
		state[j] = { x = 0, y = 0, z = 0, py = 0 }
	end
	return state
end

function AnimationController.addActor(id, model, isNPC)
	if rigs[id] then
		AnimationController.removeActor(id)
	end
	local rig = {
		id = id, model = model, isNPC = isNPC,
		joints = findJoints(model),
		current = newPoseState(),
		clip = nil, clipT = 0, clipName = nil, clipExpire = nil,
		blocking = false,
		legPhase = 0,
		root = model:FindFirstChild("HumanoidRootPart"),
		breath = math.random() * 6,
	}
	rig.katana = model:FindFirstChild("Katana")
	if not rig.katana then
		task.delay(1, function()
			if rig.model then
				rig.katana = rig.model:FindFirstChild("Katana")
			end
		end)
	end
	rigs[id] = rig
	return rig
end

function AnimationController.removeActor(id)
	rigs[id] = nil
end

-- ------------------------------------------------------------- clip control

function AnimationController.play(id, clipName, expireAfter)
	local rig = rigs[id]
	local clip = AnimPoses[clipName]
	if not rig or not clip then return end
	rig.clip = clip
	rig.clipName = clipName
	rig.clipT = 0
	rig.clipExpire = expireAfter and (os.clock() + expireAfter) or nil
end

function AnimationController.stop(id, onlyIfClip)
	local rig = rigs[id]
	if not rig then return end
	if onlyIfClip and rig.clipName ~= onlyIfClip then return end
	rig.clip = nil
	rig.clipName = nil
end

function AnimationController.setBlocking(id, on)
	local rig = rigs[id]
	if rig then
		rig.blocking = on
	end
end

-- ------------------------------------------------------------- sampling

local function sampleClip(clip, u, target)
	local keys = clip.keys
	local a, b = keys[1], keys[#keys]
	for i = 1, #keys - 1 do
		if u >= keys[i].t and u <= keys[i + 1].t then
			a, b = keys[i], keys[i + 1]
			break
		end
	end
	local span = b.t - a.t
	local f = span > 0 and Util.ease(Util.clamp((u - a.t) / span, 0, 1)) or 1
	for _, j in ipairs(JOINTS) do
		local pa = a.pose[j]
		local pb = b.pose[j]
		local tj = target[j]
		for _, field in ipairs(FIELDS) do
			local va = pa and pa[field] or 0
			local vb = pb and pb[field] or 0
			tj[field] = va + (vb - va) * f
		end
	end
end

local function sampleGuard(rig, target, dt)
	rig.breath = rig.breath + dt
	sampleClip(AnimPoses.Guard, (rig.breath % AnimPoses.Guard.dur) / AnimPoses.Guard.dur, target)
	-- velocity lean
	if rig.root then
		local vel = rig.root.AssemblyLinearVelocity
		local speed = Vector3.new(vel.X, 0, vel.Z).Magnitude
		if speed > 2 then
			local look = rig.root.CFrame.LookVector
			local forwardness = Util.flatDirection(vel):Dot(Util.flatDirection(look))
			target.Root.x = target.Root.x + Util.clamp(speed * 0.5 * forwardness, -8, 10)
			if speed > Config.Combat.SprintSpeed - 2 then
				-- sprint: katana carried low and back
				local sp = AnimPoses.Sprint.keys[1].pose
				target.Root.x = target.Root.x + (sp.Root and sp.Root.x or 0)
				target.RS.x = sp.RS.x
				target.RS.y = sp.RS.y
				target.LS.x = sp.LS.x
				target.LS.y = sp.LS.y
			end
		end
	end
end

local scratch = newPoseState()

local function updateRig(rig, dt, frozen)
	if not rig.model or not rig.model.Parent then
		rigs[rig.id] = nil
		return
	end
	local target = scratch

	-- advance clip
	if rig.clip and not frozen then
		rig.clipT = rig.clipT + dt
		local dur = rig.clip.dur
		if rig.clipT >= dur then
			if rig.clip.loop then
				rig.clipT = rig.clipT % dur
			elseif rig.clip.hold then
				rig.clipT = dur
			else
				rig.clip = nil
				rig.clipName = nil
			end
		end
		if rig.clipExpire and os.clock() > rig.clipExpire then
			rig.clip = nil
			rig.clipName = nil
			rig.clipExpire = nil
		end
	end

	if rig.clip then
		sampleClip(rig.clip, Util.clamp(rig.clipT / rig.clip.dur, 0, 1), target)
	elseif rig.blocking then
		sampleClip(AnimPoses.Block, 1, target)
	else
		sampleGuard(rig, target, dt)
	end

	-- NPC procedural leg walk (players get real locomotion from Animate)
	if rig.isNPC and rig.root then
		local vel = rig.root.AssemblyLinearVelocity
		local speed = Vector3.new(vel.X, 0, vel.Z).Magnitude
		if speed > 1 then
			rig.legPhase = rig.legPhase + dt * (speed * 0.9)
			local swing = math.sin(rig.legPhase) * Util.clamp(speed * 3.2, 0, 38)
			-- only override legs the clip left neutral
			if math.abs(target.RH.x) < 1 then target.RH.x = swing end
			if math.abs(target.LH.x) < 1 then target.LH.x = -swing end
		end
	end

	-- smooth toward target (weight = the "feel")
	local alpha = frozen and 0 or Util.clamp(dt * 14, 0, 1)
	for _, j in ipairs(JOINTS) do
		local cur, tgt = rig.current[j], target[j]
		for _, field in ipairs(FIELDS) do
			cur[field] = cur[field] + (tgt[field] - cur[field]) * alpha
		end
	end

	-- apply to motors: C0' = T(pos + py) * R(character space) * Rot(base)
	for _, j in ipairs(JOINTS) do
		local joint = rig.joints[j]
		if joint and joint.motor.Parent then
			local p = rig.current[j]
			local base = joint.baseC0
			local rot = CFrame.Angles(math.rad(-p.x), math.rad(-p.y), math.rad(p.z))
			joint.motor.C0 = CFrame.new(base.Position + Vector3.new(0, p.py, 0)) * rot * base.Rotation
		end
	end

	-- katana trail window
	if rig.katana then
		local blade = rig.katana:FindFirstChild("Blade")
		local trail = blade and blade:FindFirstChild("SwingTrail")
		if trail then
			local wantOn = false
			if rig.clip and rig.clip.trail then
				local u = rig.clipT / rig.clip.dur
				wantOn = u >= rig.clip.trail[1] and u <= rig.clip.trail[2]
			end
			if trail.Enabled ~= wantOn then
				trail.Enabled = wantOn
			end
		end
	end
end

-- ------------------------------------------------------------- events

local CLIP_FOR_EVENT = {
	guardbreak = { clip = "GuardBroken", expire = Config.Combat.GuardBreakStun },
	dash = { clip = "Dash", expire = 0.3 },
	charge = { clip = "HeavyCharge" },
	ult = { clip = "HeavyRelease" },
}

function AnimationController.init(remotes)
	remotes.Actors.OnClientEvent:Connect(function(msg)
		if msg.gone then
			AnimationController.removeActor(msg.id)
		elseif msg.model then
			task.defer(function()
				if msg.model.Parent then
					AnimationController.addActor(msg.id, msg.model, msg.npc)
				end
			end)
		end
	end)

	remotes.Combat.OnClientEvent:Connect(function(ev)
		local e = ev.e
		if e == "attack" then
			local clipName = AnimPoses.AttackClips[ev.key]
			if clipName then
				AnimationController.setBlocking(ev.id, false)
				AnimationController.play(ev.id, clipName)
			end
		elseif e == "block" then
			AnimationController.setBlocking(ev.id, ev.on)
			if ev.on then
				AnimationController.stop(ev.id)
			end
		elseif e == "parry" then
			AnimationController.play(ev.id, "ParryFlash")
			if ev.aid then
				AnimationController.play(ev.aid, "Stagger")
			end
			Bus.hitstop(0.07)
		elseif e == "hit" then
			if not ev.blocked and ev.tid then
				AnimationController.play(ev.tid, "Hurt")
			end
			Bus.hitstop(ev.heavy and 0.06 or 0.035)
		elseif e == "guardbreak" or e == "dash" or e == "charge" then
			local def = CLIP_FOR_EVENT[e]
			AnimationController.setBlocking(ev.id, false)
			AnimationController.play(ev.id, def.clip, def.expire)
		elseif e == "ult" then
			for i = 0, Config.Ult.Hits - 1 do
				task.delay(i * Config.Ult.Interval, function()
					AnimationController.play(ev.id, i % 2 == 0 and "HeavyRelease" or "Slash1")
				end)
			end
		elseif e == "execute" then
			AnimationController.play(ev.id, "ExecuteAttacker")
			AnimationController.play(ev.tid, "ExecuteVictim")
		elseif e == "death" then
			if not (rigs[ev.id] and rigs[ev.id].clipName == "ExecuteVictim") then
				AnimationController.play(ev.id, "Death")
			end
		elseif e == "stance" then
			-- brief acknowledgment flourish
			AnimationController.play(ev.id, "ParryFlash")
		end
	end)

	RunService.RenderStepped:Connect(function(dt)
		local frozen = os.clock() < Bus.hitstopUntil
		for _, rig in pairs(rigs) do
			updateRig(rig, dt, frozen)
		end
	end)
end

function AnimationController.getRig(id)
	return rigs[id]
end

return AnimationController
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX21"><Properties><string name="Name">CameraController</string><ProtectedString name="Source"><![CDATA[--[[
	CameraController — trauma-based shake, FOV language, lock-on framing and
	the execution cinematic. Shake uses smooth noise on top of whatever the
	default camera did this frame, so it composes with player control.
]]

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

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Util = require(Shared:WaitForChild("Util"))
local Bus = require(script.Parent:WaitForChild("ClientBus"))

local CameraController = {}
local player = Players.LocalPlayer
local camera = Workspace.CurrentCamera

local BASE_FOV = 70
local SPRINT_FOV = 78

local trauma = 0
local fovPunch = 0
local fovPunchDecay = 4
local lockTarget = nil -- model
local cine = nil -- { attacker, victim, untilT }
local noiseT = 0

local function shake(amp)
	trauma = math.min(1.2, trauma + amp)
end

function CameraController.init(remotes)
	Bus.Shake:Connect(shake)
	Bus.FOVPunch:Connect(function(delta, decay)
		fovPunch = delta
		fovPunchDecay = 1 / math.max(decay, 0.05)
	end)
	Bus.LockOn:Connect(function(_, model)
		lockTarget = model
	end)
	Bus.ExecuteCam:Connect(function(attacker, victim, duration)
		cine = { attacker = attacker, victim = victim, untilT = os.clock() + duration }
	end)

	camera.FieldOfView = BASE_FOV

	RunService:BindToRenderStep("TLSCamera", Enum.RenderPriority.Camera.Value + 1, function(dt)
		camera = Workspace.CurrentCamera
		local char = player.Character
		local root = char and char:FindFirstChild("HumanoidRootPart")
		local humanoid = char and char:FindFirstChildOfClass("Humanoid")

		-- execution cinematic: low orbit around the pair
		if cine then
			if os.clock() > cine.untilT
				or not cine.victim or not cine.victim.Parent then
				cine = nil
				camera.CameraType = Enum.CameraType.Custom
			else
				local vRoot = cine.victim:FindFirstChild("HumanoidRootPart")
				local aRoot = cine.attacker and cine.attacker:FindFirstChild("HumanoidRootPart")
				if vRoot then
					camera.CameraType = Enum.CameraType.Scriptable
					local mid = aRoot and (vRoot.Position + aRoot.Position) / 2 or vRoot.Position
					local angle = (os.clock() * 0.35) % (math.pi * 2)
					local eye = mid + Vector3.new(math.cos(angle) * 11, 3.4, math.sin(angle) * 11)
					camera.CFrame = camera.CFrame:Lerp(CFrame.lookAt(eye, mid + Vector3.new(0, 1.4, 0)), Util.clamp(dt * 5, 0, 1))
				end
			end
		elseif lockTarget and lockTarget.Parent and root then
			-- lock-on: shoulder frame holding both fighters
			local tRoot = lockTarget:FindFirstChild("HumanoidRootPart")
			if tRoot then
				camera.CameraType = Enum.CameraType.Scriptable
				local toTarget = Util.flatDirection(tRoot.Position - root.Position)
				local eye = root.Position - toTarget * 11 + Vector3.new(0, 4.4, 0)
					+ camera.CFrame.RightVector * 1.6
				local look = (root.Position + tRoot.Position) / 2 + Vector3.new(0, 1.2, 0)
				camera.CFrame = camera.CFrame:Lerp(CFrame.lookAt(eye, look), Util.clamp(dt * 9, 0, 1))
			else
				lockTarget = nil
				camera.CameraType = Enum.CameraType.Custom
			end
		else
			if camera.CameraType == Enum.CameraType.Scriptable then
				camera.CameraType = Enum.CameraType.Custom
			end
		end

		-- FOV: sprint stretch + punches
		local targetFOV = BASE_FOV
		if humanoid and humanoid.WalkSpeed > 18 and humanoid.MoveDirection.Magnitude > 0.1 then
			targetFOV = SPRINT_FOV
		end
		fovPunch = fovPunch * math.max(0, 1 - dt * fovPunchDecay)
		camera.FieldOfView = Util.lerp(camera.FieldOfView, targetFOV + fovPunch, Util.clamp(dt * 6, 0, 1))

		-- trauma shake (smooth pseudo-noise, decays quadratically)
		if trauma > 0.003 then
			noiseT = noiseT + dt * 30
			local amp = trauma * trauma
			local rx = (math.noise(noiseT, 0) or 0) * amp * 0.06
			local ry = (math.noise(0, noiseT) or 0) * amp * 0.06
			local rz = (math.noise(noiseT, noiseT) or 0) * amp * 0.02
			camera.CFrame = camera.CFrame * CFrame.Angles(rx, ry, rz)
			trauma = math.max(0, trauma - dt * 2.2)
		end
	end)

	-- big deaths near us rumble
	remotes.Combat.OnClientEvent:Connect(function(ev)
		if ev.e == "death" and ev.pos then
			local char = player.Character
			local root = char and char:FindFirstChild("HumanoidRootPart")
			if root and (root.Position - ev.pos).Magnitude < 40 then
				shake(0.5)
			end
		end
	end)
end

function CameraController.setLockTarget(model)
	lockTarget = model
end

return CameraController
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX22"><Properties><string name="Name">ClientBus</string><ProtectedString name="Source"><![CDATA[--[[
	ClientBus — local signals that let client controllers talk without
	depending on each other directly.
]]

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Util = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Util"))

local Bus = {
	Shake = Util.Signal.new(),        -- (amplitude)
	FOVPunch = Util.Signal.new(),     -- (delta, decaySeconds)
	Letterbox = Util.Signal.new(),    -- (on)
	LockOn = Util.Signal.new(),       -- (actorId or nil, model or nil)
	ExecuteCam = Util.Signal.new(),   -- (attackerModel, victimModel, duration)
	Announce = Util.Signal.new(),     -- (kanji, text, dur)
	ExecutePrompt = Util.Signal.new(),-- (visible)
	ComboPulse = Util.Signal.new(),   -- (count)

	hitstopUntil = 0,
}

function Bus.hitstop(duration)
	local untilT = os.clock() + duration
	if untilT > Bus.hitstopUntil then
		Bus.hitstopUntil = untilT
	end
end

return Bus
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX23"><Properties><string name="Name">CombatController</string><ProtectedString name="Source"><![CDATA[--[[
	CombatController — turns player intent into server actions.
	Keyboard+mouse, gamepad and touch (ContextActionService buttons).
	Client-side prediction is limited to what can't be felt wrong later:
	instant block pose, instant dash velocity. Everything else waits for
	the server's word.
]]

local Players = game:GetService("Players")
local UserInputService = game:GetService("UserInputService")
local ContextActionService = game:GetService("ContextActionService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))
local Bus = require(script.Parent:WaitForChild("ClientBus"))
local AnimationController = require(script.Parent:WaitForChild("AnimationController"))

local CombatController = {}
local player = Players.LocalPlayer
local remotes
local myId = player.UserId

local actorModels = {} -- id -> model (from registry)
local vulnerableUntil = {} -- id -> os.clock() deadline
local lastDash = 0
local charging = false
local sprinting = false
local lockId = nil

local function send(action)
	remotes.Action:FireServer(action)
end

local function character()
	return player.Character
end

local function root()
	local char = character()
	return char and char:FindFirstChild("HumanoidRootPart")
end

-- ------------------------------------------------------------- actions

local function doLight()
	if charging then return end
	if os.clock() - lastDash < 0.25 then
		send({ a = "dash_attack" })
	else
		send({ a = "light" })
	end
end

local function heavyStart()
	charging = true
	send({ a = "heavy_start" })
end

local function heavyRelease()
	if charging then
		charging = false
		send({ a = "heavy_release" })
	end
end

local function setBlock(on)
	send({ a = "block", on = on })
	AnimationController.setBlocking(myId, on) -- instant pose; server confirms
end

local function doDash()
	local r = root()
	local char = character()
	local humanoid = char and char:FindFirstChildOfClass("Humanoid")
	if not r or not humanoid then return end
	lastDash = os.clock()
	send({ a = "dash" })
	-- immediate motion: server has validated cost by the time others see it
	local dir = humanoid.MoveDirection.Magnitude > 0.1 and humanoid.MoveDirection
		or Util.flatDirection(r.CFrame.LookVector)
	r.AssemblyLinearVelocity = dir * Config.Combat.DashSpeed + Vector3.new(0, 2, 0)
	Bus.FOVPunch:Fire(6, 0.3)
end

local function setSprint(on)
	if sprinting ~= on then
		sprinting = on
		send({ a = "sprint", on = on })
	end
end

local function doExecute()
	send({ a = "execute" })
end

local function doUlt()
	send({ a = "ult" })
end

local function setStance(i)
	send({ a = "stance", i = i })
end

-- ------------------------------------------------------------- lock-on

local function nearestActor()
	local r = root()
	if not r then return nil, nil end
	local best, bestId, bestDist = nil, nil, 70
	for id, model in pairs(actorModels) do
		if id ~= myId and model.Parent then
			local mRoot = model:FindFirstChild("HumanoidRootPart")
			local humanoid = model:FindFirstChildOfClass("Humanoid")
			if mRoot and humanoid and humanoid.Health > 0 then
				local d = (mRoot.Position - r.Position).Magnitude
				if d < bestDist then
					best, bestId, bestDist = model, id, d
				end
			end
		end
	end
	return best, bestId
end

local function toggleLock()
	if lockId then
		lockId = nil
		Bus.LockOn:Fire(nil, nil)
		local char = character()
		local humanoid = char and char:FindFirstChildOfClass("Humanoid")
		if humanoid then
			humanoid.AutoRotate = true
		end
	else
		local model, id = nearestActor()
		if model then
			lockId = id
			Bus.LockOn:Fire(id, model)
			local char = character()
			local humanoid = char and char:FindFirstChildOfClass("Humanoid")
			if humanoid then
				humanoid.AutoRotate = false
			end
		end
	end
end

-- ------------------------------------------------------------- input wiring

local function bindInputs()
	UserInputService.InputBegan:Connect(function(input, processed)
		if processed then return end
		local t = input.UserInputType
		local k = input.KeyCode
		if t == Enum.UserInputType.MouseButton1 then
			doLight()
		elseif k == Enum.KeyCode.R then
			heavyStart()
		elseif k == Enum.KeyCode.F then
			setBlock(true)
		elseif k == Enum.KeyCode.Q then
			doDash()
		elseif k == Enum.KeyCode.LeftShift then
			setSprint(true)
		elseif k == Enum.KeyCode.E then
			doExecute()
		elseif k == Enum.KeyCode.V then
			doUlt()
		elseif k == Enum.KeyCode.Tab then
			toggleLock()
		elseif k == Enum.KeyCode.One then
			setStance(1)
		elseif k == Enum.KeyCode.Two then
			setStance(2)
		elseif k == Enum.KeyCode.Three then
			setStance(3)
		elseif k == Enum.KeyCode.Four then
			setStance(4)
		elseif k == Enum.KeyCode.ButtonR2 then
			doLight()
		elseif k == Enum.KeyCode.ButtonL2 then
			setBlock(true)
		elseif k == Enum.KeyCode.ButtonX then
			doDash()
		elseif k == Enum.KeyCode.ButtonY then
			heavyStart()
		elseif k == Enum.KeyCode.ButtonB then
			doExecute()
		elseif k == Enum.KeyCode.ButtonR3 then
			toggleLock()
		elseif k == Enum.KeyCode.DPadUp then
			doUlt()
		end
	end)

	UserInputService.InputEnded:Connect(function(input)
		local k = input.KeyCode
		if k == Enum.KeyCode.R or k == Enum.KeyCode.ButtonY then
			heavyRelease()
		elseif k == Enum.KeyCode.F or k == Enum.KeyCode.ButtonL2 then
			setBlock(false)
		elseif k == Enum.KeyCode.LeftShift then
			setSprint(false)
		end
	end)

	-- touch buttons
	if UserInputService.TouchEnabled and not UserInputService.KeyboardEnabled then
		local function bindBtn(name, title, pos, began, ended)
			ContextActionService:BindAction(name, function(_, state)
				if state == Enum.UserInputState.Begin then
					began()
				elseif state == Enum.UserInputState.End and ended then
					ended()
				end
				return Enum.ContextActionResult.Sink
			end, true)
			ContextActionService:SetTitle(name, title)
			ContextActionService:SetPosition(name, pos)
		end
		bindBtn("TLS_ATK", "斬", UDim2.new(1, -95, 1, -140), doLight)
		bindBtn("TLS_HVY", "重", UDim2.new(1, -165, 1, -95), heavyStart, heavyRelease)
		bindBtn("TLS_BLK", "防", UDim2.new(1, -95, 1, -215), function() setBlock(true) end, function() setBlock(false) end)
		bindBtn("TLS_DSH", "駆", UDim2.new(1, -165, 1, -180), doDash)
		bindBtn("TLS_ULT", "龍", UDim2.new(1, -40, 1, -215), doUlt)
		bindBtn("TLS_EXE", "閃", UDim2.new(1, -230, 1, -140), doExecute)
		bindBtn("TLS_LCK", "鎖", UDim2.new(1, -40, 1, -285), toggleLock)
	end
end

-- ------------------------------------------------------------- loops

local function watchVulnerability()
	RunService.Heartbeat:Connect(function()
		local r = root()
		if not r then
			Bus.ExecutePrompt:Fire(false)
			return
		end
		local t = os.clock()
		local showPrompt = false
		for id, deadline in pairs(vulnerableUntil) do
			if t > deadline then
				vulnerableUntil[id] = nil
			else
				local model = actorModels[id]
				local mRoot = model and model:FindFirstChild("HumanoidRootPart")
				if mRoot and (mRoot.Position - r.Position).Magnitude < Config.Combat.ExecuteRange then
					showPrompt = true
				end
			end
		end
		-- also low-HP actors nearby
		if not showPrompt then
			for id, model in pairs(actorModels) do
				if id ~= myId and model.Parent then
					local humanoid = model:FindFirstChildOfClass("Humanoid")
					local mRoot = model:FindFirstChild("HumanoidRootPart")
					if humanoid and mRoot and humanoid.Health > 0
						and humanoid.Health <= Config.Combat.ExecuteHPThreshold
						and (mRoot.Position - r.Position).Magnitude < Config.Combat.ExecuteRange then
						showPrompt = true
					end
				end
			end
		end
		Bus.ExecutePrompt:Fire(showPrompt)

		-- lock-on facing
		if lockId then
			local model = actorModels[lockId]
			local mRoot = model and model.Parent and model:FindFirstChild("HumanoidRootPart")
			local humanoid = model and model:FindFirstChildOfClass("Humanoid")
			if mRoot and humanoid and humanoid.Health > 0
				and (mRoot.Position - r.Position).Magnitude < 90 then
				r.CFrame = Util.yawCFrame(r.Position, mRoot.Position)
			else
				toggleLock()
			end
		end
	end)
end

function CombatController.init(remoteTable)
	remotes = remoteTable

	remotes.Actors.OnClientEvent:Connect(function(msg)
		if msg.gone then
			actorModels[msg.id] = nil
			if lockId == msg.id then
				toggleLock()
			end
		elseif msg.model then
			actorModels[msg.id] = msg.model
		end
	end)

	remotes.Combat.OnClientEvent:Connect(function(ev)
		if ev.e == "guardbreak" then
			vulnerableUntil[ev.id] = os.clock() + Config.Combat.GuardBreakStun
		elseif ev.e == "death" then
			vulnerableUntil[ev.id] = nil
			if ev.id == myId then
				charging = false
				sprinting = false
				if lockId then
					toggleLock()
				end
			end
		elseif ev.e == "execute" then
			local attacker = actorModels[ev.id]
			local victim = actorModels[ev.tid]
			local r = root()
			if r and ev.pos and (r.Position - ev.pos).Magnitude < 60 then
				Bus.ExecuteCam:Fire(attacker, victim, Config.Combat.ExecuteDuration + 0.3)
			end
		end
	end)

	bindInputs()
	watchVulnerability()
end

return CombatController
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX24"><Properties><string name="Name">SoundController</string><ProtectedString name="Source"><![CDATA[--[[
	SoundController — positional combat audio from engine-built-in sounds.
	One pooled emitter per shot; pitch ranges keep repeated hits organic.
]]

local Workspace = game:GetService("Workspace")
local SoundService = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Sounds = require(Shared:WaitForChild("Sounds"))
local Util = require(Shared:WaitForChild("Util"))

local SoundController = {}

local function rndRange(range)
	return range[1] + math.random() * (range[2] - range[1])
end

-- play at a world position (nil position = non-positional UI sound)
function SoundController.play(name, position)
	local def = Sounds.Map[name]
	if not def then return end
	local sound = Instance.new("Sound")
	sound.SoundId = def.id
	sound.Volume = def.volume
	sound.PlaybackSpeed = rndRange(def.pitch)
	sound.RollOffMaxDistance = 120
	sound.RollOffMinDistance = 8

	if position then
		local anchor = Instance.new("Attachment")
		anchor.Position = position
		anchor.Parent = Workspace.Terrain
		sound.Parent = anchor
		sound.Ended:Once(function()
			anchor:Destroy()
		end)
		task.delay(4, function()
			if anchor.Parent then
				anchor:Destroy()
			end
		end)
	else
		sound.Parent = SoundService
		sound.Ended:Once(function()
			sound:Destroy()
		end)
		task.delay(4, function()
			if sound.Parent then
				sound:Destroy()
			end
		end)
	end
	sound:Play()
end

function SoundController.init(remotes)
	remotes.Combat.OnClientEvent:Connect(function(ev)
		if ev.e == "attack" then
			SoundController.play(ev.key == "Heavy" and "SlashHeavy" or "Slash", ev.pos)
		elseif ev.e == "hit" then
			if ev.blocked then
				SoundController.play("Clash", ev.pos)
			else
				SoundController.play("Thud", ev.pos)
			end
		elseif ev.e == "parry" then
			SoundController.play("Parry", ev.pos)
			SoundController.play("ParryRing", ev.pos)
		elseif ev.e == "guardbreak" then
			SoundController.play("GuardBreak", ev.pos)
			SoundController.play("Bell")
		elseif ev.e == "dash" then
			SoundController.play("Dash")
		elseif ev.e == "execute" then
			SoundController.play("Execute", ev.pos)
			SoundController.play("Bell")
		elseif ev.e == "ult" then
			SoundController.play("Lunge", ev.pos)
			SoundController.play("Bell")
		elseif ev.e == "death" then
			SoundController.play("Thud", ev.pos)
		end
	end)

	remotes.Weather.OnClientEvent:Connect(function(ev)
		if ev.thunder then
			task.delay(0.3 + math.random(), function()
				SoundController.play("GuardBreak") -- deep crack doubles as distant thunder
				SoundController.play("Bell")
			end)
		end
	end)

	remotes.Announce.OnClientEvent:Connect(function()
		SoundController.play("Bell")
	end)
end

return SoundController
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX25"><Properties><string name="Name">UIController</string><ProtectedString name="Source"><![CDATA[--[[
	UIController — the lacquer & gold HUD, built entirely in code.
	Vitals bottom-left, ult bottom-right, announcer kanji center, kill feed,
	combo counter, world-space damage numbers, NPC nameplates, letterbox.
]]

local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))
local Bus = require(script.Parent:WaitForChild("ClientBus"))

local UIController = {}
local player = Players.LocalPlayer
local PAL = Config.Palette

local gui, hpFill, poFill, stFill, ultFill, ultKanji, stanceKanji, stanceName
local annKanji, annText, feedFrame, comboFrame, comboCount, execPrompt, lbTop, lbBottom

local function make(cls, props)
	return Util.make(cls, props)
end

local function spaced(text)
	local out = {}
	for _, cp in utf8.codes(text) do
		out[#out + 1] = utf8.char(cp)
	end
	return table.concat(out, " ")
end

-- ------------------------------------------------------------- construction

local function bar(parent, order, height, fillColor)
	local holder = make("Frame", {
		BackgroundColor3 = Color3.fromRGB(30, 28, 32), BackgroundTransparency = 0.35,
		BorderSizePixel = 0, Size = UDim2.new(1, 0, 0, height), LayoutOrder = order,
		Parent = parent,
	})
	make("UIStroke", { Color = PAL.Washi, Transparency = 0.82, Parent = holder })
	local fill = make("Frame", {
		BackgroundColor3 = fillColor, BorderSizePixel = 0,
		Size = UDim2.new(1, 0, 1, 0), Parent = holder,
	})
	make("UIGradient", {
		Color = ColorSequence.new(fillColor, Color3.new(
			math.min(fillColor.R * 1.35, 1), math.min(fillColor.G * 1.35, 1), math.min(fillColor.B * 1.35, 1))),
		Parent = fill,
	})
	return fill
end

local function label(parent, order, text, size, color, font)
	return make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, size + 4),
		Font = font or Enum.Font.Garamond, Text = text, TextSize = size,
		TextColor3 = color or PAL.Washi, TextXAlignment = Enum.TextXAlignment.Left,
		LayoutOrder = order, Parent = parent,
	})
end

local function buildHUD()
	gui = make("ScreenGui", {
		Name = "TLSHUD", ResetOnSpawn = false, IgnoreGuiInset = true,
		DisplayOrder = 5, Parent = player:WaitForChild("PlayerGui"),
	})

	-- vitals (bottom left)
	local vitals = make("Frame", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 24, 1, -132),
		Size = UDim2.new(0, 300, 0, 120), Parent = gui,
	})
	make("UIListLayout", { Padding = UDim.new(0, 5), SortOrder = Enum.SortOrder.LayoutOrder, Parent = vitals })
	label(vitals, 1, "K A T S U   ·   活", 11, PAL.Washi).TextTransparency = 0.4
	hpFill = bar(vitals, 2, 12, PAL.Shu)
	label(vitals, 3, "P O S T U R E   ·   体幹", 11, PAL.Washi).TextTransparency = 0.4
	poFill = bar(vitals, 4, 10, PAL.Kin)
	stFill = bar(vitals, 5, 6, PAL.Steel)
	local stanceRow = make("Frame", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 30), LayoutOrder = 6, Parent = vitals,
	})
	stanceKanji = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(0, 30, 1, 0),
		Font = Enum.Font.Antique, Text = "水", TextSize = 26, TextColor3 = PAL.Kin, Parent = stanceRow,
	})
	stanceName = make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 38, 0, 0), Size = UDim2.new(1, -38, 1, 0),
		Font = Enum.Font.Garamond, Text = "WATER STANCE — precise parries", TextSize = 13,
		TextColor3 = PAL.Washi, TextTransparency = 0.25, TextXAlignment = Enum.TextXAlignment.Left,
		Parent = stanceRow,
	})

	-- ult (bottom right)
	local ult = make("Frame", {
		BackgroundColor3 = PAL.Lacquer, BackgroundTransparency = 0.3,
		Position = UDim2.new(1, -110, 1, -122), Size = UDim2.new(0, 86, 0, 86), Parent = gui,
	})
	make("UICorner", { CornerRadius = UDim.new(1, 0), Parent = ult })
	make("UIStroke", { Color = PAL.Kin, Transparency = 0.5, Thickness = 1.4, Parent = ult })
	ultKanji = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0),
		Font = Enum.Font.Antique, Text = "龍", TextSize = 40,
		TextColor3 = PAL.Washi, TextTransparency = 0.45, Parent = ult,
	})
	local ultBarHolder = make("Frame", {
		BackgroundColor3 = Color3.fromRGB(30, 28, 32), BorderSizePixel = 0,
		Position = UDim2.new(0, -8, 1, 8), Size = UDim2.new(1, 16, 0, 5), Parent = ult,
	})
	ultFill = make("Frame", {
		BackgroundColor3 = PAL.Kin, BorderSizePixel = 0, Size = UDim2.new(0, 0, 1, 0),
		Parent = ultBarHolder,
	})
	make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 0, 1, 16), Size = UDim2.new(1, 0, 0, 14),
		Font = Enum.Font.Garamond, Text = "V  —  ULTIMATE", TextSize = 11,
		TextColor3 = PAL.Washi, TextTransparency = 0.5, Parent = ult,
	})

	-- announcer (center)
	local ann = make("Frame", {
		BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 0.3, 0),
		AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.new(0.8, 0, 0, 160), Parent = gui,
	})
	annKanji = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 110),
		Font = Enum.Font.Antique, Text = "", TextSize = 92,
		TextColor3 = PAL.Washi, TextTransparency = 1, Parent = ann,
	})
	annText = make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 0, 0, 112), Size = UDim2.new(1, 0, 0, 22),
		Font = Enum.Font.Garamond, Text = "", TextSize = 17,
		TextColor3 = PAL.Kin, TextTransparency = 1, Parent = ann,
	})

	-- kill feed
	feedFrame = make("Frame", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 24, 0, 24),
		Size = UDim2.new(0, 340, 0, 120), Parent = gui,
	})
	make("UIListLayout", { Padding = UDim.new(0, 3), SortOrder = Enum.SortOrder.LayoutOrder, Parent = feedFrame })

	-- combo counter
	comboFrame = make("Frame", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 28, 0.36, 0),
		Size = UDim2.new(0, 160, 0, 70), Visible = false, Parent = gui,
	})
	comboCount = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 46),
		Font = Enum.Font.Garamond, Text = "2", TextSize = 44, TextColor3 = PAL.Kin,
		TextXAlignment = Enum.TextXAlignment.Left, Parent = comboFrame,
	})
	make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0, 2, 0, 46), Size = UDim2.new(1, 0, 0, 14),
		Font = Enum.Font.Garamond, Text = "H I T   C O M B O", TextSize = 11,
		TextColor3 = PAL.Washi, TextTransparency = 0.4,
		TextXAlignment = Enum.TextXAlignment.Left, Parent = comboFrame,
	})

	-- execute prompt
	execPrompt = make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 0.62, 0),
		AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.new(0, 400, 0, 30),
		Font = Enum.Font.Garamond, Text = "E   —   一閃   E X E C U T E",
		TextSize = 20, TextColor3 = Color3.fromRGB(255, 106, 94), Visible = false, Parent = gui,
	})
	task.spawn(function()
		while true do
			if execPrompt.Visible then
				TweenService:Create(execPrompt, TweenInfo.new(0.5), { TextTransparency = 0.5 }):Play()
				task.wait(0.5)
				TweenService:Create(execPrompt, TweenInfo.new(0.5), { TextTransparency = 0 }):Play()
			end
			task.wait(0.55)
		end
	end)

	-- letterbox
	lbTop = make("Frame", {
		BackgroundColor3 = Color3.new(0, 0, 0), BorderSizePixel = 0,
		Size = UDim2.new(1, 0, 0, 0), Parent = gui, ZIndex = 10,
	})
	lbBottom = make("Frame", {
		BackgroundColor3 = Color3.new(0, 0, 0), BorderSizePixel = 0,
		AnchorPoint = Vector2.new(0, 1), Position = UDim2.new(0, 0, 1, 0),
		Size = UDim2.new(1, 0, 0, 0), Parent = gui, ZIndex = 10,
	})

	-- controls hint
	make("TextLabel", {
		BackgroundTransparency = 1, Position = UDim2.new(0.5, 0, 1, -22),
		AnchorPoint = Vector2.new(0.5, 0.5), Size = UDim2.new(0.9, 0, 0, 16),
		Font = Enum.Font.Garamond, TextSize = 12, TextColor3 = PAL.Washi, TextTransparency = 0.55,
		Text = "CLICK slash · R heavy · F block (time it = parry) · Q dash · SHIFT sprint · E execute · V ultimate · TAB lock-on · 1-4 stance",
		Parent = gui,
	})
end

-- ------------------------------------------------------------- behaviors

local function announce(kanji, text, dur)
	annKanji.Text = kanji or ""
	annText.Text = spaced(text or "")
	annKanji.TextTransparency = 1
	annText.TextTransparency = 1
	local inInfo = TweenInfo.new(0.35, Enum.EasingStyle.Quart, Enum.EasingDirection.Out)
	TweenService:Create(annKanji, inInfo, { TextTransparency = 0 }):Play()
	TweenService:Create(annText, inInfo, { TextTransparency = 0.1 }):Play()
	task.delay(dur or 2, function()
		local outInfo = TweenInfo.new(0.8, Enum.EasingStyle.Quad)
		TweenService:Create(annKanji, outInfo, { TextTransparency = 1 }):Play()
		TweenService:Create(annText, outInfo, { TextTransparency = 1 }):Play()
	end)
end

local function feed(text, gold)
	local entry = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 16),
		Font = Enum.Font.Garamond, Text = text, TextSize = 13,
		TextColor3 = gold and PAL.Kin or PAL.Washi, TextTransparency = 0.15,
		TextXAlignment = Enum.TextXAlignment.Left, Parent = feedFrame,
	})
	if #feedFrame:GetChildren() > 6 then
		for _, child in ipairs(feedFrame:GetChildren()) do
			if child:IsA("TextLabel") then
				child:Destroy()
				break
			end
		end
	end
	task.delay(5, function()
		TweenService:Create(entry, TweenInfo.new(1), { TextTransparency = 1 }):Play()
		task.delay(1.1, function()
			entry:Destroy()
		end)
	end)
end

local function damageNumber(position, text, color, big)
	local part = make("Part", {
		Transparency = 1, Anchored = true, CanCollide = false, CanQuery = false, CanTouch = false,
		Size = Vector3.new(0.2, 0.2, 0.2), Position = position, Parent = Workspace,
	})
	local bb = make("BillboardGui", {
		Size = UDim2.new(0, 200, 0, 50), AlwaysOnTop = true,
		StudsOffset = Vector3.new(math.random(-10, 10) / 10, 1.2, 0), Parent = part,
	})
	local lbl = make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 1, 0),
		Font = Enum.Font.Garamond, Text = text, TextSize = big and 30 or 21,
		TextColor3 = color, TextStrokeTransparency = 0.4,
		TextStrokeColor3 = Color3.new(0, 0, 0), Parent = bb,
	})
	TweenService:Create(bb, TweenInfo.new(0.9, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {
		StudsOffset = bb.StudsOffset + Vector3.new(0, 2.2, 0),
	}):Play()
	TweenService:Create(lbl, TweenInfo.new(0.9), { TextTransparency = 1, TextStrokeTransparency = 1 }):Play()
	task.delay(1, function()
		part:Destroy()
	end)
end

local function nameplate(model, displayName)
	local head = model:FindFirstChild("Head")
	if not head then return end
	local bb = make("BillboardGui", {
		Name = "TLSPlate", Size = UDim2.new(0, 150, 0, 40), MaxDistance = 110,
		StudsOffset = Vector3.new(0, 2.6, 0), Parent = head,
	})
	make("TextLabel", {
		BackgroundTransparency = 1, Size = UDim2.new(1, 0, 0, 18),
		Font = Enum.Font.Garamond, Text = displayName, TextSize = 14,
		TextColor3 = PAL.Washi, TextStrokeTransparency = 0.5, Parent = bb,
	})
	local hpHolder = make("Frame", {
		BackgroundColor3 = Color3.fromRGB(20, 18, 22), BorderSizePixel = 0,
		Position = UDim2.new(0.15, 0, 0, 22), Size = UDim2.new(0.7, 0, 0, 5), Parent = bb,
	})
	local hpBar = make("Frame", {
		BackgroundColor3 = PAL.Crimson, BorderSizePixel = 0,
		Size = UDim2.new(1, 0, 1, 0), Parent = hpHolder,
	})
	local humanoid = model:FindFirstChildOfClass("Humanoid")
	if humanoid then
		humanoid.HealthChanged:Connect(function(h)
			hpBar.Size = UDim2.new(Util.clamp(h / humanoid.MaxHealth, 0, 1), 0, 1, 0)
		end)
	end
end

-- ------------------------------------------------------------- init

local combo, comboTimer = 0, 0

function UIController.init(remotes)
	buildHUD()

	Bus.Announce:Connect(announce)
	Bus.Letterbox:Connect(function(on)
		local h = on and 0.09 or 0
		local info = TweenInfo.new(0.5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut)
		TweenService:Create(lbTop, info, { Size = UDim2.new(1, 0, h, 0) }):Play()
		TweenService:Create(lbBottom, info, { Size = UDim2.new(1, 0, h, 0) }):Play()
	end)
	Bus.ExecutePrompt:Connect(function(visible)
		execPrompt.Visible = visible
	end)

	remotes.State.OnClientEvent:Connect(function(s)
		hpFill.Size = UDim2.new(Util.clamp(s.hp / Config.Combat.MaxHP, 0, 1), 0, 1, 0)
		poFill.Size = UDim2.new(Util.clamp(s.po / Config.Combat.MaxPosture, 0, 1), 0, 1, 0)
		stFill.Size = UDim2.new(Util.clamp(s.st / Config.Combat.MaxStamina, 0, 1), 0, 1, 0)
		ultFill.Size = UDim2.new(Util.clamp(s.ult / Config.Combat.MaxUlt, 0, 1), 0, 1, 0)
		local ready = s.ult >= Config.Combat.MaxUlt
		ultKanji.TextColor3 = ready and PAL.Kin or PAL.Washi
		ultKanji.TextTransparency = ready and 0 or 0.45
		local stance = Config.Stances[s.stance]
		if stance then
			stanceKanji.Text = stance.kanji
			stanceName.Text = stance.id:upper() .. " STANCE — " .. stance.desc
		end
	end)

	remotes.Announce.OnClientEvent:Connect(function(a)
		announce(a.kanji, a.text, a.dur)
	end)

	remotes.KillFeed.OnClientEvent:Connect(function(k)
		if k.killer then
			feed(("⚔  %s %s %s"):format(k.killer, k.executed and "executed" or "cut down", k.victim), k.executed)
		else
			feed(("%s has fallen"):format(k.victim))
		end
	end)

	remotes.Actors.OnClientEvent:Connect(function(msg)
		if msg.npc and msg.model and not msg.gone then
			task.defer(function()
				if msg.model.Parent then
					nameplate(msg.model, msg.npcName or "浪人")
				end
			end)
		end
	end)

	local myId = player.UserId
	remotes.Combat.OnClientEvent:Connect(function(ev)
		if ev.e == "hit" and ev.pos then
			if ev.blocked then
				if ev.tid == myId and ev.guardBreak then
					damageNumber(ev.pos, "GUARD SHATTERED", Color3.fromRGB(255, 120, 90), true)
				end
			else
				damageNumber(ev.pos, tostring(ev.dmg), ev.backstab and Color3.fromRGB(224, 138, 90) or PAL.Washi, ev.heavy)
				if ev.backstab then
					damageNumber(ev.pos + Vector3.new(0, 1, 0), "BACKSTAB", Color3.fromRGB(224, 138, 90), false)
				end
				if ev.id == myId then
					combo = combo + 1
					comboTimer = os.clock() + 1.8
					comboCount.Text = tostring(combo)
					comboFrame.Visible = combo >= 2
				end
			end
		elseif ev.e == "parry" and ev.pos then
			damageNumber(ev.pos + Vector3.new(0, 1, 0), "弾き  PERFECT PARRY", PAL.Kin, true)
			if ev.id == myId then
				announce("弾き", "PERFECT PARRY", 0.9)
			end
		elseif ev.e == "guardbreak" and ev.pos then
			damageNumber(ev.pos + Vector3.new(0, 1.5, 0), "崩し  GUARD BROKEN", Color3.fromRGB(255, 106, 94), true)
		elseif ev.e == "execute" then
			announce("一閃", "EXECUTION", 1.6)
		elseif ev.e == "ult" then
			announce("龍斬", "DRAGON SLASH", 1.4)
		elseif ev.e == "death" and ev.id == myId then
			announce("敗北", "SLAIN — THE WIND CARRIES YOU HOME", 3)
			combo = 0
			comboFrame.Visible = false
		elseif ev.e == "evade" and ev.pos then
			damageNumber(ev.pos, "EVADED", Color3.fromRGB(159, 214, 200), false)
		end
	end)

	RunService.Heartbeat:Connect(function()
		if comboFrame.Visible and os.clock() > comboTimer then
			combo = 0
			comboFrame.Visible = false
		end
	end)

	announce("最後ノ侍", "THE LAST SAMURAI", 3.4)
	feed("the wind rises — draw your blade", true)
end

return UIController
]]></ProtectedString></Properties></Item><Item class="ModuleScript" referent="RBX26"><Properties><string name="Name">VFXController</string><ProtectedString name="Source"><![CDATA[--[[
	VFXController — grounded, readable effects in the Ghost of Tsushima key:
	white metal sparks, dust, petals, smoke. No neon anime beams.
	Uses a small pool of emitter rigs with :Emit() bursts, plus camera-follow
	ambience for rain / snow / petals / fireflies.
]]

local Workspace = game:GetService("Workspace")
local Lighting = game:GetService("Lighting")
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Shared = ReplicatedStorage:WaitForChild("Shared")
local Config = require(Shared:WaitForChild("Config"))
local Util = require(Shared:WaitForChild("Util"))
local Bus = require(script.Parent:WaitForChild("ClientBus"))

local VFXController = {}
local camera = Workspace.CurrentCamera

-- ------------------------------------------------------------- emitter pool

local POOL_SIZE = 8
local pool, poolIndex = {}, 1

local function makeEmitterRig()
	local part = Util.make("Part", {
		Name = "TLS_FX", Size = Vector3.new(0.2, 0.2, 0.2),
		Transparency = 1, Anchored = true, CanCollide = false,
		CanQuery = false, CanTouch = false, Parent = Workspace,
	})
	local function emitter(props)
		props.Enabled = false
		props.Parent = part
		return Util.make("ParticleEmitter", props)
	end
	return {
		part = part,
		sparkWhite = emitter({
			Name = "SparkWhite",
			Color = ColorSequence.new(Color3.fromRGB(255, 252, 240)),
			LightEmission = 1, Size = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 0.28), NumberSequenceKeypoint.new(1, 0.05) }),
			Transparency = NumberSequence.new(0.1),
			Lifetime = NumberRange.new(0.1, 0.3), Speed = NumberRange.new(18, 42),
			SpreadAngle = Vector2.new(180, 180), Drag = 6,
			Acceleration = Vector3.new(0, -60, 0), Rate = 0,
		}),
		sparkEmber = emitter({
			Name = "SparkEmber",
			Color = ColorSequence.new(Color3.fromRGB(255, 178, 87), Color3.fromRGB(255, 122, 60)),
			LightEmission = 0.9, Size = NumberSequence.new(0.14),
			Transparency = NumberSequence.new(0.15),
			Lifetime = NumberRange.new(0.2, 0.5), Speed = NumberRange.new(10, 26),
			SpreadAngle = Vector2.new(180, 180), Drag = 4,
			Acceleration = Vector3.new(0, -70, 0), Rate = 0,
		}),
		dust = emitter({
			Name = "Dust",
			Color = ColorSequence.new(Color3.fromRGB(174, 160, 138)),
			Size = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 1.4), NumberSequenceKeypoint.new(1, 3.4) }),
			Transparency = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 0.72), NumberSequenceKeypoint.new(1, 1) }),
			Lifetime = NumberRange.new(0.4, 0.9), Speed = NumberRange.new(3, 9),
			SpreadAngle = Vector2.new(80, 80), Drag = 2, Rate = 0,
		}),
		petals = emitter({
			Name = "PetalBurst",
			Color = ColorSequence.new(Color3.fromRGB(232, 178, 194), Color3.fromRGB(214, 148, 170)),
			Size = NumberSequence.new(0.24),
			Transparency = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 0.1), NumberSequenceKeypoint.new(1, 1) }),
			Lifetime = NumberRange.new(1.6, 3.2), Speed = NumberRange.new(8, 22),
			SpreadAngle = Vector2.new(180, 180), Drag = 3,
			Rotation = NumberRange.new(0, 360), RotSpeed = NumberRange.new(-180, 180),
			Acceleration = Vector3.new(1, -8, 0.5), Rate = 0,
		}),
		smoke = emitter({
			Name = "Smoke",
			Color = ColorSequence.new(Color3.fromRGB(120, 116, 110)),
			Size = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 1.2), NumberSequenceKeypoint.new(1, 4.6) }),
			Transparency = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 0.6), NumberSequenceKeypoint.new(1, 1) }),
			Lifetime = NumberRange.new(0.8, 1.6), Speed = NumberRange.new(2, 6),
			SpreadAngle = Vector2.new(180, 180), Rate = 0,
		}),
	}
end

local function burstAt(position, bursts)
	local rig = pool[poolIndex]
	poolIndex = poolIndex % POOL_SIZE + 1
	rig.part.Position = position
	for name, count in pairs(bursts) do
		local em = rig[name]
		if em then
			em:Emit(count)
		end
	end
end

-- ------------------------------------------------------------- screen fx

local flashCC
local function screenFlash(intensity, duration)
	flashCC.Brightness = intensity
	TweenService:Create(flashCC, TweenInfo.new(duration, Enum.EasingStyle.Quad), { Brightness = 0 }):Play()
end

local function shockRing(position, endSize, duration, color)
	local ring = Util.make("Part", {
		Shape = Enum.PartType.Ball, Size = Vector3.new(1, 1, 1),
		Color = color or Color3.fromRGB(255, 244, 220), Material = Enum.Material.Neon,
		Transparency = 0.4, Anchored = true, CanCollide = false, CanQuery = false, CanTouch = false,
		Position = position, Parent = Workspace,
	})
	TweenService:Create(ring, TweenInfo.new(duration, Enum.EasingStyle.Quart, Enum.EasingDirection.Out), {
		Size = Vector3.new(endSize, endSize * 0.4, endSize),
		Transparency = 1,
	}):Play()
	task.delay(duration + 0.1, function()
		ring:Destroy()
	end)
end

-- ------------------------------------------------------------- weather ambience

local ambient -- camera-follow rig
local function makeAmbience()
	local part = Util.make("Part", {
		Name = "TLS_Ambience", Size = Vector3.new(90, 1, 90),
		Transparency = 1, Anchored = true, CanCollide = false, CanQuery = false, CanTouch = false,
		Parent = Workspace,
	})
	local function em(props)
		props.Parent = part
		props.Enabled = false
		return Util.make("ParticleEmitter", props)
	end
	return {
		part = part,
		rain = em({
			Name = "Rain",
			Color = ColorSequence.new(Color3.fromRGB(170, 190, 215)),
			Size = NumberSequence.new(0.06),
			Transparency = NumberSequence.new(0.35),
			Lifetime = NumberRange.new(0.9, 1.1), Speed = NumberRange.new(80, 95),
			EmissionDirection = Enum.NormalId.Bottom,
			Rate = 900, Drag = 0,
		}),
		snow = em({
			Name = "Snow",
			Color = ColorSequence.new(Color3.fromRGB(238, 242, 248)),
			Size = NumberSequence.new(0.16),
			Transparency = NumberSequence.new(0.15),
			Lifetime = NumberRange.new(4, 7), Speed = NumberRange.new(6, 10),
			EmissionDirection = Enum.NormalId.Bottom,
			Rate = 220, Drag = 1,
			Acceleration = Vector3.new(2, 0, 1),
		}),
		petals = em({
			Name = "Petals",
			Color = ColorSequence.new(Color3.fromRGB(232, 178, 194)),
			Size = NumberSequence.new(0.22),
			Transparency = NumberSequence.new(0.2),
			Lifetime = NumberRange.new(5, 9), Speed = NumberRange.new(3, 7),
			EmissionDirection = Enum.NormalId.Bottom,
			Rotation = NumberRange.new(0, 360), RotSpeed = NumberRange.new(-120, 120),
			Rate = 26, Drag = 0.5,
			Acceleration = Vector3.new(3, 2, 1.4),
		}),
		fireflies = em({
			Name = "Fireflies",
			Color = ColorSequence.new(Color3.fromRGB(217, 232, 138)),
			LightEmission = 1, Size = NumberSequence.new(0.12),
			Transparency = NumberSequence.new({
				NumberSequenceKeypoint.new(0, 1), NumberSequenceKeypoint.new(0.2, 0.1),
				NumberSequenceKeypoint.new(0.8, 0.2), NumberSequenceKeypoint.new(1, 1) }),
			Lifetime = NumberRange.new(3, 6), Speed = NumberRange.new(0.5, 2),
			EmissionDirection = Enum.NormalId.Bottom,
			Rate = 6, Drag = 0,
		}),
	}
end

local function setWeather(index)
	local preset = Config.Weather[index]
	if not preset then return end
	ambient.rain.Enabled = preset.rain == true
	ambient.snow.Enabled = preset.snow == true
	ambient.petals.Enabled = preset.petals == true
	ambient.fireflies.Enabled = preset.fireflies == true
end

-- ------------------------------------------------------------- charge glow

local function setChargeGlow(actorModel, on)
	if not actorModel then return end
	local katana = actorModel:FindFirstChild("Katana")
	local hamon = katana and katana:FindFirstChild("Hamon")
	if hamon then
		hamon.Material = on and Enum.Material.Neon or Enum.Material.SmoothPlastic
		hamon.Color = on and Color3.fromRGB(255, 170, 90) or Color3.fromRGB(244, 244, 240)
	end
end

-- ------------------------------------------------------------- init

local actorModels = {}

function VFXController.init(remotes)
	for _ = 1, POOL_SIZE do
		pool[#pool + 1] = makeEmitterRig()
	end
	ambient = makeAmbience()
	flashCC = Util.make("ColorCorrectionEffect", { Name = "TLSFlash", Brightness = 0, Parent = Lighting })

	RunService.Heartbeat:Connect(function()
		ambient.part.Position = camera.CFrame.Position + Vector3.new(0, 42, 0)
	end)

	remotes.Actors.OnClientEvent:Connect(function(msg)
		if msg.gone then
			actorModels[msg.id] = nil
		else
			actorModels[msg.id] = msg.model
		end
	end)

	remotes.Weather.OnClientEvent:Connect(function(ev)
		if ev.thunder then
			screenFlash(0.5, 0.4)
			task.delay(0.12, function()
				screenFlash(0.25, 0.5)
			end)
			return
		end
		if ev.index then
			setWeather(ev.index)
		end
	end)

	remotes.Combat.OnClientEvent:Connect(function(ev)
		local e = ev.e
		local model = actorModels[ev.id]
		if e == "hit" and ev.pos then
			if ev.blocked then
				burstAt(ev.pos, { sparkWhite = 10, sparkEmber = 5 })
				Bus.Shake:Fire(0.35)
			else
				burstAt(ev.pos, { sparkWhite = 5, dust = 4 })
				Bus.Shake:Fire(ev.heavy and 0.6 or 0.4)
				if ev.heavy then
					burstAt(ev.pos - Vector3.new(0, 1.5, 0), { dust = 10 })
				end
			end
		elseif e == "parry" and ev.pos then
			burstAt(ev.pos, { sparkWhite = 34, sparkEmber = 16, smoke = 3 })
			shockRing(ev.pos, 9, 0.35)
			screenFlash(0.32, 0.22)
			Bus.Shake:Fire(0.9)
			Bus.FOVPunch:Fire(-4, 0.25)
		elseif e == "guardbreak" and ev.pos then
			burstAt(ev.pos, { sparkEmber = 20, dust = 8 })
			shockRing(ev.pos, 14, 0.5, Color3.fromRGB(255, 120, 90))
			Bus.Shake:Fire(0.8)
		elseif e == "dash" and model then
			local root = model:FindFirstChild("HumanoidRootPart")
			if root then
				burstAt(root.Position - Vector3.new(0, 2.4, 0), { dust = 8 })
			end
		elseif e == "charge" then
			setChargeGlow(model, true)
			task.delay(Config.Combat.HeavyChargeMax + 0.8, function()
				setChargeGlow(model, false)
			end)
		elseif e == "attack" then
			if ev.key == "Heavy" then
				setChargeGlow(model, false)
			end
		elseif e == "death" and ev.pos then
			burstAt(ev.pos, { petals = 40, smoke = 6, dust = 6 })
			if ev.executed then
				burstAt(ev.pos + Vector3.new(0, 2, 0), { petals = 30 })
			end
		elseif e == "execute" and ev.pos then
			screenFlash(0.28, 0.3)
			burstAt(ev.pos, { sparkWhite = 18 })
			Bus.Letterbox:Fire(true)
			task.delay(Config.Combat.ExecuteDuration + 0.4, function()
				Bus.Letterbox:Fire(false)
			end)
		elseif e == "ult" and ev.pos then
			screenFlash(0.22, 0.35)
			shockRing(ev.pos, 20, 0.6, Color3.fromRGB(255, 210, 150))
			Bus.Shake:Fire(1)
		elseif e == "evade" and ev.pos then
			burstAt(ev.pos, { dust = 3 })
		end
	end)
end

return VFXController
]]></ProtectedString></Properties></Item></Item></Item></Item><Item class="Lighting" referent="RBX29"><Properties><string name="Name">Lighting</string><token name="Technology">4</token><bool name="GlobalShadows">true</bool></Properties></Item><Item class="Workspace" referent="RBX30"><Properties><string name="Name">Workspace</string></Properties></Item><Item class="SoundService" referent="RBX31"><Properties><string name="Name">SoundService</string></Properties></Item><Item class="StarterGui" referent="RBX32"><Properties><string name="Name">StarterGui</string></Properties></Item><Item class="StarterPack" referent="RBX33"><Properties><string name="Name">StarterPack</string></Properties></Item></roblox>