Neat GUI-Based Purchase Prompters - For Clothing and UGC!

INTRODUCTION
Today, I created a cute purchase-prompter system specifically (but not limited to) individuals and groups who would like to sell their clothing and UGC products in-game! This system was solely created for the purpose of having fun and testing my limits, though I’m here to make this system completely free and easy to use for all of you!

Here’s a demonstration of what the system looks like:

~

FEATURES

  • Custom-made proximity system
  • Purchase and ownership caching (doesn’t prompt player to purchase twice)
  • Customizable configurations (animation IDs, viewport rotation speed, etc.)
  • Will function with any type of mannequin used
  • Will function with mix-match clothing (shirt and t-shirt, pants and hat, or hell - ALL IN ONE!)

~

HOW TO USE

  1. Get the model here
  2. Open Configurations and make any necessary adjustments
    image
  3. Duplicate / Add your own mannequins in the folder, and everything should function (as long as the assets are directly parented to the mannequin model)!

CRUCIAL STEP
ALL ASSETS MUST have an “ID” attribute with its respective ID found in the catalog. This is the numerical part of the URL (ex. https ://www.roblox.com/catalog/356214819, ID = 356214819).
image

~

CODE

Configurations
return {
	
	
	ANIMATION_CHANGE_FREQUENCY = 5, 			-- Changes to random animation every 5 seconds
	
	ANIMATION_IDS = {							-- ANIMATIONS PLAYED IN VIEWPORT UI
		507771019, 	-- Dance 1
		507776043, 	-- Dance 2
		507777268, 	-- Dance 3	
	},
	
	
	
	

	ACTIVATION_DISTANCE = 6,					-- Minimum distance mannequin must be from player to prompt UI

	ROTATION_INCREMENT = 100,					-- Lower number to increase viewport's rotation speed around character

	REMOVE_ACCESSORIES_ON_PREVIEW = false,		-- Removes in the viewport when player is trying on a hat

	ADD_MISSING_CLOTHES_ON_PREVIEW = false,		-- Load default missing articles of clothing when player is trying on a shirt/pants
												-- EXAMPLE: If a player is trying on shirts with no pants, their default pants are loaded

	DISPLAY_MANNEQUIN_NAME = true,				-- Shows ProductName (mannequin names) when UI appears
}
Server
--// Awesom3_Eric
--// September 2nd, 2021
--// Server Initialization



--// Services and Variables \\--
local Players = game:GetService("Players")
local StarterGui = game:GetService("StarterGui")
local RunService = game:GetService("RunService")
local StarterPlayer = game:GetService("StarterPlayer")
local PhysicsService = game:GetService("PhysicsService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")
local ServerScriptService = game:GetService("ServerScriptService")


-- Variables
local viewports = Instance.new("Folder")
viewports.Name = "Viewports"
viewports.Parent = workspace

local clones = Instance.new("Folder")
clones.Name = "Clones"
clones.Parent = ReplicatedStorage



--// Collisions \\--
PhysicsService:CreateCollisionGroup("Players")
PhysicsService:CollisionGroupSetCollidable("Players", "Players", false)

-- Set character to collision group
local function setCollision(instance)
	if instance:IsA("BasePart") then
		PhysicsService:SetPartCollisionGroup(instance, "Players")
	end
	for _, child in ipairs(instance:GetChildren()) do
		setCollision(child)
	end
	instance.ChildAdded:Connect(setCollision)
end



--// Move Instances \\--
local Configurations = script.Parent
script.Parent = ServerScriptService

Configurations.ClientHandler.Parent =StarterPlayer.StarterPlayerScripts
Configurations.AddAccessory.Parent = ReplicatedStorage
Configurations.AssetBought.Parent = ReplicatedStorage
Configurations.Prompt.Parent = StarterGui
Configurations.Parent = ReplicatedStorage
Configurations = require(Configurations)



--// CharacterAdded \\--
local function characterAdded(character)
	local player = Players:GetPlayerFromCharacter(character)
	player.CharacterAppearanceLoaded:Connect(function(fullCharacter)	
		-- Create clone
		fullCharacter.Humanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
		fullCharacter.Archivable = true
		local characterClone = fullCharacter:Clone()
		
		-- Replace old clone
		local oldClone = clones:FindFirstChild(player.Name)
		if oldClone then
			oldClone:Destroy()
		end
		fullCharacter:Clone().Parent = clones
		
		-- Replace viewport clone
		local oldViewportClone = viewports:FindFirstChild(player.Name)
		if oldViewportClone then
			oldViewportClone:Destroy()
		end
		
		-- Set up viewport (invisible in workspace)
		local viewportClone = fullCharacter:Clone()
		viewportClone.HumanoidRootPart.Anchored = true
		viewportClone.HumanoidRootPart.CFrame *= CFrame.new(0, -100, 0)
		for _, part in ipairs(viewportClone:GetDescendants()) do
			if part:IsA("BasePart") or part:IsA("Decal") then
				part.Transparency = 1
			end
		end
		viewportClone.Parent = viewports
		
		-- Play animation
		local cache = {}
		local runners = {}
		for _, animationId in ipairs(Configurations.ANIMATION_IDS) do
			local animation = Instance.new("Animation")
			animation.AnimationId = "rbxassetid://" .. tostring(animationId)
			animation.Parent = viewportClone.Humanoid
			table.insert(runners, viewportClone.Humanoid.Animator:LoadAnimation(animation))
		end
		
		-- Random animation
		local update = tick()
		local index = 1
		local changeConnection; changeConnection = RunService.Heartbeat:Connect(function()
			if player.Parent == nil then
				changeConnection:Disconnect()
				return
			else
				if tick() - update >= Configurations.ANIMATION_CHANGE_FREQUENCY then
					update = tick()
					index = index == #runners and 1 or index + 1
					for _, runner in ipairs(runners) do
						runner:Stop()
					end
					runners[index]:Play()
				end
			end
		end)
		fullCharacter.Humanoid.Died:Connect(function()
			changeConnection:Disconnect()
		end)
	end)
	setCollision(character)	
end




--// Player Initialization \\--
-- PlayerAdded
local function playerAdded(player)
	characterAdded(player.Character or player.CharacterAdded:Wait())
	player.CharacterAdded:Connect(characterAdded)
end
for _, player in ipairs(Players:GetPlayers()) do
	coroutine.wrap(playerAdded)(player)
end
Players.PlayerAdded:Connect(playerAdded)

-- PlayerRemoving
local function playerRemoving(player)
	local clone = ReplicatedStorage.Clones:FindFirstChild(player.Name)
	if clone then
		clone:Destroy()
	end
end
Players.PlayerRemoving:Connect(playerRemoving)

-- Game shutdown
game:BindToClose(function()
	for _, player in ipairs(Players:GetPlayers()) do
		coroutine.wrap(playerRemoving)(player)
	end
end)



--// Remotes \\--
ReplicatedStorage.AddAccessory.OnServerEvent:Connect(function(player, accessory)
	local viewportCharacter = viewports:FindFirstChild(player.Name)
	if viewportCharacter then
		local alreadyHas = viewportCharacter:FindFirstChild(accessory.Name)
		if not alreadyHas then
			if accessory ~= nil and accessory:IsA("Accessory") then
				local clone = accessory:Clone()
				clone.Handle.Transparency = 1
				clone.Parent = viewportCharacter
			end
		end
	end
end)



--// Purchased \\--
MarketplaceService.PromptPurchaseFinished:Connect(function(player, assetId, isPurchased)
	if isPurchased then
		ReplicatedStorage.AssetBought:FireClient(player, assetId)
	end
end)
Client
--// Awesom3_Eric
--// September 2nd, 2021
--// Client UI Handling



--// Services and Variables \\--
-- Services
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MarketplaceService = game:GetService("MarketplaceService")

-- Modules
local Maid = require(script.Maid)
local Configurations = require(ReplicatedStorage.Configurations)

-- Character
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
player.CharacterAdded:Connect(function(newCharacter)
	character = newCharacter
end)

-- UI
local Prompt = player:WaitForChild("PlayerGui"):WaitForChild("Prompt")
local main = Prompt.Main
local choices = main.Choices
local expand = main.Expand
local collapse = main.Collapse
local preview = main.Preview
local productName = main.ProductName

-- Variables
local mannequins = workspace.Mannequins
local viewports = workspace.Viewports
local clones = ReplicatedStorage.Clones

local camera = workspace.CurrentCamera
local CURRENT_MANNEQUIN = nil
local VIEWPORT_CHARACTER = nil



--// Set-Up Mannequins \\--
-- Index mannequins and position using :GetPivot()
local mannequinData = {}
for _, mannequin in ipairs(mannequins:GetChildren()) do
	mannequinData[mannequin] = mannequin:GetPivot().Position
end

-- Returns closest mannequin from character in mannequinDictionary
local function getClosestMannequin()
	local closestMannequin = nil
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if humanoidRootPart then
		local currentDistance = Configurations.ACTIVATION_DISTANCE
		for mannequin, position in pairs(mannequinData) do
			local distance = (humanoidRootPart.Position - position).Magnitude
			if distance <= currentDistance then
				currentDistance = distance
				closestMannequin = mannequin
			end
		end
	end
	return closestMannequin
end

-- Updates viewport character (called when adornee is changed)
local clothesTypes = {"ShirtGraphic", "Shirt", "Pants"}
local function updateCharacter()
	productName.Text = Configurations.DISPLAY_MANNEQUIN_NAME and CURRENT_MANNEQUIN.Name or ""
	
	-- Sets button visibility based on clothing on mannequin
	choices.BuyTShirt.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic")
	choices.BuyPants.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Pants")
	choices.BuyShirt.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Shirt")
	choices.BuyHat.Visible = CURRENT_MANNEQUIN:FindFirstChildOfClass("Accessory")
	
	-- Apply mannequin clothing to viewport character
	if VIEWPORT_CHARACTER then		
		local mannequinAccessory = CURRENT_MANNEQUIN:FindFirstChildOfClass("Accessory")
		if mannequinAccessory  then
			ReplicatedStorage.AddAccessory:FireServer(mannequinAccessory)
			for _, accessory in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
				if accessory:IsA("Accessory") then
					if accessory:GetAttribute("AddedAccessory") then
						accessory.Handle.Transparency = accessory.Name == mannequinAccessory.Name and 0 or 1
					else
						accessory.Handle.Transparency = Configurations.REMOVE_ACCESSORIES_ON_PREVIEW and 1 or 0
					end
				end
			end
		else
			for _, accessory in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
				if accessory:IsA("Accessory") then
					accessory.Handle.Transparency = accessory:GetAttribute("AddedAccessory") and 1 or 0
				end
			end
		end
		
		-- Replicate Clothing
		for _, cloth in ipairs(VIEWPORT_CHARACTER:GetChildren()) do
			if cloth:IsA("Clothing") or cloth:IsA("ShirtGraphic") then
				cloth:Destroy()
			end
		end
		
		local noClothes = not CURRENT_MANNEQUIN:FindFirstChildOfClass("Shirt") and not CURRENT_MANNEQUIN:FindFirstChildOfClass("Pants")
	
		local characterClone = ReplicatedStorage.Clones[player.Name]
		if noClothes then
			for _, defaultCloth in ipairs(characterClone:GetChildren()) do
				if defaultCloth:IsA("Clothing") or defaultCloth:IsA("ShirtGraphic") then
					defaultCloth:Clone().Parent = VIEWPORT_CHARACTER
				end
			end
			if CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic") then
				CURRENT_MANNEQUIN:FindFirstChildOfClass("ShirtGraphic"):Clone().Parent = VIEWPORT_CHARACTER
			end
		else
			for _, cloth in ipairs(CURRENT_MANNEQUIN:GetChildren()) do
				if cloth:IsA("Clothing") or cloth:IsA("ShirtGraphic") then
					cloth:Clone().Parent = VIEWPORT_CHARACTER
				end
			end
			if Configurations.ADD_MISSING_CLOTHES_ON_PREVIEW then
				for _, cloth in ipairs(clothesTypes) do
					if not VIEWPORT_CHARACTER:FindFirstChildOfClass(cloth) then
						local defaultCloth = characterClone:FindFirstChildOfClass(cloth)
						if defaultCloth then
							defaultCloth:Clone().Parent = VIEWPORT_CHARACTER
						end
					end
				end
			end
		end
	end
end

-- RunService updates adornee with closest mannequin
local update = tick()
RunService.Heartbeat:Connect(function()
	if tick() - update > 0.1 then
		update = tick()
		local closestMannequin = getClosestMannequin()
		if closestMannequin then
			local node = closestMannequin:FindFirstChild("Node")
			if not node then
				node = Instance.new("Part")
				node.Anchored = true
				node.Transparency = 1
				node.CanCollide = false
				node.Size = Vector3.new(0, 0, 0)
				node.Position = mannequinData[closestMannequin]
				node.Name = "Node"
				node.Parent = closestMannequin
			end
			Prompt.Adornee = node
			Prompt.Enabled = true

			if closestMannequin ~= CURRENT_MANNEQUIN then
				CURRENT_MANNEQUIN = closestMannequin
				main.Size = UDim2.new(0, 0, 0, 0)
				main:TweenSize(UDim2.new(1, 0, 1, 0), "Out", "Quad", 0.2, true)
				updateCharacter()
			end
		else
			Prompt.Adornee = nil
			Prompt.Enabled = false
		end
	end
end)



--// Update Part CFrames of Viewport Character \\--
local updater = Maid.new()
local function updateViewport()
	updater:DoCleaning()
	
	-- Create new camera instance
	local newCamera = Instance.new("Camera")
	newCamera.Parent = preview.Viewport
	preview.Viewport.CurrentCamera = newCamera
	updater:GiveTask(newCamera)
	
	-- Clone character into viewport frame
	local viewportCharacter = viewports[player.Name]
	local newClone = clones[player.Name]:Clone()
	newClone.Parent = preview.Viewport
	VIEWPORT_CHARACTER = newClone
	updater:GiveTask(newClone)
	
	-- Rotate camera around HumanoidRootPart
	local humanoidRootPart = newClone:WaitForChild("HumanoidRootPart")
	local rotationalIndex = 1
	updater:GiveTask(RunService.Heartbeat:Connect(function()
		rotationalIndex += 1
		newCamera.CFrame = humanoidRootPart.CFrame * CFrame.Angles(0, rotationalIndex * (math.pi/Configurations.ROTATION_INCREMENT), 0) * CFrame.new(0, -0.25, 6)
	end))
	
	-- Index limbs/accessories and match with viewport character in workspace
	local partData = {}
	for _, part in ipairs(newClone:GetChildren()) do
		if part:IsA("BasePart") then
			partData[part] = viewportCharacter:WaitForChild(part.Name)
		elseif part:IsA("Accessory") and part.Parent then
			partData[part:WaitForChild("Handle")] = viewportCharacter:WaitForChild(part.Name):WaitForChild("Handle")
		end
	end
	
	-- Update CFrames of baseparts to match animation
	updater:GiveTask(RunService.RenderStepped:Connect(function()
		for part, target in pairs(partData) do
			part.CFrame = target.CFrame
		end
	end))
	
	-- Adds new accessory to viewport
	updater:GiveTask(viewportCharacter.ChildAdded:Connect(function(accessory)
		if accessory:IsA("Accessory") then
			accessory:WaitForChild("Handle")
			local clone = accessory:Clone()
			clone:SetAttribute("AddedAccessory", true)
			clone.Handle.Transparency = 0
			clone.Parent = newClone
			updater:GiveTask(clone)
			partData[accessory.Handle] = clone.Handle
		end
	end))
end

-- Initialize
coroutine.wrap(function()
	viewports:WaitForChild(player.Name); updateViewport()
	viewports.ChildAdded:Connect(function(child)
		if child.Name == player.Name then
			updateViewport()
		end
	end)
end)()



--// Choice Functions \\--
local ownedCache = {}
local function isOwned(id)
	if not ownedCache[id] then
		local success, owned = pcall(MarketplaceService.PlayerOwnsAsset, MarketplaceService, player, id)
		ownedCache[id] = success and owned or false
	end
	return ownedCache[id]
end
ReplicatedStorage.AssetBought.OnClientEvent:Connect(function(id)
	ownedCache[id] = true
end)

local function promptPurchase(button, class)
	local id = CURRENT_MANNEQUIN:FindFirstChildOfClass(class):GetAttribute("ID")
	if id then
		if not isOwned(id) then
			MarketplaceService:PromptPurchase(player, id)
		else
			print("NOPE")
			button.Label.Text = "OWNED"
			delay(0.5, function()
				button.Label.Text = button:GetAttribute("OriginalText")
			end)
		end
	end
end
choices.BuyShirt.Detection.MouseButton1Click:Connect(function()
	promptPurchase(choices.BuyShirt, "Shirt")
end)
choices.BuyPants.Detection.MouseButton1Click:Connect(function()
	promptPurchase(choices.BuyPants, "Pants")
end)
choices.BuyTShirt.Detection.MouseButton1Click:Connect(function()
	promptPurchase(choices.BuyTShirt, "ShirtGraphic")
end)
choices.BuyHat.Detection.MouseButton1Click:Connect(function()
	promptPurchase(choices.BuyHat, "Accessory")
end)



--// Main Button Functions \\--
-- Opens/Closes choices frame
local buttonDebounce = false
local function toggleChoices(toggled)
	if toggled then
		choices.Visible = true
		choices.Size = UDim2.new(0, 0, 0, 0)
		choices:TweenSize(UDim2.new(1, 0, 1, 0), "Out", "Quad", 0.1, true)
		main.Collapse.Visible = true
		main.Collapse.Size = UDim2.new(0, 0, 0, 0)
		main.Collapse:TweenSize(UDim2.new(0.15, 0, 0.15, 0), "Out", "Quad", 0.1, true)
	else
		choices:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
		main.Collapse:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
		delay(0.1, function()
			choices.Visible = false
			main.Collapse.Visible = false
		end)
	end
end

-- Close preview and opens choices
preview.Close.Detection.MouseButton1Click:Connect(function()
	if not buttonDebounce then
		buttonDebounce = true
		toggleChoices(true)
		preview:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
		delay(0.1, function()
			preview.Visible = false
			buttonDebounce = false
		end)
	end
end)

-- Opens preview and closes choices
choices.Preview.Detection.MouseButton1Click:Connect(function()
	if not buttonDebounce then
		buttonDebounce = true
		preview.Visible = true
		preview.Size = UDim2.new(0, 0, 0, 0)
		preview:TweenSize(UDim2.new(0.85, 0, 0.85, 0), "Out", "Quad", 0.1, true)

		toggleChoices(false)
		task.wait(0.1)
		buttonDebounce = false
	end
end)

-- Opens choices and closes expand button and product name
main.Expand.Detection.MouseButton1Click:Connect(function()
	if not buttonDebounce then
		buttonDebounce = true
		toggleChoices(true)
		expand.Visible = false
		productName:TweenSize(UDim2.new(0, 0, 0, 0), "Out", "Quad", 0.1, true)
		delay(0.1, function()
			buttonDebounce = false
			main.ProductName.Visible = false
		end)
	end
end)

-- Closes choices and opens expand button and product name
main.Collapse.Detection.MouseButton1Click:Connect(function()
	if not buttonDebounce then
		buttonDebounce = true
		expand.Visible = true
		productName.Visible = true
		productName:TweenSize(UDim2.new(2, 0, 0.15, 0), "Out", "Quad", 0.1, true)
		toggleChoices(false)
		delay(0.1, function()
			buttonDebounce = false
			choices.Visible = false
		end)
	end
end)



--// Button Effects \\--
local function createButton(frame)
	local originalSize = frame.Size
	local expandSize = UDim2.fromScale(originalSize.X.Scale * 1.1, originalSize.Y.Scale * 1.1)
	local shrinkSize = UDim2.fromScale(originalSize.X.Scale * 0.9, originalSize.Y.Scale * 0.9)
	local detection = frame.Detection
	detection.MouseEnter:Connect(function()
		frame:TweenSize(expandSize, "Out", "Quad", 0.1, true)
	end)
	detection.MouseLeave:Connect(function()
		frame:TweenSize(originalSize, "Out", "Quad", 0.1, true)
	end)
	detection.MouseButton1Down:Connect(function()
		frame:TweenSize(shrinkSize, "Out", "Quad", 0.1, true)
	end)
	detection.MouseButton1Up:Connect(function()
		frame:TweenSize(expandSize, "Out", "Quad", 0.1, true)
	end)
end

-- Initialize button effects on buttons
for _, choice in ipairs(choices:GetChildren()) do
	if choice:IsA("Frame") then
		choice:SetAttribute("OriginalText", choice.Label.Text)
		createButton(choice)
	end
end
createButton(preview.Close)
createButton(expand)
createButton(collapse)

~

CLOSING
This system probably will NOT be updated, though if there are considerable critiques and suggestions, as well as critical bugs in the system, I most definitely will edit them. Let me know if you have any questions and I will assist you :slight_smile: Enjoy!

137 Likes

Wow, this is actually pretty neat and unique. I feel like this has huge potential, and many clothing groups will surely love it. Thank you.

9 Likes

This Will Improve Most Clothing Store Games. Since You Can See The Clothing Before You Purchase It In A Better Way.

7 Likes

This is amazing! So kind of you to create this and share it with us! I’ll for sure be incorporating it into my games.

4 Likes

Could I see the UGC video version example please?

4 Likes

You are a hero! This will help a lot, legit thank you much!!!

3 Likes

You just saved so many homestore builders their time :slight_smile: Good Job

4 Likes

The system will function automatically by loading the UGC accessory and parenting it to the mannequin (and setting the ID attribute, of course). The kit has a mannequin with a hat already in place, you can use that as an example, or you can watch this video here:

6 Likes

this looks extremely helpful but i got a question, i have mannequins in my store that have r6 bodies, will the different body type effect it at all?

5 Likes

I honestly do not think the body type should affect it at all! The system supports all types of mannequins, even ones that don’t look like mannequins. If your game is solely an R6 game, it may affect the animations that the viewport is currently playing, but you can easily switch the animation IDs in the configurations module. Other than that, the functionality should be the same :slight_smile:

1 Like

This is an awesome system but I’ve ran into a few pretty bad bugs.

  1. Avatar scaling seems to mess up the viewport:

image

  1. After a while, it seems the game performance degrades heavily. Not sure if it’s this system specifically causing it, but ever since adding it to my game today, the longer the server is open, the more the FPS tanks for everyone playing. I’ve removed it for now but I’ll edit this if that’s not the cause of the issue.
3 Likes

You’re right, the scaling of the avatar does heavily influence how it appears on the viewport. I’ll definitely give that a look-over and see if I can come up with a solution for that. On the other hand, the UI is pretty much 100% local other than the fact that the animations are running on the server and the client is just replicating the positions of each limb based on that. I’ll give that a look over too to see if that is the cause of lag. I appreciate this feedback! :slight_smile:

2 Likes

From re-evaluating my scripts, I noticed that I did NOT destroy the character clones when they leave the game, which may have caused performance issues

As for the viewport frame, until I find a better solution for fitting the character model precisely within the given size of the viewport, I have placed the camera a bit further away so that the viewport is compatible with various avatar scales.

The system has been updated :slight_smile:

6 Likes

Awesome, I’ll give it a shot and keep you updated!

2 Likes

That’s one of the videos I used a couple of months ago for a solution

5 Likes

AYO, it’s all trig math, my favorite! and it’s by the one and only sleitnick, even better!

Thanks so much, I’ll definitely give this a look and see if I can incorporate the math into my works :slight_smile:

2 Likes

A try on would be very useful with this even though preview is there people tend to try things on :slight_smile:

1 Like

I’ve decided to bring this back to life, in the name of power! :evil:

Release

1 Like

There is no model link on your release… in the name of power! where is the link?