Guest User

Untitled

a guest
Sep 14th, 2018
81
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 14.79 KB | None | 0 0
  1. --[[cannon-smoke]]
  2. --allows a workshop to function as a pseudo siege engine cannon
  3. --author zaporozhets
  4. --based on scripts by Roses, expwnent and Fleeting Frames
  5. --thanks and credits for researching completion_timer and many other great suggestions, bugfixes and improvements to Grimlocke
  6. --CANNON to the right of them! CANNON to the left of them!
  7.  
  8. local eventful = require 'plugins.eventful'
  9. eventful.enableEvent(eventful.eventType.JOB_INITIATED, 0)
  10. eventful.enableEvent(eventful.eventType.JOB_COMPLETED, 0)
  11.  
  12. local maxrange = 200 --maximum range of cannon (in tiles)
  13. local minrange = 5 --minimum range of cannon (in tiles)
  14. local max_hitrate = 100 --TODO: account for firer siege operation skill (skill number 48) skill levels are 0-20(+? (normalize in case)). dfhack.units.getEffectiveSkill(firer, 48)
  15. local velocity = 20000 --cannonball velocity
  16. local rotationMult = 0.1 --cannon rotation speed multiplier, lower is faster
  17. local loadMult = 0.3 --cannon load speed multiplier
  18.  
  19. gasSpread = 20 --distance (in tiles) gas will attempt to spread output_items
  20. gasDensity = 5000 --amount of gas produced by gas cannisters
  21.  
  22. eventful.onProjItemCheckMovement.cannon = function(projectile)
  23.     if projectile.distance_flown == 1 then
  24.         cannonSmoke(projectile, 5, 10)
  25.     elseif projectile.distance_flown == 2 then
  26.         cannonSmoke(projectile, 5, 25)
  27.     elseif projectile.distance_flown <= 7 then
  28.         cannonSmoke(projectile, 5,  25)
  29.     end
  30. end
  31.  
  32. eventful.onProjItemCheckImpact.cannon = function(projectile)
  33.     if projectile.item:getSubtype() ~= -1 and dfhack.items.getSubtypeDef(projectile.item:getType(),projectile.item:getSubtype()).id == "ITEM_TRAPCOMP_EXPLOSIVESHELL" then
  34.         cannonSmoke(projectile, 6, 50)
  35.     elseif projectile.item:getSubtype() ~= -1 and dfhack.items.getSubtypeDef(projectile.item:getType(),projectile.item:getSubtype()).id == "ITEM_TRAPCOMP_GASSHELL" then
  36.         cannonSmoke(projectile, 10, 5000)
  37.     end
  38. end
  39.  
  40. eventful.onJobInitiated.cannon = function(job)
  41.     if string.find(job.reaction_name, "GUN_") then
  42.         modifyJob(job)
  43.     end
  44. end
  45.  
  46. function modifyJob(job) --modifies job once it's started, otherwise waits for job start and checks again
  47.     if #job.general_refs > 1 then
  48.         local firer = df.unit.find(job.general_refs[1].unit_id)
  49.         makeFearless(firer, true) --only cowards flee
  50.     end
  51.     if job.completion_timer ~= -1 then
  52.         if job.reaction_name == "GUN_ROTATE_CANNON_R" or job.reaction_name == "GUN_ROTATE_CANNON_L" then
  53.             job.completion_timer = math.floor(job.completion_timer * rotationMult) --0.2
  54.         elseif job.reaction_name == "GUN_LOAD_CANNON" then
  55.             job.completion_timer = math.floor(job.completion_timer * loadMult) --0.5
  56.         elseif job.reaction_name == "GUN_FIRE_CANNON" or job.reaction_name == "GUN_TEST_FIRE_CANNON" then
  57.             local firer = df.unit.find(job.general_refs[1].unit_id)
  58.             local cannon = df.building.find(job.general_refs[0].building_id)
  59.             local loadedItems = cannonIsLoaded(firer, cannon)
  60.             if loadedItems then
  61.                 waitForTarget(job, firer, cannon, loadedItems)
  62.             else --not loaded
  63.                 job.completion_timer = 0
  64.             end
  65.         end
  66.     else
  67.         dfhack.timeout(50, "ticks", function() modifyJob(job) end)
  68.     end
  69. end
  70.  
  71. function makeFearless(firer, mode)
  72.     if mode == true then
  73.         if firer.flags2.vision_missing == false then
  74.             firer.flags2.vision_good = false
  75.             firer.flags2.vision_missing = true
  76.             dfhack.timeout(1000, "ticks", function() makeFearless(firer, false) end)
  77.         end
  78.     else
  79.         firer.flags2.vision_good = true
  80.         firer.flags2.vision_missing = false
  81.     end
  82. end
  83.  
  84. function cannonIsLoaded(firer, cannon)
  85.     local ammo
  86.     local powder
  87.     for _, cannonItem in ipairs(cannon.contained_items) do
  88.         if cannonItem.use_mode == 0 then
  89.             local item = cannonItem.item
  90.             if tostring(item._type) == "<type: item_trapcompst>" and item.flags.forbid == true then
  91.                 ammo = item
  92.             elseif tostring(item._type) == "<type: item_barst>" and item.mat_type == 8 and item.flags.forbid == true then
  93.                 powder = item
  94.             end
  95.         end
  96.     end
  97.     if ammo == nil then
  98.         dfhack.gui.showAnnouncement(getFullName(firer).." cancels fire cannon: No ammo loaded.", 12)
  99.         if powder ~= nil then powder.flags.forbid = false end
  100.         return false
  101.     elseif powder == nil then
  102.         dfhack.gui.showAnnouncement(getFullName(firer).." cancels fire cannon: No potash loaded.", 12)
  103.         if ammo ~= nil then ammo.flags.forbid = false end
  104.         return false
  105.     else
  106.         return {ammo, powder}
  107.     end
  108. end
  109.  
  110. function waitForTarget(job, firer, cannon, loadedItems)
  111.     local nearestUnit = false
  112.     local nearestDistance =  768
  113.     for _,target in ipairs(df.global.world.units.active) do
  114.         if checkDirection(cannon, target, 1) then
  115.             local targetDistance = findDistance(firer.pos, target.pos)
  116.             if targetDistance < nearestDistance and targetDistance > minrange then
  117.                 if isHostile(firer, target, job.reaction_name) then
  118.                     if canPathTo(firer.pos, target.pos, targetDistance) then
  119.                         nearestUnit = target
  120.                         nearestDistance = targetDistance
  121.                     end
  122.                 end
  123.             end
  124.         end
  125.     end
  126.     if nearestUnit then --found a target
  127.         --printTargetDebug(firer, nearestUnit, nearestDistance) --TODO: DISABLE
  128.         fireCannon(firer, cannon, loadedItems, nearestUnit)
  129.         job.completion_timer = 0
  130.     else
  131.         if firer.job.current_job ~= nil and firer.job.current_job.id == job.id then
  132.             job.completion_timer = 100
  133.             dfhack.timeout(50, "ticks", function() waitForTarget(job, firer, cannon, loadedItems) end)
  134.         else
  135.             job.completion_timer = 0
  136.         end
  137.     end
  138. end
  139.  
  140. function fireCannon(firer, cannon, loadedItems, nearestUnit)
  141.     local ammo = loadedItems[1]
  142.     local powder = loadedItems[2]
  143.     local ammoClone = df.item.find(dfhack.items.createItem(ammo:getType(), ammo:getSubtype(), ammo.mat_type, ammo.mat_index, firer))
  144.     if ammo.subtype.id == "ITEM_TRAPCOMP_GASSHELL" then
  145.         ammoClone.improvements:insert("#", ammo.improvements[0])
  146.     end
  147.     ammoClone.flags.forbid = true
  148.     ammoClone.quality = ammo.quality
  149.     ammo.flags.garbage_collect = true
  150.     powder.flags.garbage_collect = true
  151.     local muzzlePos = checkDirection(cannon, nil, 2)
  152.     dfhack.items.moveToGround(ammoClone, muzzlePos)
  153.     fireProjectile(ammoClone, muzzlePos, nearestUnit)
  154. end
  155.  
  156. eventful.onJobCompleted.cannon = function(job)
  157.     if job.reaction_name == "GUN_ROTATE_CANNON_R" or job.reaction_name == "GUN_ROTATE_CANNON_L" then
  158.         local cannon = df.building.find(job.general_refs[0].building_id)
  159.         local buildingDirection = string.sub(df.global.world.raws.buildings.all[cannon.custom_type].code, -2)
  160.         if job.reaction_name == "GUN_ROTATE_CANNON_R" then
  161.             if buildingDirection == "_N" or buildingDirection == "_E" or buildingDirection == "_S" then
  162.                 cannon.custom_type = cannon.custom_type + 1
  163.             else
  164.                 cannon.custom_type = cannon.custom_type - 3
  165.             end
  166.         else
  167.             if buildingDirection == "_E" or buildingDirection == "_S" or buildingDirection == "_W" then
  168.                 cannon.custom_type = cannon.custom_type - 1
  169.             else
  170.                 cannon.custom_type = cannon.custom_type + 3
  171.             end
  172.         end
  173.     elseif job.reaction_name == "GUN_LOAD_CANNON" then
  174.         if #job.items == 2 then
  175.             job.items[0].item.flags.forbid = true
  176.             job.items[1].item.flags.forbid = true
  177.             cannon = df.building.find(job.general_refs[0].building_id)
  178.             local loadedCount = 0
  179.             for _ in pairs(cannon.contained_items) do loadedCount = loadedCount + 1 end
  180.             if loadedCount > 4 then
  181.                 dfhack.gui.showAnnouncement(getFullName(unit).." cancels load cannon: Already loaded.", 12)        
  182.                 job.items[0].item.flags.forbid = false
  183.                 job.items[1].item.flags.forbid = false
  184.             end
  185.         end
  186.     elseif job.reaction_name == "GASSHELL" then
  187.         for indx, itemRef in ipairs(job.items) do
  188.             local item = itemRef.item
  189.             if tostring(item._type) == "<type: item_trapcompst>" then
  190.                 item:setSubtype(item:getSubtype() + 1) --venom is stored as an item improvement, filled shell is seperate trapcomp to prevent multiple fillings and for graphical reasons
  191.             end
  192.         end
  193.     end
  194. end
  195.  
  196. function getFullName(unit) --doesn't account for heroic names yet because I wasn't sure if they replace the normal surname
  197.     local firstName = unit.name.first_name
  198.     firstName = firstName:sub(1,1):upper()..firstName:sub(2)
  199.     local surname = ""
  200.     for indx, wordID in pairs(unit.name.words) do
  201.         if wordID ~= -1 then
  202.             local word = df.global.world.raws.language.translations[unit.name.language].words[unit.name.words[indx]].value
  203.             if indx == 0 then
  204.                 surname = word:sub(1,1):upper()..word:sub(2)
  205.             elseif indx == 1 then
  206.                 surname = surname..word
  207.             end
  208.         end
  209.     end
  210.     return firstName.." "..surname..", Siege Operator"
  211. end
  212.  
  213. function checkDirection(cannon, target, mode) --mode 1 checks direction of target against cannon orientation, mode 2 returns muzzle position
  214.     local cannon_direction = string.sub(df.global.world.raws.buildings.all[cannon.custom_type].code, -2)
  215.     if cannon_direction == "_N" then --Never
  216.         if mode == 1 then
  217.             if target.pos.y < cannon.centery - minrange then return true else return false end
  218.         elseif mode == 2 then
  219.             return {x = cannon.centerx, y = cannon.centery - 1, z = cannon.z}
  220.         end
  221.     elseif cannon_direction == "_E" then --Eat
  222.         if mode == 1 then
  223.             if target.pos.x > cannon.centerx + minrange then return true else return false end
  224.         elseif mode == 2 then
  225.             return {x = cannon.centerx + 1, y = cannon.centery, z = cannon.z}
  226.         end
  227.     elseif cannon_direction == "_S" then --Shredded
  228.         if mode == 1 then
  229.             if target.pos.y > cannon.centery + minrange then return true else return false end 
  230.         elseif mode == 2 then
  231.             return {x = cannon.centerx, y = cannon.centery + 1, z = cannon.z}
  232.         end
  233.     elseif cannon_direction == "_W" then --Wheat
  234.         if mode == 1 then
  235.             if target.pos.x < cannon.centerx - minrange then return true else return false end
  236.         elseif mode == 2 then
  237.             return {x = cannon.centerx - 1, y = cannon.centery, z = cannon.z}
  238.         end
  239.     end
  240. end
  241.  
  242. function isHostile(firer, target, reaction) --TODO: implement better hostility check
  243.     if reaction == "GUN_FIRE_CANNON" then
  244.         if target.flags2.killed == false then
  245.             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
  246.                 return false
  247.             else
  248.                 return true
  249.             end
  250.         end
  251.     elseif reaction ==  "GUN_TEST_FIRE_CANNON" then
  252.         if target.flags2.killed == false then
  253.             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
  254.                 return false
  255.             else
  256.                 return true
  257.             end
  258.         end
  259.     end
  260. end
  261.  
  262. function canPathTo(originPos, targetPos, targetDistance)
  263.     local directionVector = df.coord:new()
  264.     directionVector.x = targetPos.x - originPos.x
  265.     directionVector.y = targetPos.y - originPos.y
  266.     directionVector.z = targetPos.z - originPos.z
  267.     local stepAmount = 1 / targetDistance
  268.     local step = 0
  269.     local passCount = 0
  270.     local linePath = df.coord:new()
  271.     while step <= 1 do
  272.         linePath.x = math.floor(originPos.x + (directionVector.x * step) + 0.5)
  273.         linePath.y = math.floor(originPos.y + (directionVector.y * step) + 0.5)
  274.         linePath.z = math.floor(originPos.z + (directionVector.z * step) + 0.5)
  275.         if tilePassable(linePath) then
  276.             passCount = passCount + 1
  277.         end
  278.         step = step + stepAmount
  279.     end
  280.     if passCount >= 1 / stepAmount then
  281.         return true
  282.     else
  283.         return false
  284.     end
  285. end
  286.  
  287. function findDistance(originPos, targetPos)
  288.     return math.floor(math.sqrt(((originPos.x - targetPos.x)^2) + ((originPos.y - targetPos.y)^2) + ((originPos.z - targetPos.z)^2)) + 0.5 )
  289. end
  290.  
  291. function tilePassable(position)
  292.     local isPassable = false
  293.     if dfhack.maps.getTileBlock(position) ~= nil then
  294.         if dfhack.maps.getTileBlock(position).walkable[position.x%16][position.y%16] ~= 0 or --found walkable tile
  295.         dfhack.maps.getTileBlock(position).tiletype[position.x%16][position.y%16] == 32 or --found empty space
  296.         dfhack.maps.getTileBlock(position).tiletype[position.x%16][position.y%16] == 1 then --found stairs
  297.             isPassable = true
  298.         end
  299.     end
  300.     return isPassable
  301. end
  302.  
  303. function cannonSmoke(projectile, type, amount)
  304.     if projectile.item:getSubtype() ~= -1 and tostring(dfhack.items.getSubtypeDef(projectile.item:getType(),projectile.item:getSubtype())._type) == "<type: itemdef_trapcompst>" then
  305.         if type == 10 then --gas cannisters
  306.             local matType = projectile.item.improvements[0].mat_type
  307.             local matIndex = projectile.item.improvements[0].mat_index
  308.             local northFlow = dfhack.maps.spawnFlow(projectile.cur_pos, type, matType, matIndex, amount)
  309.                 northFlow.density = gasDensity
  310.                 northFlow.dest = {x = projectile.target_pos.x, y = projectile.target_pos.y - gasSpread, z = projectile.target_pos.z}
  311.             local eastFlow = dfhack.maps.spawnFlow(projectile.cur_pos, type, matType, matIndex, amount)
  312.                 eastFlow.density = gasDensity
  313.                 eastFlow.dest = {x = projectile.target_pos.x - gasSpread, y = projectile.target_pos.y, z = projectile.target_pos.z}        
  314.             local southFlow = dfhack.maps.spawnFlow(projectile.cur_pos, type, matType, matIndex, amount)
  315.                 southFlow.density = gasDensity
  316.                 southFlow.dest = {x = projectile.target_pos.x, y = projectile.target_pos.y + gasSpread, z = projectile.target_pos.z}           
  317.             local westFlow = dfhack.maps.spawnFlow(projectile.cur_pos, type, matType, matIndex, amount)
  318.                 westFlow.density = gasDensity
  319.                 westFlow.dest = {x = projectile.target_pos.x + gasSpread, y = projectile.target_pos.y, z = projectile.target_pos.z}
  320.         else
  321.             dfhack.maps.spawnFlow(projectile.cur_pos, type, -1, -1, amount) --other ammo types
  322.         end
  323.     end
  324. end
  325.  
  326. function fireProjectile(item, muzzlePos, target)
  327.     local projectile = dfhack.items.makeProjectile(item)
  328.     projectile.origin_pos.x = muzzlePos.x
  329.     projectile.origin_pos.y = muzzlePos.y
  330.     projectile.origin_pos.z = muzzlePos.z
  331.     projectile.prev_pos.x = muzzlePos.x
  332.     projectile.prev_pos.y = muzzlePos.y
  333.     projectile.prev_pos.z = muzzlePos.z
  334.     projectile.cur_pos.x = muzzlePos.x
  335.     projectile.cur_pos.y = muzzlePos.y
  336.     projectile.cur_pos.z = muzzlePos.z
  337.     projectile.target_pos.x = target.pos.x
  338.     projectile.target_pos.y = target.pos.y
  339.     projectile.target_pos.z = target.pos.z
  340.     projectile.flags.no_impact_destroy=false
  341.     projectile.flags.bouncing=false
  342.     projectile.flags.piercing=true
  343.     projectile.flags.parabolic=false
  344.     projectile.flags.unk9=false
  345.     projectile.flags.no_collide=false
  346.     projectile.distance_flown=0
  347.     projectile.fall_threshold=maxrange
  348.     projectile.min_hit_distance=minrange
  349.     projectile.min_ground_distance=maxrange-1
  350.     projectile.fall_counter=0
  351.     projectile.fall_delay=0
  352.     projectile.hit_rating=max_hitrate
  353.     projectile.unk22 = velocity
  354.     projectile.speed_x=0
  355.     projectile.speed_y=0
  356.     projectile.speed_z=0
  357. end
  358.  
  359. function printTargetDebug(firer, nearestUnit, nearestDistance)
  360.     print(firer.name.first_name.." fired cannon from: ")
  361.     print(pos2xyz(firer.pos))
  362.     print("  at:")
  363.     if nearestUnit.name.first_name ~= "" then
  364.         print(" "..nearestUnit.name.first_name)
  365.     else
  366.         print(" Creature")
  367.     end
  368.     print(" race: "..tostring(df.global.world.raws.creatures.all[nearestUnit.race].name[0]))
  369.     print(" distance: "..tostring(nearestDistance))
  370. end
Add Comment
Please, Sign In to add comment