Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[modtools/arty-mod]]
- --Allows a unit to use fixed artillery.
- --modified version of zaporozhets' awesome cannon-smoke script
- --based in turn on scripts by Roses, expwnent and Fleeting Frames
- --now with miltibarrel support! more blam, more fun!
- local usage = [====[
- modtools/arty-mod
- ===========================
- Turns buildings into artillery pieces.
- -clear
- unregister all entries
- -list
- list current ammo/weapons/material entries and their values
- -artyType
- argument is barrel item subtype, such as ITEM_TOOL_BARREL_BOMBARD. will pass properties to any building containing this item, and set the item to be loaded with ammunition. type of ammunition used and the buildings themselves are defined in raws; reactions prefixed with ARTILLERY_LOAD will load their reagents into an empty barrel, ARTILLERY_FIRE will fire the gun and ARTILLERY_ROTATE_R and ARTILLERY_ROTATE_L will rotate the building, if the names of its 4 directions end in _N, _E, _S and _W. 1x1 guns will fire in all directions and do not need rotate reactions. multiple barrels can be housed in a single building, setting the load and fire jobs to repeat will load/fire until all barrels are loaded/unloaded.
- -shootForce - amount of force the projectile is give, works exactly the same as ranged weapon raw shoot force token (shoot force * 50000 / size * density)
- -range - minrange, maxrange. affects targeting and projectile behavior, targetting is done within range, actual projectile maxrange is maxrange + 3 to prevent guns wasting ammunition on targets moving around the edge of targeting range.
- -loadTime - delay, skill scaling. time in ticks it takes to load a barrel and how strongly this scales with skill. defined as [ number scale ] or just number, in which case scaling defaults to 1.
- -rotateTime - delay, skill scaling. same as loadTime, only for building rotation.
- -baseRof - delay, skill scaling. delay between shots for multibarrel buildings, if fire is set to repeat.
- -altRof - delay, interval, skill scaling. every (interval) many shots this delay is used instead.
- -windupRof - delay multiplier, shots before wound up, ticks per winddown. multiplies delay until a certain number of shots is fired, and winds back down once the gun stops firing.
- examples:
- -artyType ITEM_TOOL_BARREL_BOMBARD -loadTime 1000 -range 100 -shootForce 72000 -rotateTime 600
- -artyType ITEM_TOOL_BARREL_HANDGONNE -loadTime 180 -baseRof 30 -range 40 -shootForce 4000 -rotateTime 200
- ]====]
- local utils = require 'utils'
- local eventful = require 'plugins.eventful'
- eventful.enableEvent(eventful.eventType.JOB_INITIATED, 5)
- artyProperties = artyProperties or {}
- jobTable = false
- gunTable = {}
- local validArgs = utils.invert({
- 'help',
- 'clear',
- 'list',
- 'artyType',
- 'shootForce',
- 'range',
- 'loadTime',
- 'rotateTime',
- 'baseRof',
- 'altRof',
- 'windupRof'
- })
- local args = utils.processArgs({...}, validArgs)
- if args.artyType then
- artyProperties[args.artyType] = { shootForce = tonumber(args.shootForce) }
- if args.loadTime then artyProperties[args.artyType].loadTime = { tonumber(args.loadTime[1]) or tonumber(args.loadTime), tonumber(args.loadTime[2]) or 1 } end
- if args.rotateTime then artyProperties[args.artyType].rotateTime = { tonumber(args.rotateTime[1]) or tonumber(args.rotateTime), tonumber(args.rotateTime[2]) or 1 } end
- if args.range then artyProperties[args.artyType].range = { tonumber(args.range[1]) or 0, tonumber(args.range[2]) or tonumber(args.range) } end
- if args.baseRof then artyProperties[args.artyType].baseRof = { tonumber(args.baseRof[1]) or tonumber(args.baseRof), tonumber(args.baseRof[2]) or 1 } end
- if args.altRof then artyProperties[args.artyType].altRof = { tonumber(args.altRof[1]) or tonumber(args.altRof), tonumber(args.altRof[2]) or 2, tonumber(args.altRof[3]) or 1 } end
- if args.windupRof then artyProperties[args.artyType].windupRof = { tonumber(args.windupRof[1]) or tonumber(args.windupRof), tonumber(args.windupRof[2]) or 10, tonumber(args.windupRof[3]) or 40 } end
- end
- if args.clear then
- artyProperties = {}
- return
- end
- if args.list then
- for rer,ere in pairs(artyProperties) do
- print("- "..rer..":")
- for rerrer,rerere in pairs(ere) do
- print(" "..rerrer,rerere)
- end
- print()
- end
- return
- end
- if args.help then
- print(usage)
- return
- end
- eventful.onJobInitiated.blam = function(job)
- if string.match(job.reaction_name, "ARTILLERY") then
- if jobTable == false then
- jobTable = {}
- jobChecker()
- end
- end
- end
- function jobChecker()
- jobTable = {}
- for link,job in utils.listpairs(df.global.world.jobs.list) do
- if string.match(job.reaction_name, "ARTILLERY") then
- jobTable[#jobTable + 1] = job
- end
- end
- for _,job in pairs(jobTable) do
- local funcy = {nop, nop, nop}
- local mode = 1
- if not gunTable[job.general_refs[0].building_id] then funcy[1] = updateGunTable end
- if #job.general_refs > 1 then makeFearless(job.general_refs[1].unit_id, 1) end
- if job.completion_timer > 0 and job.flags.suspend == false and #job.general_refs > 0 then
- if job.reaction_name == "ARTILLERY_FIRE" then
- funcy[3] = findTarget
- elseif job.reaction_name == "ARTILLERY_ROTATE_L" or job.reaction_name == "ARTILLERY_ROTATE_R" then
- funcy[3] = rotGun
- elseif string.match(job.reaction_name, "ARTILLERY_LOAD") then
- funcy[3] = loadGun
- end
- for _,func in ipairs(funcy) do
- func(job, df.building.find(job.general_refs[0].building_id), df.unit.find(job.general_refs[1].unit_id), mode)
- end
- end
- end
- --for rer,ere in pairs(jobTable) do print('checking '..ere.reaction_name,"id: "..ere.id, "timer: "..ere.completion_timer) end
- for _,rer in pairs(jobTable) do
- dfhack.timeout(50,'ticks',function() jobChecker() end)
- return
- end
- jobTable = false
- --print('killed jobChecker')
- end
- function nop() end --function table iterator will break if it finds a field with a nothing, but is just fine running a nothing
- function updateGunTable(job, gun, firer, mode)
- if mode == 1 then
- gunTable[gun.id] = {props = {increment = 1, windup = 0, baseRof = {80, 1}}, loaded = {}, active = false, targetPos = {}}
- gunTable[gun.id].muzzlePos = checkDirection(gun, nil, 2)
- for _, barrelItem in ipairs(gun.contained_items) do
- if barrelItem.item:getSubtype() ~= -1 and artyProperties[barrelItem.item.subtype.id] and barrelItem.use_mode == 2 then
- barrelItem.item.flags.container = true
- for field,value in pairs(artyProperties[barrelItem.item.subtype.id]) do
- gunTable[gun.id].props[field] = value
- end
- end
- end
- end
- for _, barrelItem in ipairs(gun.contained_items) do
- if barrelItem.item.flags.container == true and barrelItem.use_mode == 2 and #dfhack.items.getContainedItems(barrelItem.item) > 0 then
- for _, loadItem in ipairs(dfhack.items.getContainedItems(barrelItem.item)) do
- if tostring(loadItem._type) == "<type: item_barst>" and loadItem.mat_type == 8 then
- gunTable[gun.id].loaded[#gunTable[gun.id].loaded].powder = loadItem
- else
- gunTable[gun.id].loaded[#gunTable[gun.id].loaded + 1] = {}
- gunTable[gun.id].loaded[#gunTable[gun.id].loaded].ammo = loadItem
- end
- end
- end
- end
- end
- function makeFearless(unitId, mode)
- local firer = df.unit.find(unitId)
- if not firer then return end
- if mode == 1 then
- if firer.flags2.vision_missing == false then
- firer.flags2.vision_good = false
- firer.flags2.vision_missing = true
- dfhack.timeout(1000, "ticks", function() makeFearless(firer.id, 2) end)
- end
- else
- firer.flags2.vision_good = true
- firer.flags2.vision_missing = false
- end
- end
- function findTarget(job, gun, firer, mode)
- --mode 2 is run after a delay, and needs a sanity check to see if job, firer aren't canceled/dead/on fire/etc
- if mode == 1 then
- if gunTable[gun.id].active == true then return end
- else
- local isJobDead = sanityCheck(job)
- if isJobDead == true then
- gunTable[gun.id].active = false
- return
- end
- end
- if #gunTable[gun.id].loaded == 0 then
- dfhack.gui.showAnnouncement(getFullName(firer).." cancels fire cannon: Not loaded.", 12)
- job.flags['repeat'] = false
- job.completion_timer = 0
- gunTable[gun.id].active = false
- return
- end
- local range = gunTable[gun.id].props.range or {0, 100}
- local nearestUnit = false
- for _,target in ipairs(df.global.world.units.active) do
- if checkDirection(gun, target, 1) then
- local targetDistance = findDistance(firer.pos, target.pos)
- if targetDistance >= range[1] and targetDistance <= range[2] then
- if isHostile(firer, target, job.reaction_name) then
- if canPathTo(firer.pos, target.pos, targetDistance) then
- nearestUnit = target
- end
- end
- end
- end
- end
- if nearestUnit then
- job.completion_timer = -1
- gunTable[gun.id].targetPos = xyz2pos(nearestUnit.pos.x,nearestUnit.pos.y,nearestUnit.pos.z)
- firefirefire(job, gun, firer, mode)
- else
- job.completion_timer = -1
- gunTable[gun.id].active = false
- end
- end
- function firefirefire(job, gun, firer, mode)
- local ammoItem = gunTable[gun.id].loaded[#gunTable[gun.id].loaded].ammo
- local powderItem = gunTable[gun.id].loaded[#gunTable[gun.id].loaded].powder
- local barrelId = dfhack.items.getContainer(ammoItem).id
- if powderItem then powderItem.flags.garbage_collect = true end
- ammoItem.flags.forbid = false
- ammoItem:moveToGround(gunTable[gun.id].muzzlePos.x,gunTable[gun.id].muzzlePos.y,gunTable[gun.id].muzzlePos.z)
- local projectile = dfhack.items.makeProjectile(ammoItem)
- --same formula ranged weapons use
- projectile.unk22 = math.floor(gunTable[gun.id].props.shootForce * 50000 / (projectile.item.weight * 1000000 + projectile.item.weight_fraction))
- --not the same formula ranged weapons use, building guns get a high hitrate bonus through ranged-mod
- local firerSkill = 0
- for _,skill in ipairs(firer.status.current_soul.skills) do
- if skill.id == 48 then firerSkill = skill.rating end
- end
- projectile.hit_rating = math.floor(math.random(0,10) + firerSkill * 3.5)
- projectile.origin_pos = gunTable[gun.id].muzzlePos
- projectile.prev_pos = gunTable[gun.id].muzzlePos
- projectile.cur_pos = gunTable[gun.id].muzzlePos
- projectile.target_pos = gunTable[gun.id].targetPos
- projectile.min_hit_distance = gunTable[gun.id].props.range[1]
- projectile.fall_threshold = gunTable[gun.id].props.range[2] + 3
- projectile.min_ground_distance = gunTable[gun.id].props.range[2] + 2
- projectile.bow_id = barrelId
- projectile.firer = firer
- projectile.distance_flown = 0
- projectile.fall_counter = 0
- projectile.fall_delay = 0
- gunTable[gun.id].loaded[#gunTable[gun.id].loaded] = nil
- if job.flags['repeat'] == true then --'job.flags.repeat' makes lua very confused, good thing theres another way to check this
- gunTable[gun.id].active = true
- local completionTime = getCompletionTime(job, gun, firer, 1)
- dfhack.timeout(completionTime,'ticks',function() findTarget(job, gun, firer, 2) end)
- else
- job.completion_timer = 0
- gunTable[gun.id].active = false
- end
- end
- function loadGun(job, gun, firer, mode)
- if mode == 1 then
- if gunTable[gun.id].active == true then return else gunTable[gun.id].active = true end
- local completionTime = getCompletionTime(job, gun, firer, 3)
- dfhack.timeout(completionTime,'ticks',function() loadGun(job, gun, firer, 2) end)
- job.completion_timer = 400
- return
- else
- local isJobDead = sanityCheck(job)
- if isJobDead == true then
- gunTable[gun.id].active = false
- return
- end
- end
- if #job.items == 0 then
- gunTable[gun.id].active = false
- job.completion_timer = 0
- end
- local alreadyLoaded = true
- for _, barrelItem in ipairs(gun.contained_items) do
- if barrelItem.item.flags.container == true and barrelItem.use_mode == 2 and #dfhack.items.getContainedItems(barrelItem.item) == 0 then
- alreadyLoaded = false
- for _, loadItem in ipairs(job.items) do
- local deletThis = false
- if loadItem.item:getStackSize() > 1 then
- loadItem.item:addStackSize(-1)
- else
- deletThis = true
- end
- local loadItemCopy = df.item.find(dfhack.items.createItem(loadItem.item:getType(), loadItem.item:getSubtype(), loadItem.item.mat_type, loadItem.item.mat_index, firer))
- if loadItemCopy:getSubtype() > -1 then loadItemCopy.quality = loadItem.item.quality end
- dfhack.items.moveToContainer(loadItemCopy,barrelItem.item)
- updateGunTable(job, gun, firer, 2)
- if deletThis == true then loadItem.item.flags.garbage_collect = true end
- end
- break
- end
- end
- if alreadyLoaded then
- dfhack.gui.showAnnouncement(getFullName(firer).." cancels loading artillery: Fully loaded.", 12)
- job.flags['repeat'] = false
- gunTable[gun.id].active = false
- job.completion_timer = 0
- --df.global.world.jobs:cancel_job(job) explodes the game, bad idea
- return
- elseif job.flags['repeat'] == true then
- gunTable[gun.id].active = true
- local completionTime = getCompletionTime(job, gun, firer, 2)
- dfhack.timeout(completionTime,'ticks',function() loadGun(job, gun, firer, 2) end)
- job.completion_timer = 400
- else
- gunTable[gun.id].active = false
- job.completion_timer = 0
- end
- end
- function rotGun(job, gun, firer, mode)
- if mode == 1 then
- if gunTable[gun.id].active == true then return else gunTable[gun.id].active = true end
- local completionTime = getCompletionTime(job, gun, firer, 1)
- dfhack.timeout(completionTime,'ticks',function() rotGun(job, gun, firer, 2) end)
- job.completion_timer = 400
- return
- end
- gunTable[gun.id].active = false
- local isJobDead = sanityCheck(job)
- if isJobDead == true then return end
- local gunDirection = string.sub(df.global.world.raws.buildings.all[gun.custom_type].code, -2)
- if job.reaction_name == "ARTILLERY_ROTATE_R" then
- if gunDirection == "_N" or gunDirection == "_E" or gunDirection == "_S" then
- gun.custom_type = gun.custom_type + 1
- else
- gun.custom_type = gun.custom_type - 3
- end
- else
- if gunDirection == "_E" or gunDirection == "_S" or gunDirection == "_W" then
- gun.custom_type = gun.custom_type - 1
- else
- gun.custom_type = gun.custom_type + 3
- end
- end
- gunTable[gun.id].muzzlePos = checkDirection(gun, nil, 2)
- job.completion_timer = 0
- end
- function getCompletionTime(job, gun, firer, mode)
- local completionTime
- local skillMult = 1
- local artyProps = gunTable[gun.id].props
- for _,firerSkill in ipairs(firer.status.current_soul.skills) do
- if firerSkill.id == 48 then
- skillMult = (30 - firerSkill.rating) / 30
- end
- end
- if mode == 1 then
- local windupMult = 1
- if artyProps.windupRof then windupMult = windupRof(artyProps, 1) end
- if not artyProps.altRof then
- completionTime = math.floor(artyProps.baseRof[1] * windupMult * (1 + (skillMult - 1) * artyProps.baseRof[2]))
- elseif artyProps.increment < artyProps.altRof[2] then
- artyProps.increment = artyProps.increment + 1
- completionTime = math.floor(artyProps.baseRof[1] * windupMult * (1 + (skillMult - 1) * artyProps.baseRof[2]))
- else
- artyProps.increment = 1
- completionTime = math.floor(artyProps.altRof[1] * windupMult * (1 + (skillMult - 1) * artyProps.altRof[3]))
- end
- elseif mode == 2 then
- completionTime = math.floor(artyProps.loadTime[1] * (1 + (skillMult - 1) * artyProps.loadTime[2]))
- elseif mode == 3 then
- completionTime = math.floor(artyProps.rotateTime[1] * (1 + (skillMult - 1) * artyProps.rotateTime[2]))
- end
- return completionTime
- end
- function windupRof(artyProps, mode)
- if mode == 1 then
- local windupMult = 1 + (artyProps.windupRof[1] - 1) * (artyProps.windupRof[2] - math.min(artyProps.windupRof[2],artyProps.windup)) / artyProps.windupRof[2]
- if artyProps.windup == 0 then
- dfhack.timeout(artyProps.windupRof[3],'ticks',function() windupRof(artyProps, 2) end)
- end
- artyProps.windup = math.min(artyProps.windup + 1, artyProps.windupRof[2] + 1)
- return windupMult
- end
- artyProps.windup = artyProps.windup - 1
- if artyProps.windup > 0 then
- dfhack.timeout(artyProps.windupRof[3],'ticks',function() windupRof(artyProps, 2) end)
- end
- end
- function canPathTo(originPos, targetPos, targetDistance)
- local directionVector = df.coord:new()
- directionVector.x = targetPos.x - originPos.x
- directionVector.y = targetPos.y - originPos.y
- directionVector.z = targetPos.z - originPos.z
- local stepAmount = 1 / targetDistance
- local step = 0
- local passCount = 0
- local linePath = df.coord:new()
- while step <= 1 do
- linePath.x = math.floor(originPos.x + (directionVector.x * step) + 0.5)
- linePath.y = math.floor(originPos.y + (directionVector.y * step) + 0.5)
- linePath.z = math.floor(originPos.z + (directionVector.z * step) + 0.5)
- if tilePassable(linePath) then
- passCount = passCount + 1
- end
- step = step + stepAmount
- end
- if passCount >= 1 / stepAmount then
- return true
- else
- return false
- end
- end
- function sanityCheck(job)
- local isJobDead = true
- for link,joblisting in utils.listpairs(df.global.world.jobs.list) do
- if joblisting == job then
- isJobDead = false
- break
- end
- end
- if isJobDead == true or #job.general_refs < 1 then return isJobDead end
- end
- function tilePassable(position)
- local isPassable = false
- if dfhack.maps.getTileBlock(position) ~= nil then
- if dfhack.maps.getTileBlock(position).walkable[position.x%16][position.y%16] ~= 0 or --found walkable tile
- dfhack.maps.getTileBlock(position).tiletype[position.x%16][position.y%16] == 32 or --found empty space
- dfhack.maps.getTileBlock(position).tiletype[position.x%16][position.y%16] == 1 then --found stairs
- isPassable = true
- end
- end
- return isPassable
- end
- function findDistance(originPos, targetPos)
- return math.floor(math.sqrt(((originPos.x - targetPos.x)^2) + ((originPos.y - targetPos.y)^2) + ((originPos.z - targetPos.z)^2)) + 0.5 )
- end
- function isHostile(firer, target, reaction) --TODO: implement better hostility check
- if reaction == "ARTILLERY_FIRE" then
- if target.flags2.killed == false then
- if target.civ_id == firer.civ_id or target.flags1.diplomat == true or target.flags1.merchant == true or target.flags2.visitor == true or target.flags2.resident == true then
- return false
- else
- return true
- end
- end
- elseif reaction == "ARTILLERY_FIRE_TEST" then
- if target.flags2.killed == false then
- if target.name.first_name ~= "" or target.flags1.diplomat == true or target.flags1.merchant == true or target.flags2.visitor == true or target.flags2.resident == true then
- return false
- else
- return true
- end
- end
- end
- end
- function checkDirection(gun, target, mode) --mode 1 checks direction of target against cannon orientation, mode 2 returns muzzle position
- if gun.centerx == gun.x1 then -- If your top right is also your center square, then your a 1x1 gun and able to turn 360 degrees.
- if mode == 1 then return true
- elseif mode == 2 then
- return xyz2pos(gun.centerx, gun.centery, gun.z)
- end
- end
- local gunDirection = string.sub(df.global.world.raws.buildings.all[gun.custom_type].code, -2)
- if gunDirection == "_N" then --Never
- if mode == 1 then
- if target.pos.y < gun.centery and math.abs(target.pos.x - gun.centerx) < math.abs(target.pos.y - gun.centery) * 1.2 then
- return true else return false
- end
- elseif mode == 2 then
- return xyz2pos(gun.centerx, gun.centery - 1, gun.z)
- end
- elseif gunDirection == "_E" then --Eat
- if mode == 1 then
- if target.pos.x > gun.centerx and math.abs(target.pos.y - gun.centery) < math.abs(target.pos.x - gun.centerx) * 1.2 then
- return true else return false
- end
- elseif mode == 2 then
- return xyz2pos(gun.centerx + 1, gun.centery, gun.z)
- end
- elseif gunDirection == "_S" then --Shredded
- if mode == 1 then
- if target.pos.y > gun.centery and math.abs(target.pos.x - gun.centerx) < math.abs(target.pos.y - gun.centery) * 1.2 then
- return true else return false
- end
- elseif mode == 2 then
- return xyz2pos(gun.centerx, gun.centery + 1, gun.z)
- end
- elseif gunDirection == "_W" then --Wheat
- if mode == 1 then
- if target.pos.x < gun.centerx and math.abs(target.pos.y - gun.centery) < math.abs(target.pos.x - gun.centerx) * 1.2 then
- return true else return false
- end
- elseif mode == 2 then
- return xyz2pos(gun.centerx - 1, gun.centery, gun.z)
- end
- end
- end
- function getFullName(unit) --doesn't account for heroic names yet because I wasn't sure if they replace the normal surname
- local firstName = unit.name.first_name
- firstName = firstName:sub(1,1):upper()..firstName:sub(2)
- local surname = ""
- for indx, wordID in pairs(unit.name.words) do
- if wordID ~= -1 then
- local word = df.global.world.raws.language.translations[unit.name.language].words[unit.name.words[indx]].value
- if indx == 0 then
- surname = word:sub(1,1):upper()..word:sub(2)
- elseif indx == 1 then
- surname = surname..word
- end
- end
- end
- return firstName.." "..surname..", Siege Operator"
- end
- function printTargetDebug(firer, nearestUnit, nearestDistance)
- print(firer.name.first_name.." fired cannon from: ")
- print(pos2xyz(firer.pos))
- print(" at:")
- if nearestUnit.name.first_name ~= "" then
- print(" "..nearestUnit.name.first_name)
- else
- print(" Creature")
- end
- print(" race: "..tostring(df.global.world.raws.creatures.all[nearestUnit.race].name[0]))
- print(" distance: "..tostring(nearestDistance))
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement