-
Notifications
You must be signed in to change notification settings - Fork 9
Implement misc spells for OpenMW #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6f074c9
67c1c52
b37d919
6517136
c0308a1
e4eff0f
0d154e2
e8cbe1e
5a56df1
1fc3ed0
658504a
f63aa31
18f79bd
0feef41
b716f4b
dfca7ee
7524d92
fdc993a
8feed06
b35cd20
352562a
ba0268a
492ac47
7bfa24a
7bfe18f
2a4c468
88090bf
e0252c1
bc810ad
7cac36b
4c0e7f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,13 @@ | ||
| MENU: scripts/TamrielData/menu_settings.lua | ||
| GLOBAL: scripts/TamrielData/global_restrict_equipment.lua | ||
| GLOBAL: scripts/TamrielData/global_magic_passwall.lua | ||
| GLOBAL: scripts/TamrielData/global_mwscript_variable.lua | ||
| PLAYER: scripts/TamrielData/player_magic.lua | ||
| PLAYER: scripts/TamrielData/player_restrict_equipment.lua | ||
| MENU: scripts/TamrielData/menu_version_warning.lua | ||
| GLOBAL: scripts/TamrielData/global_magic.lua | ||
| GLOBAL: scripts/TamrielData/global_miscspells.lua | ||
| GLOBAL: scripts/TamrielData/global_summons.lua | ||
| PLAYER, NPC, CREATURE: scripts/TamrielData/actor_magic.lua | ||
| PLAYER, NPC, CREATURE: scripts/TamrielData/actor_summons.lua | ||
| LOAD: scripts/TamrielData/load_magic.lua | ||
| CUSTOM: scripts/TamrielData/container_banish.lua |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,294 @@ | ||
| local animation = require('openmw.animation') | ||
| local core = require('openmw.core') | ||
| local I = require('openmw.interfaces') | ||
| local nearby = require('openmw.nearby') | ||
| local self = require('openmw.self') | ||
| local types = require('openmw.types') | ||
| local util = require('openmw.util') | ||
| local auxUtil = require('openmw_aux.util') | ||
| local l10n = core.l10n('TamrielData') | ||
| local helpers = require('scripts.TamrielData.actor_magic_blink') | ||
|
|
||
| local FT_TO_UNITS = 22.1 | ||
|
|
||
| local activeEffects = self.type.activeEffects(self) | ||
| local activeSpells = self.type.activeSpells(self) | ||
|
|
||
| local function calculateReflect(health, fatigue) | ||
| local reflectedHealth = 0 | ||
| local reflectedFatigue = 0 | ||
| for _, spell in pairs(activeSpells) do | ||
| for _, effect in pairs(spell.effects) do | ||
| if effect.id == 't_mysticism_reflectdmg' then | ||
| local mult = effect.magnitudeThisFrame / 100 | ||
| reflectedHealth = reflectedHealth + health * mult | ||
| health = health * (1 - mult) | ||
| reflectedFatigue = reflectedFatigue + fatigue * mult | ||
| fatigue = fatigue * (1 - mult) | ||
| end | ||
| end | ||
| end | ||
| if health <= 0 then | ||
| health = nil | ||
| end | ||
| if fatigue <= 0 then | ||
| fatigue = nil | ||
| end | ||
| if reflectedHealth <= 0 then | ||
| reflectedHealth = nil | ||
| end | ||
| if reflectedFatigue <= 0 then | ||
| reflectedFatigue = nil | ||
| end | ||
| return health, fatigue, reflectedHealth, reflectedFatigue | ||
| end | ||
|
|
||
| I.Combat.addOnHitHandler(function(attack) | ||
| if not attack.successful or not attack.damage or not attack.attacker or not attack.attacker:isValid() then | ||
| return | ||
| elseif attack.sourceType ~= I.Combat.ATTACK_SOURCE_TYPES.Melee and attack.sourceType ~= I.Combat.ATTACK_SOURCE_TYPES.Ranged then | ||
| return | ||
| end | ||
| local health = attack.damage.health or 0 | ||
| local fatigue = attack.damage.fatigue or 0 | ||
| if health <= 0 and fatigue <= 0 then | ||
| return | ||
| elseif activeEffects:getEffect('t_mysticism_reflectdmg').magnitude <= 0 then | ||
| return | ||
| end | ||
| local newHealth, newFatigue, reflectedHealth, reflectedFatigue = calculateReflect(health, fatigue) | ||
| local reflectedAttack = auxUtil.shallowCopy(attack) | ||
| reflectedAttack.attacker = self.object | ||
| reflectedAttack.sourceType = I.Combat.ATTACK_SOURCE_TYPES.Unspecified | ||
| reflectedAttack.damage = {} | ||
| if attack.damage.health then | ||
| attack.damage.health = newHealth | ||
| reflectedAttack.damage.health = reflectedHealth | ||
| end | ||
| if attack.damage.fatigue then | ||
| attack.damage.fatigue = newFatigue | ||
| reflectedAttack.damage.fatigue = reflectedFatigue | ||
| end | ||
| attack.attacker:sendEvent('Hit', reflectedAttack) | ||
| end) | ||
|
|
||
| local function getDistractDestination(caster, range) | ||
| local casterPos = caster and caster.position | ||
| local selfPos = self.position | ||
| local agentBounds = self.type.getPathfindingAgentBounds(self) | ||
|
|
||
| local function getAvoidCasterBonus(candidate) | ||
| if not casterPos then | ||
| return 0 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like with the convention this function is doing (i.e. if I'm not misunderstanding this, then: it's not strictly a penalty, it's a bonus, but the bonus is lower the closer the candidate point is to the player 🤓) returning math.huge here would work better. But maybe it's a matter of no consequence. No strong feeling
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning You're right about the name, though. |
||
| end | ||
| local status, path = nearby.findPath(selfPos, candidate, { agentBounds = agentBounds }) | ||
| local penalty = (candidate - casterPos):length() * 0.25 | ||
| if status == nearby.FIND_PATH_STATUS.Success and next(path) then | ||
| local min = math.huge | ||
| for _, point in pairs(path) do | ||
| local distance = (point - casterPos):length2() | ||
| min = math.min(min, distance) | ||
| end | ||
| penalty = penalty + min | ||
| end | ||
| return penalty | ||
| end | ||
|
|
||
| local bestPos = nil | ||
| local bestScore = 0 | ||
| local SAMPLES = 12 | ||
|
|
||
| for i = 1, SAMPLES do | ||
| local candidate = nearby.findRandomPointAroundCircle(selfPos, range, { agentBounds = agentBounds }) | ||
| if candidate and math.abs(candidate.z - selfPos.z) < 384 then | ||
| local score = getAvoidCasterBonus(candidate) + (candidate - selfPos):length() * 0.5 | ||
| if score > bestScore then | ||
| bestScore = score | ||
| bestPos = candidate | ||
| end | ||
| end | ||
| end | ||
| return bestPos | ||
| end | ||
|
|
||
| function playDistractedVoiceLine(isEnd) | ||
| if types.NPC.objectIsInstance(self) and not self.type.isDead(self) and not self.type.isWerewolf(self) and activeEffects:getEffect('Vampirism').magnitude <= 0 then | ||
| -- Handling this in a global script so we only need one instance of the voice lines table in memory | ||
| core.sendGlobalEvent('T_DistractVoice', { actor = self.object, isEnd = isEnd }) | ||
| end | ||
| end | ||
|
|
||
| local state = {} | ||
|
|
||
| local timer = 0 | ||
|
|
||
| return { | ||
| engineHandlers = { | ||
| onInactive = function() | ||
| core.sendGlobalEvent('T_ActorInactive', self.object) | ||
| end, | ||
| onSave = function() | ||
| return state | ||
| end, | ||
| onLoad = function(data) | ||
| if data then | ||
| state = data | ||
| end | ||
| end, | ||
| onUpdate = function(dt) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Expanding my organizational comment in player_magic.lua https://github.com/TD-Addon/TD_Lua_Addons/pull/33/changes#r3448199617 I suggest the same about this actor_magic - that it may be better if it was split into files per spell/effect. Because for instance if I understand correctly, this onUpdate is purely for Distract?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now it's only for Distract, yes. Hard to say if it'll stay that way of course. |
||
| if not state.distract or not state.distract.returning then | ||
| return | ||
| end | ||
| timer = timer + dt | ||
| if timer >= 1 then | ||
| timer = 0 | ||
| local active = I.AI.getActivePackage() | ||
| if not active or active.type ~= 'Travel' then | ||
| self.type.stats.ai.hello(self).base = state.distract.hello | ||
| local resetRotation = true | ||
| if state.distract.wander then | ||
| resetRotation = state.distract.wander.distance == 0 | ||
| I.AI.startPackage(state.distract.wander) | ||
| end | ||
| if resetRotation then | ||
| local yaw = self.rotation:getYaw() | ||
| self.controls.yawChange = state.distract.originYaw - yaw | ||
| end | ||
| state.distract = nil | ||
| end | ||
| end | ||
| end | ||
| }, | ||
| eventHandlers = { | ||
| Died = function() | ||
| state.distract = nil | ||
| if state.banish then | ||
| core.sendGlobalEvent('T_BanishCorpse', { actor = self.object, height = state.banish }) | ||
| state.banish = nil | ||
| end | ||
| end, | ||
| T_Distract = function(data) | ||
| local active = I.AI.getActivePackage() | ||
| if active and active.type ~= 'Wander' then | ||
| return | ||
| end | ||
| local destination = getDistractDestination(data.caster, data.magnitude * FT_TO_UNITS) | ||
| if destination then | ||
| if not state.distract then | ||
| local hello = self.type.stats.ai.hello(self) | ||
| state.distract = { | ||
| hello = hello.base, | ||
| origin = self.position, | ||
| originYaw = self.rotation:getYaw(), | ||
| worldSpace = self.cell.worldSpaceId | ||
| } | ||
| if active then | ||
| state.distract.wander = { | ||
| type = 'Wander', | ||
| distance = active.distance, | ||
| duration = active.duration, | ||
| idle = active.idle and auxUtil.shallowCopy(active.idle), | ||
| isRepeat = active.isRepeat | ||
| } | ||
| end | ||
| hello.base = 0 | ||
| end | ||
| state.distract.returning = false | ||
| if math.random() < 0.45 then | ||
| playDistractedVoiceLine(false) | ||
| end | ||
| I.AI.startPackage({ type = 'Travel', destPosition = destination, cancelOther = true, isRepeat = false }) | ||
| end | ||
| end, | ||
| T_DistractFinished = function(effect) | ||
| if not state.distract then | ||
| return | ||
| end | ||
| if activeEffects:getEffect(effect).magnitude <= 0 then | ||
| state.distract.returning = true | ||
| if math.random() < 0.45 then | ||
| playDistractedVoiceLine(true) | ||
| end | ||
| if self.cell and self.cell.worldSpaceId == state.distract.worldSpace then | ||
| timer = 0 | ||
| I.AI.startPackage({ type = 'Travel', destPosition = state.distract.origin, cancelOther = true, isRepeat = false }) | ||
| end | ||
| end | ||
| end, | ||
| T_MarkWabbajack = function(data) | ||
| local dynamic = types.Actor.stats.dynamic | ||
| for _, key in pairs({ 'health', 'magicka', 'fatigue' }) do | ||
| local stat = dynamic[key](self) | ||
| local v = stat.base * data[key] | ||
| if v < 2 and key == 'health' then | ||
| v = 2 | ||
| end | ||
| stat.current = v | ||
| end | ||
| core.sound.playSound3d('alteration hit', self, { loop = false }) | ||
| activeSpells:add({ id = 'T_Dae_Alt_UNI_WabbajackTrans', effects = { 0 }, ignoreResistances = true, ignoreSpellAbsorption = true, ignoreReflect = true, caster = data.caster }) | ||
| end, | ||
| T_EndWabbajack = function(data) | ||
| local dynamic = types.Actor.stats.dynamic | ||
| local kill = false | ||
| for _, key in pairs({ 'health', 'magicka', 'fatigue' }) do | ||
| local stat = dynamic[key](self) | ||
| local v = stat.base * data[key] | ||
| if key == 'health' and v < 2 then | ||
| kill = data[key] <= 0 | ||
| v = 2 | ||
| end | ||
| stat.current = v | ||
| end | ||
| core.sound.playSound3d('alteration hit', self, { loop = false }) | ||
| if kill then | ||
| -- makes crime work | ||
| -- TODO: !5302 | ||
| types.Actor._onHit(self, { | ||
| damage = { health = 999 }, | ||
| sourceType = I.Combat.ATTACK_SOURCE_TYPES.Unspecified, | ||
| attacker = data.caster, | ||
| successful = true | ||
| }) | ||
| end | ||
| end, | ||
| T_AttemptBanish = function(data) | ||
| for _, actor in pairs(I.AI.getTargets('Follow')) do -- Could check Escort as well, I guess | ||
| if actor == data.caster then | ||
| return | ||
| end | ||
| end | ||
| local targetLevel = types.Actor.stats.level(self).current | ||
| local health = types.Actor.stats.dynamic.health(self) | ||
| if data.magnitude < targetLevel / 2 * (1 + health.current / math.max(health.base, 1)) then | ||
| I.AI.startPackage({ type = 'Combat', target = data.caster }) | ||
| if types.Player.objectIsInstance(data.caster) then | ||
| local record = self.type.records[self.recordId] | ||
| local name = record.name | ||
| if not name or name == '' then | ||
| name = record.id | ||
| end | ||
| data.caster:sendEvent('ShowMessage', { message = l10n('Magic_banishFailure', { target = name }) }) | ||
| end | ||
| return | ||
| end | ||
| activeEffects:remove('soultrap') | ||
| state.banish = self.type.getPathfindingAgentBounds(self).halfExtents.z * 2 -- yields better results than getBoundingBox | ||
| I.AnimationController.addPlayBlendedAnimationHandler(function(groupName, options) | ||
| if groupName:find('death') then | ||
| options.speed = 100 | ||
| end | ||
| end) | ||
| health.current = 0 | ||
| local model = types.Static.records['T_VFX_Banish'].model | ||
| core.sendGlobalEvent('SpawnVfx', { model = model, position = self.position }) | ||
| core.sound.playSound3d('mysticism hit', self) -- TODO !3029 | ||
| end, | ||
| T_Blink = function(magnitude) | ||
| -- TODO: check if levitation is disabled | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I made a ticket for that in https://gitlab.com/OpenMW/openmw/-/work_items/9163 |
||
| local destination, options = helpers.getBlinkDestination(magnitude) | ||
| -- TODO: don't use teleportation and preserve momentum | ||
| core.sendGlobalEvent('T_Teleport', { object = self.object, cell = self.cell.id, position = destination, options = options }) | ||
| end | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO if a file is not a PLAYER/ACTOR/GLOBAL/MENU script in omwscripts, then it shouldn't have a player/actor/global/menu prefix in its naming. And this - in its current form - is a utility file with a function used by both the actor and player blink scripting. I suggest one of these options:
EDIT: On the other hand, even if the script is not marked directly in omwscripts as a PLAYER/ACTOR, it may be written in a way that makes it usable only by player/actor scripts (ex. it calls functions only accessible within that scope). One could say that such approach should warrant even utility files to have a prefix in their name. Maybe also a valid point. Will think on it, unless you have opinions. Not a gamebreaking thing, really
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no strong feelings on naming. I tried to match what was there mostly. Adding additional subdirectories could be worthwhile (whether The only thing that really matters is that we're essentially locked in once we release. For any script that saves state anyway. So ideally we'd get it right the first time 😛 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| local core = require('openmw.core') | ||
| local nearby = require('openmw.nearby') | ||
| local self = require('openmw.self') | ||
| local util = require('openmw.util') | ||
|
|
||
| local FT_TO_UNITS = 22.1 | ||
| local BLINK_COLLISION = nearby.COLLISION_TYPE.AnyPhysical + nearby.COLLISION_TYPE.VisualOnly - nearby.COLLISION_TYPE.Water - nearby.COLLISION_TYPE.Projectile | ||
|
|
||
| local function getBlinkDestination(magnitude) | ||
| local range = magnitude * FT_TO_UNITS | ||
| local halfExtents = self.type.getPathfindingAgentBounds(self).halfExtents | ||
| local start = self.position + util.vector3(0, 0, halfExtents.z * 1.4) | ||
| local destination = start + self.rotation * util.vector3(0, range, 0) | ||
| local rayOptions = { ignore = self, collisionType = BLINK_COLLISION } | ||
| local result = nearby.castRay(start, destination, rayOptions) | ||
| local options | ||
| local ground | ||
| if result.hit then | ||
| destination = result.hitPos - self.rotation * util.vector3(0, halfExtents.y + 16, 0) | ||
| end | ||
| local height = util.vector3(0, 0, halfExtents.z * 2) | ||
| result = nearby.castRay(destination, destination + height, rayOptions) | ||
| if result.hit then -- bumped into the ceiling | ||
| local floor = result.hitPos - height | ||
| rayOptions.ignore = result.hitObject | ||
| result = nearby.castRay(result.hitPos, floor, rayOptions) | ||
| if result.hit then -- bumped into the floor; no room here | ||
| destination = self.position | ||
| else | ||
| destination = floor | ||
| end | ||
| end | ||
| if self.cell.isExterior then | ||
| local height = core.land.getHeightAt(destination, self.cell) | ||
| if destination.z < height then | ||
| ground = height | ||
| options = { onGround = true } | ||
| end | ||
| end | ||
| return destination, options, ground | ||
| end | ||
|
|
||
| return { | ||
| getBlinkDestination = getBlinkDestination | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if there's a difference, but to ensure the same number is accumulated and subtracted, I'd have written this part like that:
but maybe it's of no consequence. I'm always paranoid about floating points