SS13 = require("SS13")
local admin = "homek"
local adminUser = dm.global_vars:get_var("GLOB"):get_var("directory"):get(admin)
local SCROLL_SPAWN_AMOUNT = 1
local MODE_RUNITE = 1
local MODE_GRIND = 2
local MODE_CLICKER = 3
local MODE_PRECISION = 4
local MODE = MODE_RUNITE
local NON_GRIND_SPEED_BOOST = 1
local TIER_CAP = 5
local CRAFTABLE_TIER_CAP = 15
local CRAFTABLE_QUALITY_REQ_MULT = 0
local FANTASY_TIER_PER_QUALITY = 8
local QUALITY_CAP
if TIER_CAP then
QUALITY_CAP = TIER_CAP * FANTASY_TIER_PER_QUALITY
end
local CRAFTABLE_QUALITY_CAP = CRAFTABLE_TIER_CAP * FANTASY_TIER_PER_QUALITY
local UPPER_REFINEMENT_LIMIT = (2 ^ 1023) * 1.999
local function notifyPlayer(ply, msg)
ply:call_proc("balloon_alert", ply, msg)
end
local function getXpReq(data)
if MODE == MODE_GRIND then
return 10 * 2 ^ (math.ceil((data.level + 1) / 10) - 1)
else
return 100
end
end
local function replaceBadChars(str)
local result = string.gsub(str, "([^ -~]+)", "")
return result
end
local function norm1000()
local x
repeat
x = math.ceil(math.log(1/math.random())^.5*math.cos(math.pi*math.random())*150+500)
until x >= 1 and x <= 1000
return x
end
local function hsvToRgb(h, s, v, a)
h = math.abs((h % 360) / 360)
local r, g, b
local i = math.floor(h * 6)
local f = h * 6 - i
local p = v * (1 - s)
local q = v * (1 - f * s)
local t = v * (1 - (1 - f) * s)
i = i % 6
if i == 0 then
r, g, b = v, t, p
elseif i == 1 then
r, g, b = q, v, p
elseif i == 2 then
r, g, b = p, v, t
elseif i == 3 then
r, g, b = p, q, v
elseif i == 4 then
r, g, b = t, p, v
elseif i == 5 then
r, g, b = v, p, q
end
return { r * 255, g * 255, b * 255, a * 255 }
end
local function rgbToHex(rgb)
local hexadecimal = "#"
for _, value in pairs(rgb) do
local hex = ""
while value > 0 do
local index = math.fmod(value, 16) + 1
value = math.floor(value / 16)
hex = string.sub("0123456789ABCDEF", index, index) .. hex
end
if string.len(hex) == 0 then
hex = "00"
elseif string.len(hex) == 1 then
hex = "0" .. hex
end
hexadecimal = hexadecimal .. hex
end
return hexadecimal
end
refiningSound = refiningSound or {}
anvilIcon = anvilIcon or nil
if #refiningSound == 0 or anvilIcon == nil then
refiningSound = {}
local refiningSoundUrl = {
"https://raw.githubusercontent.com/tgstation/auxlua-cookbook/main/waltermeldron/assets/blacksmith/blacksmith.ogg",
"https://raw.githubusercontent.com/tgstation/auxlua-cookbook/main/waltermeldron/assets/blacksmith/blacksmith2.ogg",
"https://raw.githubusercontent.com/tgstation/auxlua-cookbook/main/waltermeldron/assets/blacksmith/blacksmith3.ogg"
}
for _, url in refiningSoundUrl do
local request = SS13.new("/datum/http_request")
local file_name = "tmp/custom_map_sound.ogg"
request:call_proc("prepare", "get", url, "", "", file_name)
request:call_proc("begin_async")
while request:call_proc("is_complete") == 0 do
sleep()
end
table.insert(refiningSound, SS13.new("/sound", file_name))
end
do
local request = SS13.new("/datum/http_request")
local file_name = "tmp/custom_map_icon.dmi"
request:call_proc("prepare", "get", "https://raw.githubusercontent.com/tgstation/auxlua-cookbook/main/waltermeldron/assets/blacksmith/anvil.dmi", "", "", file_name)
request:call_proc("begin_async")
while request:call_proc("is_complete") == 0 do
sleep()
end
anvilIcon = SS13.new("/icon", file_name)
end
end
local function pairsByValue (t, f)
local a = {}
local valueKey = {}
for n, value in pairs(t) do
table.insert(a, value)
valueKey[value] = n
end
table.sort(a, f)
local i = 0
local iter = function ()
i = i + 1
if a[i] == nil then return nil
else return valueKey[a[i]], a[i]
end
end
return iter
end
local anyItemInTheGame = SS13.new("/obj/structure/mystery_box")
sleep()
local craftable = {
["Clothing"] = {
minimumLevel = 50,
materialsRequired = 10,
typepath = "/obj/item/clothing",
blacklist = {
"/obj/item/clothing/mask/facehugger"
},
materialUnits = 5,
},
["Melee"] = {
minimumLevel = 100,
materialsRequired = 20,
typepath = "/obj/item/melee",
blacklist = {
"/obj/item/melee/supermatter_sword",
"/obj/item/melee/energy/axe"
},
materialUnits = 15,
},
["Gun"] = {
minimumLevel = 150,
materialsRequired = 20,
typepath = "/obj/item/gun",
blacklist = {
"/obj/item/gun/magic",
"/obj/item/gun/energy/pulse",
"/obj/item/gun/energy/laser/instakill",
"/obj/item/gun/energy/meteorgun"
},
materialUnits = 15,
},
["Ammo"] = {
minimumLevel = 150,
materialsRequired = 10,
typepath = "/obj/item/ammo_box",
materialUnits = 5,
},
["Grenade"] = {
minimumLevel = 200,
materialsRequired = 30,
typepath = "/obj/item/grenade",
materialUnits = 10,
},
["Magic Tool"] = {
minimumLevel = 300,
materialsRequired = 30,
typepath = "/obj/item/gun/magic",
blacklist = {
"/obj/item/gun/magic/wand/death/debug",
"/obj/item/gun/magic/wand/resurrection/debug",
"/obj/item/gun/magic/wand/safety/debug"
},
materialUnits = 10,
},
["Any"] = {
minimumLevel = 500,
materialsRequired = 15,
typepath = "/obj/item",
blacklist = {
"/obj/item/paper",
"/obj/item/circuit_component"
},
materialUnits = 10,
}
}
for _, craftData in craftable do
craftData.typepath = dm.global_proc("_text2path", craftData.typepath)
if craftData.blacklist then
for index, blacklistType in craftData.blacklist do
craftData.blacklist[index] = dm.global_proc("_text2path", blacklistType)
end
end
end
for index, typepath in anyItemInTheGame:get_var("valid_types") do
if over_exec_usage(0.7) then
sleep()
end
for _, craftData in craftable do
if over_exec_usage(0.7) then
sleep()
end
craftData.items = craftData.items or {}
if dm.global_proc("_ispath", typepath, craftData.typepath) == 1 then
local isBlacklisted = false
if craftData.blacklist then
for _, blacklistType in craftData.blacklist do
if dm.global_proc("_ispath", typepath, blacklistType) == 1 then
isBlacklisted = true
break
end
end
end
if not isBlacklisted then
table.insert(craftData.items, typepath)
end
end
end
end
SS13.qdel(anyItemInTheGame)
local specialRefining = {
["/obj/item/stack/sheet/plasteel"] = {
noAnvilRequired = true,
hitsRequiredToFinish = 3,
onHitComplete = function(data, user)
local plasteel = data.item
local location = plasteel:get_var("loc")
if not SS13.istype(location, "/turf") then
return false
end
for _, object in location:get_var("contents") do
if object:get_var("density") == 1 then
notifyPlayer(user, "not enough room here!")
return false
end
end
if plasteel:get_var("amount") >= 50 then
local anvil = SS13.new("/obj/structure/table/reinforced", location)
anvil:set_var("icon", anvilIcon)
anvil:set_var("icon_state", "anvil")
anvil:set_var("canSmoothWith", {})
anvil:set_var("smoothing_groups", {})
anvil:set_var("smoothing_flags", 0)
anvil:set_var("name", "anvil")
anvil:set_var("desc", "Although it looks pretty weird, this is definitely an anvil. Used by blacksmiths to improve gear")
dm.global_proc("_add_trait", anvil, "anvil", "blacksmith")
return true
else
notifyPlayer(user, "need a stack of 50 plasteel")
end
return false
end
},
["/obj/item/stack/sheet/plastic"] = {
onRefineStart = function(humanData, data)
notifyPlayer(humanData.human, "too flimsy to work with!")
return false
end
},
["/obj/item/stack/sheet"] = {
onRefineStart = function(humanData, data)
local item = data.item
local craftableNames = {}
local nameMapping = {}
for name, craftData in pairsByValue(craftable, function(a, b) return a.minimumLevel < b.minimumLevel end) do
if craftData.craftableAmount then
humanData.crafted[craftData] = humanData.crafted[craftData] or 0
if humanData.crafted[craftData] >= craftData.craftableAmount then
continue
end
end
local newName = name .. " (level "..tostring(craftData.minimumLevel).." required)"
table.insert(craftableNames, newName)
nameMapping[newName] = craftData
end
local response = SS13.await(SS13.global_proc, "tgui_input_list", humanData.human, "Select what to craft", "Crafting selection", craftableNames)
if response == nil or item:is_null() or humanData.human:is_null() then
return false
end
local craftingData = nameMapping[response]
if humanData.level < craftingData.minimumLevel then
notifyPlayer(humanData.human, "level not high enough!")
return false
end
if item:get_var("amount") < craftingData.materialsRequired then
notifyPlayer(humanData.human, "not enough materials!")
return false
end
data.qualityHardCap = math.max(data.qualityHardCap, CRAFTABLE_QUALITY_CAP)
data.crafting = craftingData
notifyPlayer(humanData.human, "you ready your hammer")
return true
end,
getQuality = function(data, expectedQuality)
if data.crafting then
return math.max(expectedQuality - (data.crafting.minimumLevel / 10) * CRAFTABLE_QUALITY_REQ_MULT, 0)
end
return expectedQuality
end,
onRefineComplete = function(humanData, data, quality, oldLocation)
if data.crafting then
local materials = data.item
if materials:get_var("amount") < data.crafting.materialsRequired or quality <= 0 then
data.failed = true
return
end
humanData.crafted[data.crafting] = humanData.crafted[data.crafting] or 0
humanData.crafted[data.crafting] += 1
local possibleItems = data.crafting.items
local toSpawn = possibleItems[math.random(#possibleItems)]
dm.global_proc("_remove_trait", materials, "being_refined", "blacksmith")
data.item = SS13.new(toSpawn, oldLocation)
data.item:set_var("material_flags", 7)
local materialComp = {}
local custom_materials = materials:get_var("mats_per_unit")
if custom_materials then
for mat, amount in custom_materials do
materialComp[mat] = amount * (data.crafting.materialUnits or data.crafting.materialsRequired)
end
end
data.item:call_proc("set_custom_materials",materialComp)
data.item:call_proc("visible_message", "The "..replaceBadChars(tostring(data.item)).." is formed from "..tostring(materials).."")
materials:call_proc("use", data.crafting.materialsRequired)
end
end
}
}
local classNames = {
[0] = "Amateur",
[50] = "Novice",
[100] = "Capable",
[150] = "Competent",
[200] = "Skilled",
[225] = "Expert",
[250] = "Master",
[300] = "Grandmaster",
[400] = "Legendary",
[500] = "Celestial",
[750] = "God",
[1000] = "Admin"
}
local magnitudes = {
[0] = "",
[2] = "million",
[3] = "billion",
[4] = "trillion",
[5] = "quadrillion",
[6] = "quintillion",
[7] = "sextillion",
[8] = "septillion",
[9] = "octillion",
[10] = "nonillion",
[11] = "decillion",
[12] = "undecillion",
[13] = "duodecillion",
[14] = "tredecillion",
[15] = "quattuordecillion",
[16] = "quindecillion",
[17] = "sexdecillion",
[18] = "septendecillion",
[19] = "octodecillion",
[20] = "novemdecillion",
[21] = "vigintillion",
[22] = "unvigintillion",
[23] = "duovigintillion",
[24] = "tresvigintillion",
[25] = "quattuorvigintillion",
[26] = "quinvigintillion",
[27] = "sesvigintillion",
[28] = "septemvigintillion",
[29] = "octovigintillion",
[30] = "novemvigintillion",
[31] = "trigintillion",
[32] = "untrigintillion",
[33] = "duotrigintillion",
[34] = "trestrigintillion",
[35] = "quattuortrigintillion",
[36] = "quintrigintillion",
[37] = "sestrigintillion",
[38] = "septentrigintillion",
[39] = "adminizillion",
[40] = "watermillion",
[50] = "googleplex"
}
local function magnitudeToString(experience)
local magnitude = math.floor(math.max(math.log(math.max(experience, 1)) / math.log(10), 0) / 3)
experience = (experience / (10 ^ (magnitude * 3)))
if experience == 0 then
experience = 0
end
local magnitudeString = magnitudes[magnitude]
local experienceRounded = math.floor(experience * 100) / 100
if magnitudeString then
if #magnitudeString == 0 then
return tostring(experienceRounded)
end
return tostring(experienceRounded).." "..magnitudeString
end
local iteration = 1
local magnitudePosition = magnitude
while iteration < 10 do
magnitudePosition -= 1
magnitudeString = magnitudes[magnitudePosition]
if magnitudeString then
break
end
iteration += 1
end
if magnitudeString then
experienceRounded = math.floor(experience * (10 ^ (3 * iteration)) * 100) / 100
if #magnitudeString == 0 then
return tostring(experienceRounded)
end
return tostring(experienceRounded).." "..magnitudeString
end
return tostring(experienceRounded).." E+"..tostring(magnitude * 3)
end
local function updateVisualData(data)
data.image:set_var(
"maptext",
string.format(
"Level: %d
Experience %s/%s
%s
",
data.level,
magnitudeToString(data.exp),
magnitudeToString(getXpReq(data)),
data.class
)
)
end
local function addExp(data, exp, cause)
data.exp += math.floor(exp)
local originalLevel = data.level
local iterations = 0
while data.exp >= getXpReq(data) do
if iterations >= 100 then
break
end
data.exp = data.exp - getXpReq(data)
data.level = data.level + 1
iterations += 1
end
local highestSatisfiedReq = 0
for levelReq, class in classNames do
if levelReq > highestSatisfiedReq and levelReq <= data.level then
data.class = class .. " Blacksmith"
highestSatisfiedReq = levelReq
end
end
if cause ~= nil then
notifyPlayer(data.human, string.format("%s (%s xp)", cause, magnitudeToString(math.floor(exp))))
end
if originalLevel ~= data.level then
local turf = dm.global_proc("_get_step", data.human, 0)
notifyPlayer(data.human, "level up!")
end
updateVisualData(data)
if data.exp >= getXpReq(data) then
SS13.set_timeout(0.1, function()
addExp(data, 0, nil)
end)
end
end
local function REF(obj)
return dm.global_proc("REF", obj)
end
local function determineQuality(refiningData, skipCustom)
local quality = math.ceil(math.log((math.max(refiningData.refinedAmount, 0) + 501) / 500) / math.log(2))
if not skipCustom then
if refiningData.specialData.getQuality then
quality = refiningData.specialData.getQuality(refiningData, quality)
end
quality = math.min(quality, refiningData.qualityHardCap)
end
return quality
end
local function getQualityRange(quality)
local quality = quality
local lowerBound = 500 * (2 ^ (quality - 1)) - 1
local upperBound = 500 * (2 ^ quality) - 1
return { lowerBound = lowerBound, upperBound = upperBound, range = upperBound - lowerBound }
end
local function setupHuman(human)
local humanData = {
human = human,
exp = 0,
level = 1,
class = classNames[0].. " Blacksmith",
image = SS13.new("/atom/movable/screen/text"),
unallocatedPoints = 0,
crafted = {}
}
humanData.image:set_var("screen_loc", "WEST:4,CENTER-0:17")
local hud = human:get_var("hud_used")
local hudElements = hud:get_var("static_inventory")
hudElements:add(humanData.image)
hud:call_proc("show_hud", hud:get_var("hud_version"))
updateVisualData(humanData)
local hammer = SS13.new("/obj/item/nullrod/hammer", human:call_proc("drop_location"))
hammer:set_var("name", "smithing hammer")
hammer:set_var("desc", "A blacksmith's trusty hammer. Used to smith weapons.")
dm.global_proc("qdel", hammer:call_proc("GetComponent", dm.global_proc("_text2path", "/datum/component/anti_magic")))
human:call_proc("put_in_hands", hammer)
local function finishRefining(refiningData, oldLoc)
local refinedAmountMultiplier = (norm1000() / 1000) + 0.5
local refinedAmount = refiningData.refinedAmount * refinedAmountMultiplier
local quality = determineQuality(refiningData)
if refiningData.maximum_quality then
quality = math.min(refiningData.maximum_quality, quality)
end
local expectedTier = math.floor(quality / FANTASY_TIER_PER_QUALITY)
if refiningData.specialData.onRefineComplete then
refiningData.specialData.onRefineComplete(humanData, refiningData, expectedTier, oldLoc)
end
if refiningData.failed then
dm.global_proc("playsound", refiningData.item:call_proc("drop_location"), "sound/items/knell"..math.random(1, 4)..".ogg", 30, true)
refiningData.item:call_proc("visible_message", "The "..replaceBadChars(tostring(refiningData.item)).." falls apart as the refinement is completed.")
refiningData.item:call_proc("burn")
return
end
refiningData.refined = true
if expectedTier ~= 0 then
refiningData.item:call_proc("_AddComponent", { dm.global_proc("_text2path", "/datum/component/fantasy"), expectedTier, nil, false, true })
end
dm.global_proc("_add_trait", refiningData.item, "blacksmith_refined", "blacksmith")
dm.global_proc("_remove_trait", refiningData.item, "being_refined", "blacksmith")
dm.global_proc("playsound", refiningData.item:call_proc("drop_location"), "sound/effects/coin2.ogg", 30, true)
local uncappedQuality = determineQuality(refiningData, true)
if MODE == MODE_GRIND then
addExp(humanData, 10 * 2 ^ (uncappedQuality), "created a quality "..tostring(quality).." item")
end
local ghostSound = nil
if expectedTier >= 10 then
ghostSound = "sound/effects/coin2.ogg"
end
notifyText = tostring(human).." has crafted a "..replaceBadChars(tostring(refiningData.item))
dm.global_proc("notify_ghosts", notifyText, refiningData.item, notifyText, nil, false, "", ghostSound)
end
local refiningProgress = {}
local nextAttack = 0
local isOpen = false
SS13.register_signal(hammer, "item_pre_attack_secondary", function(_, target, user)
local time = dm.world:get_var("time")
if user ~= human or not SS13.istype(target, "/obj/item") or user:get_var("combat_mode") == 1 then
return
end
if dm.global_proc("_has_trait", target, "blacksmith_refined") == 1 then
notifyPlayer(user, "already refined!")
return 1
end
if isOpen then
return 1
end
SS13.set_timeout(0, function()
local targetRef = REF(target)
local itemProgress = refiningProgress[targetRef]
if not itemProgress or itemProgress.item:is_null() or itemProgress.item ~= target then
return
end
isOpen = true
local input = SS13.await(SS13.global_proc, "tgui_input_number", user, "Set maximum quality level", "Set maximum quality level", 1000, 10000000000, -99)
isOpen = false
if input == nil then
return
end
itemProgress.maximum_quality = input
end)
return 1
end)
SS13.register_signal(hammer, "parent_qdeleting", function()
if not hud:is_null() and not human:is_null() then
hudElements:remove(humanData.image)
hud:call_proc("show_hud", hud:get_var("hud_version"))
SS13.unregister_signal(human, "atom_examine")
SS13.unregister_signal(human, "ctrl_click")
dm.global_proc("_remove_trait", human, "blacksmith", "blacksmith")
end
end)
local isOpen = false
SS13.register_signal(hammer, "item_pre_attack", function(_, target, user)
local time = dm.world:get_var("time")
if user ~= human or not SS13.istype(target, "/obj/item") or user:get_var("combat_mode") == 1 then
return
end
if nextAttack > time or isOpen then
return 1
end
if dm.global_proc("_has_trait", target, "blacksmith_refined") == 1 then
notifyPlayer(user, "already refined!")
return 1
end
if MODE ~= MODE_CLICKER then
nextAttack = time + 10
end
local specialRefiningData = {}
local pathLength = 0
for typepath, data in specialRefining do
if SS13.istype(target, typepath) then
if pathLength < #typepath then
specialRefiningData = data
pathLength = #typepath
end
end
end
if not specialRefiningData.noAnvilRequired then
local location = target:get_var("loc")
if not SS13.istype(location, "/turf") then
notifyPlayer(user, "need to do this on an anvil!")
return 1
end
local anvilExists = false
for _, object in location:get_var("contents") do
if dm.global_proc("_has_trait", object, "anvil") == 1 then
anvilExists = true
break
end
end
if not anvilExists then
notifyPlayer(user, "need to do this on an anvil!")
return 1
end
end
SS13.set_timeout(0, function()
local targetRef = REF(target)
local itemProgress = refiningProgress[targetRef]
local shouldStart = true
if not itemProgress or itemProgress.item:is_null() or itemProgress.item ~= target then
if dm.global_proc("_has_trait", target, "being_refined") == 1 then
notifyPlayer(user, "you didn't begin this refinement process!")
return
end
dm.global_proc("_add_trait", target, "being_refined", "blacksmith")
itemProgress = {
refinedAmount = 0,
hitAmount = 0,
item = target,
diminishing = 0,
specialData = specialRefiningData,
completionImage = SS13.new("/image", nil, target),
qualityHardCap = QUALITY_CAP or 10000,
name = target:get_var("name")
}
if specialRefiningData.onRefineStart then
isOpen = true
local result = specialRefiningData.onRefineStart(humanData, itemProgress)
isOpen = false
if not result then
dm.global_proc("_remove_trait", target, "being_refined", "blacksmith")
return
end
shouldStart = false
end
itemProgress.completionImage:set_var("appearance_flags", 74)
local function updateCompletionImage()
local expectedQuality = determineQuality(itemProgress, true)
local qualityAmount = 8
local maxQuality = 360 / 5
local currentQualityDisplay = math.floor(expectedQuality / qualityAmount) * qualityAmount
local lowerBound = math.log(math.max(getQualityRange(currentQualityDisplay).lowerBound, 1))
local upperBound = math.log(getQualityRange(currentQualityDisplay + qualityAmount - 1).upperBound)
local barAmount = 32
local refinedAmount = math.log(math.max(itemProgress.refinedAmount, 1))
local progress = math.max((refinedAmount - lowerBound) / (upperBound - lowerBound), 0)
local hueLoops = math.floor(currentQualityDisplay / maxQuality)
local barProgressCount = 0
local barRight = ""
local currentPos = 1
while currentPos <= barAmount do
if (currentPos - 1) / barAmount <= progress then
barProgressCount += 1
else
barRight = barRight .. "|"
end
currentPos += 1
end
local qualities = ""
for i = 1, qualityAmount do
if barProgressCount <= 0 then
break
end
local bars = ""
for i = 1, barAmount / qualityAmount do
if barProgressCount <= 0 then
break
end
bars = bars .. "|"
barProgressCount -= 1
end
qualities = qualities .. ""..bars.. ""
end
if hueLoops > 0 then
itemProgress.completionImage:call_proc("add_filter", "glowing", 2, dm.global_proc("outline_filter", 2, rgbToHex(hsvToRgb(math.max(hueLoops * 30, 0), 2, 1, 0.2))))
end
itemProgress.completionImage:set_var("maptext", "