Skip to content

Capabilities

Ruin0x11 edited this page Mar 17, 2021 · 1 revision

Example: Magic prevention

I want to:

  1. Prevent casting Teleport and Create Wall in a specific map.
  2. Prevent casting Teleport if it's raining.

First I'd make some capability, ISkillPrevention.

local ISkillPrevention = class.interface("ISkillPrevention", {
                                            can_use_skill = "function",
                                            set_can_use_skill = "function",
                                         },
                                         { ICapability })

return ISkillPrevention

Then I'd make some default implementation for this capability.

local SkillPrevention = class.class("SkillPrevention", { ISkillPrevention })

function SkillPrevention:init()
   self.skills_enabled = {}
end

function SkillPrevention:can_use_skill(skill_id)
   if not data["base.skill"][skill_id] then
      return true
   end

   local enabled = self.skills_enabled[skill_id]
   if enabled == nil
      return true
   end
   return enabled
end

function SkillPrevention:set_can_use_skill(skill_id, enabled)
   if not data["base.skill"][skill_id] then
      return 
   end

   if enabled == nil then
      enabled = true
   end
   self.skills_enabled[skill_id] = not not enabled
end

return SkillPrevention

In the mod that defines this class, I'd have to register the default capability implementation for ISkillPrevention so the runtime can instantiate it when chara:get_or_create_capability(ISkillPrevention) gets called.

--- init.lua
local Capability = require("api.Capability")
local ISkillPrevention = require("mod.skill_prevention.api.ISkillPrevention")
local SkillPrevention = require("mod.skill_prevention.api.SkillPrevention")

Capability.set_default_implementation(ISkillPrevention, SkillPrevention)

Then, inside the map's archetype, I'd add a callback for base.on_map_set_archetype that sets up the capability.

function my_map.on_set_archetype(map)
   local skill_prevention = map:get_or_create_capability(ISkillPrevention)
   skill_prevention:set_can_use_skill("elona.teleport", false)
   skill_prevention:set_can_use_skill("elona.wall_creation", false)
end

Or maybe we could use some kind of extension system to specify the skills inside the map_archetype entry itself...

data:add {
   _type = "base.map_archetype",
   _id = "my_map",
   
   _ext = {
      skill_prevention = {
         disabled_skills = {
            "elona.teleport",
            "elona.wall_creation"
         }
      }
   }
}

...and then bind to base.on_set_map_archetype globally for the setup.

local function set_skill_preventions(map)
   local _ext = map:archetype()._ext.skill_prevention
   
   if _ext and _ext.disabled_skills then
      local skill_prevention = map:get_or_create_capability(ISkillPrevention)
      for _, skill_id in ipairs(_ext.disabled_skills)
         skill_prevention:set_can_use_skill(skill_id, false)
      end
   end
end
Event.register("base.on_set_map_archetype", "Set skill preventions", set_skill_preventions)

Finally, to implement the logic, I'd bind to elona_sys.before_cast_magic and check if the skill cannot be cast.

local function proc_magic_prevention(caster, params, result)
   local map = caster:current_map()
   local skill_prevention = map:get_capability(ISkillPrevention)
   if skill_prevention and if not skill_prevention:can_use_skill(params.magic_id) then
      Gui.mes("skill_prevention:cast.prevented")
      return { blocked = true, turn_result = "turn_end" }
   end
   return result
end
Event.register("elona_sys.before_cast_magic", "Proc magic prevention", proc_magic_prevention)

The part where Create Wall is prevented based on the weather doesn't use the capability, only an event hook.

local function proc_create_wall_prevention(caster, params, result)
   if Weather.is_raining() and params.magic_id == "elona.wall_creation" then
      Gui.mes("skill_prevention:cast.prevented_create_wall")
      return { blocked = true, turn_result = "turn_end" }
   end
end
Event.register("elona_sys.before_cast_magic", "Prevent Create Wall if raining", proc_create_wall_prevention)

Ultimately, because it's important to notify the player of the reason the teleport was prevented if it's raining, an event callback is necessary to show the specific message. Because of this, I think it makes more sense to just return { blocked = true } in the elona_sys.before_cast_magic event along with displaying the message, instead of using the capability.