Parcourir la source

Battle module: playable battles, AI, random encounters, world map encounters

Installer
- Fix battle IDs (scene * 4 + setup), battle locations, scene model table
  and formation flags/layouts
- Fix .da animation decoding and export attacks, enemy attacks, elements,
  statuses, items, drops, steal and AI scripts
- Export field random encounter tables to map.xml and world map encounter
  tables (ENC_W.BIN) to world_encounters.xml
- Decompile BMUSC (battle music) and BTLMD (battle mode) field opcodes

Engine
- Formation layout, escape, next battle, preemptive/back/pincer/side attacks
  and BTLMD battle modes
- Battle camera fixes, field entity hiding and audio handling
  (battle music, victory fanfare, resume field music)
- Random encounters in fields using the original game's logic
  (lower rate = more frequent, special battles first, no immediate repeats)
- World map random encounters: region and terrain group lookup, pause and
  resume the world around battles
- Player lock respected by the world map module
- Fix Manager::SetPreviousModule

Battle scripts and UI
- Action system, animations, damage formulas, magic, summons, items,
  statuses, escape, rewards and game over screen
- Enemy AI interpreter (battle_ai_vm.lua) and battle effects screen
- White flash transition into battles with movement lock
- BTLMD flags: no results, no victory music, no escape, timed battles,
  no game over

Data
- New battle_fx and game_over screens, debug materia and items
Iñigo Valentin il y a 3 jours
Parent
commit
f8c17ebcf2
57 fichiers modifiés avec 5650 ajouts et 894 suppressions
  1. 4 0
      data/data/screens/_screens.xml
  2. 292 0
      data/data/screens/battle/battle_fx.lua
  3. 72 0
      data/data/screens/battle/battle_fx.xml
  4. 20 0
      data/data/screens/fade/fade.lua
  5. 2 0
      data/data/screens/fade/fade.xml
  6. 64 0
      data/data/screens/game_over/game_over.lua
  7. 9 0
      data/data/screens/game_over/game_over.xml
  8. 1 0
      data/data/scripts/_scripts.xml
  9. 65 3
      data/data/scripts/battle.lua
  10. 471 0
      data/data/scripts/battle/battle_ai_vm.lua
  11. 2422 211
      data/data/scripts/battle/battle_ui.lua
  12. 42 0
      data/data/scripts/debug_data.lua
  13. 1 1
      data/data/scripts/menu/begin_menu.lua
  14. 1 0
      data/data/texts/english/menu.xml
  15. 1 0
      src/CMakeLists.txt
  16. 35 1
      src/core/AudioManager.cpp
  17. 17 0
      src/core/AudioManager.h
  18. 233 25
      src/core/BattleManager.cpp
  19. 251 0
      src/core/BattleManager.h
  20. 30 6
      src/core/CameraManager.cpp
  21. 30 0
      src/core/CameraManager.h
  22. 62 2
      src/core/Enemy.cpp
  23. 152 0
      src/core/Enemy.h
  24. 19 7
      src/core/Entity.cpp
  25. 26 0
      src/core/Entity.h
  26. 155 2
      src/core/EntityManager.cpp
  27. 146 0
      src/core/EntityManager.h
  28. 12 0
      src/core/EntityModel.cpp
  29. 4 0
      src/core/EntityModel.h
  30. 10 8
      src/core/Manager.cpp
  31. 81 6
      src/core/ScriptManagerBinds.h
  32. 78 0
      src/core/XmlAttackFile.cpp
  33. 125 0
      src/core/XmlAttackFile.h
  34. 48 14
      src/core/XmlEnemyFile.cpp
  35. 4 0
      src/core/XmlFormationFile.cpp
  36. 19 0
      src/core/XmlMapFile.cpp
  37. 6 0
      src/data/VGearsFLevelFile.cpp
  38. 24 0
      src/data/VGearsFLevelFile.h
  39. 5 1
      src/data/VGearsFLevelFileSerializer.cpp
  40. 88 25
      src/installer/BattleDataInstaller.cpp
  41. 28 0
      src/installer/FieldDataInstaller.cpp
  42. 23 41
      src/installer/KernelDataInstaller.cpp
  43. 60 0
      src/installer/WorldInstaller.cpp
  44. 9 0
      src/installer/WorldInstaller.h
  45. 93 422
      src/installer/data/BattleSceneFile.cpp
  46. 5 0
      src/installer/data/BattleSceneFile.h
  47. 46 95
      src/installer/data/DaFile.cpp
  48. 13 3
      src/installer/data/DaFile.h
  49. 15 0
      src/installer/data/Enemy.h
  50. 14 12
      src/installer/data/FF7Data.h
  51. 1 1
      src/installer/data/Formation.h
  52. 8 1
      src/installer/decompiler/field/instruction/FieldMediaInstruction.cpp
  53. 2 0
      src/installer/decompiler/field/instruction/FieldMediaInstruction.h
  54. 8 1
      src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp
  55. 2 0
      src/installer/decompiler/field/instruction/FieldModuleInstruction.h
  56. 154 6
      src/modules/worldmap/WorldmapModule.cpp
  57. 42 0
      src/modules/worldmap/WorldmapModule.h

+ 4 - 0
data/data/screens/_screens.xml

@@ -20,6 +20,10 @@
     <!-- Battle UI -->
     <prototype file_name="screens/battle/prototypes.xml" />
     <screen file_name="screens/battle/battle.xml" />
+    <screen file_name="screens/battle/battle_fx.xml" />
+
+    <!-- Game over screen -->
+    <screen file_name="screens/game_over/game_over.xml" />
 
     <!-- Fade effect for map changer -->
     <screen file_name="screens/fade/fade.xml" />

+ 292 - 0
data/data/screens/battle/battle_fx.lua

@@ -0,0 +1,292 @@
+if UiContainer == nil then UiContainer = {} end
+
+--- Battle visual effects.
+--
+-- Simple generic effects: bursts of coloured particles by element, screen flashes and floating
+-- numbers. Everything is made with the sprites and text areas of the BattleFx screen, moved from
+-- here every frame. Positions of the scene are projected to the UI with camera_manager.
+UiContainer.BattleFx = {
+
+    --- Size of the pools of the screen. Must match battle_fx.xml.
+    PARTICLES = 60,
+    NUMBERS = 8,
+
+    --- Height, in scene units, above the position of a combatant where effects are shown.
+    BODY_HEIGHT = 1.6,
+
+    --- Effects of each element: colour, particle count, speed (UI units per second), gravity (UI
+    -- units per second squared, negative makes them rise), size, life (seconds) and the flash
+    -- (colour and strength) of the spells of the element. "converge" makes the particles come to
+    -- the center instead of leaving it.
+    ELEMENTS = {
+        [0] = {colour = {1, 0.55, 0.1}, count = 24, speed = 90, gravity = -140, size = 10, life = 0.9, flash = {1, 0.5, 0.1, 0.3}},
+        [1] = {colour = {0.6, 0.9, 1}, count = 18, speed = 110, gravity = 180, size = 8, life = 0.8, flash = {0.6, 0.9, 1, 0.3}},
+        [2] = {colour = {1, 1, 0.4}, count = 16, speed = 260, gravity = 0, size = 6, life = 0.35, flash = {1, 1, 0.6, 0.55}},
+        [3] = {colour = {0.65, 0.45, 0.25}, count = 18, speed = 140, gravity = 320, size = 9, life = 0.9, up = true},
+        [4] = {colour = {0.5, 0.9, 0.3}, count = 16, speed = 50, gravity = -70, size = 9, life = 1.1, flash = {0.5, 0.9, 0.3, 0.2}},
+        [5] = {colour = {0.6, 0.3, 0.9}, count = 20, speed = 120, gravity = 0, size = 8, life = 0.8, converge = true, flash = {0.3, 0.1, 0.5, 0.35}},
+        [6] = {colour = {0.3, 0.6, 1}, count = 20, speed = 90, gravity = 240, size = 8, life = 0.9, flash = {0.3, 0.6, 1, 0.25}},
+        [7] = {colour = {0.8, 1, 0.9}, count = 16, speed = 240, gravity = 0, size = 5, life = 0.5, flash = {0.8, 1, 0.9, 0.2}},
+        [8] = {colour = {1, 1, 0.85}, count = 22, speed = 70, gravity = -100, size = 9, life = 1.0, flash = {1, 1, 0.9, 0.45}},
+    },
+
+    --- Effect of healing.
+    HEAL = {colour = {0.4, 1, 0.55}, count = 16, speed = 45, gravity = -80, size = 9, life = 1.0, flash = {0.4, 1, 0.55, 0.2}},
+
+    --- Effect of attacks without an element.
+    PHYSICAL = {colour = {1, 1, 1}, count = 9, speed = 150, gravity = 260, size = 6, life = 0.35},
+
+    --- Colours of the floating numbers.
+    COLOUR_DAMAGE = {1, 1, 1},
+    COLOUR_HEAL = {0.4, 1, 0.5},
+    COLOUR_MP = {0.5, 0.7, 1},
+    COLOUR_MISS = {0.7, 0.7, 0.7},
+
+    --- Active particles and numbers.
+    particles = {},
+    numbers = {},
+
+    --- Active screen flash.
+    flash_state = nil,
+
+    --- Game time of the last update.
+    last_time = 0,
+
+    --- Shows the effects screen and clears everything.
+    show = function(self)
+        self:clear()
+        self.last_time = timer:get_game_time_total()
+        ui_manager:get_widget("BattleFx"):set_visible(true)
+    end,
+
+    --- Hides the effects screen.
+    hide = function(self)
+        self:clear()
+        ui_manager:get_widget("BattleFx"):set_visible(false)
+    end,
+
+    --- Removes every effect.
+    clear = function(self)
+        for _, particle in pairs(self.particles) do ui_manager:get_widget("BattleFx.P" .. tostring(particle.index)):set_visible(false) end
+        for _, number in pairs(self.numbers) do ui_manager:get_widget("BattleFx.N" .. tostring(number.index)):set_visible(false) end
+        ui_manager:get_widget("BattleFx.Flash"):set_visible(false)
+        self.particles = {}
+        self.numbers = {}
+        self.flash_state = nil
+    end,
+
+    --- Gets the position of a combatant in the UI.
+    --
+    -- @param side Battle.TargetGroup.PARTY or ENEMIES.
+    -- @param index Index of the combatant.
+    -- @param height Height above the combatant, in scene units.
+    -- @return X and Y in UI units, nil if the combatant has no entity.
+    get_position = function(self, side, index, height)
+        local entity = UiContainer.BattleUi:get_entity(side, index)
+        if entity == nil then return nil end
+        local x, y, z = entity:get_position()
+        local ux, uy = camera_manager:project_to_ui(x, y, z + height)
+        return ux, uy
+    end,
+
+    --- Finds a free particle of the pool.
+    free_particle = function(self)
+        for i = 1, self.PARTICLES do
+            if self.particles[i] == nil then return i end
+        end
+        return nil
+    end,
+
+    --- Creates a particle.
+    --
+    -- @param p Table with x, y, vx, vy (per second), gravity, life, size, colour and alpha.
+    spawn = function(self, p)
+        local i = self:free_particle()
+        if i == nil then do return end end
+        p.index = i
+        p.age = 0
+        self.particles[i] = p
+        local widget = ui_manager:get_widget("BattleFx.P" .. tostring(i))
+        widget:set_colour(p.colour[1], p.colour[2], p.colour[3])
+        widget:set_alpha(p.alpha or 1)
+        widget:set_width(0, p.size)
+        widget:set_height(0, p.size)
+        widget:set_x(0, p.x - p.size / 2)
+        widget:set_y(0, p.y - p.size / 2)
+        widget:set_visible(true)
+    end,
+
+    --- Creates a burst of particles from a preset around a position.
+    burst = function(self, preset, x, y)
+        for _ = 1, preset.count do
+            local angle = math.random() * 2 * math.pi
+            local speed = preset.speed * (0.4 + 0.6 * math.random())
+            local vx, vy = math.cos(angle) * speed, math.sin(angle) * speed
+            local px, py = x, y
+            if preset.converge then
+                -- Start on a ring and move to the center.
+                px, py = x - vx * 0.5, y - vy * 0.5
+                vx, vy = vx, vy
+            end
+            if preset.up then vy = -math.abs(vy) * 1.3 end
+            -- Slightly different tones.
+            local shade = 0.8 + 0.2 * math.random()
+            self:spawn({
+                x = px, y = py, vx = vx, vy = vy, gravity = preset.gravity,
+                life = preset.life * (0.7 + 0.5 * math.random()), size = preset.size * (0.6 + 0.6 * math.random()),
+                colour = {preset.colour[1] * shade, preset.colour[2] * shade, preset.colour[3] * shade},
+            })
+        end
+    end,
+
+    --- Shows a screen flash that fades out.
+    --
+    -- @param colour Table with r, g, b.
+    -- @param strength Starting alpha.
+    -- @param life Seconds it lasts.
+    flash = function(self, colour, strength, life)
+        self.flash_state = {colour = colour, strength = strength, life = life or 0.35, age = 0}
+        local widget = ui_manager:get_widget("BattleFx.Flash")
+        widget:set_colour(colour[1], colour[2], colour[3])
+        widget:set_alpha(strength)
+        widget:set_visible(true)
+    end,
+
+    --- Gets the effect preset of an attack, for the element it has or if it heals.
+    get_preset = function(self, attack, heals)
+        if heals then return self.HEAL end
+        if attack ~= nil and attack.elements ~= nil and #attack.elements > 0 then
+            return self.ELEMENTS[attack.elements[1]] or self.PHYSICAL
+        end
+        return self.PHYSICAL
+    end,
+
+    --- Effect of casting a spell: sparks around the caster.
+    cast = function(self, attack, side, index)
+        -- Only magic is cast. Other attacks just hit.
+        if attack == nil or attack.id == nil or attack.id < 0 or (attack.damage.formula or 0) ~= 2 then do return end end
+        local x, y = self:get_position(side, index, self.BODY_HEIGHT)
+        if x == nil then do return end end
+        local preset = self:get_preset(attack, (attack.damage.modifier or 0) == 5 and (attack.damage.formula or 0) == 2)
+        for _ = 1, 8 do
+            local angle = math.random() * 2 * math.pi
+            self:spawn({
+                x = x + math.cos(angle) * 30, y = y + math.sin(angle) * 30,
+                vx = -math.cos(angle) * 40, vy = -math.sin(angle) * 40 - 30, gravity = 0,
+                life = 0.8, size = 7, colour = preset.colour,
+            })
+        end
+    end,
+
+    --- Effect of an attack that hits a target: particles and, for spells, a screen flash.
+    --
+    -- @param attack Attack data.
+    -- @param result The result of the attack, as in BattleUi.calculate_attack.
+    hit = function(self, attack, side, index, result)
+        local x, y = self:get_position(side, index, self.BODY_HEIGHT)
+        if x == nil then do return end end
+        local preset = self:get_preset(attack, result.amount < 0)
+        self:burst(preset, x, y)
+        if preset.flash ~= nil and attack.id ~= nil and attack.id >= 0 then
+            self:flash({preset.flash[1], preset.flash[2], preset.flash[3]}, preset.flash[4], 0.4)
+        end
+    end,
+
+    --- Shows a floating text over a combatant.
+    --
+    -- @param text The text to show.
+    -- @param colour Table with r, g, b.
+    text = function(self, side, index, text, colour)
+        local x, y = self:get_position(side, index, self.BODY_HEIGHT + 0.6)
+        if x == nil then do return end end
+        local slot = nil
+        for i = 1, self.NUMBERS do
+            if self.numbers[i] == nil then slot = i break end
+        end
+        -- If all are used, take the one that has been there the longest.
+        if slot == nil then
+            local oldest = 1
+            for i = 2, self.NUMBERS do
+                if self.numbers[i].age > self.numbers[oldest].age then oldest = i end
+            end
+            slot = oldest
+        end
+        -- Spread the numbers of the same target a bit so they don't pile up.
+        self.numbers[slot] = {
+            index = slot, x = x + (math.random() - 0.5) * 40 - 7 * #text / 2, y = y - 10 * math.random(),
+            age = 0, life = 1.1, text = text, colour = colour,
+        }
+        local number = self.numbers[slot]
+        local widget = ui_manager:get_widget("BattleFx.N" .. tostring(slot))
+        -- The colour is applied when the text is set.
+        widget:set_colour(colour[1], colour[2], colour[3])
+        widget:set_alpha(1)
+        widget:set_text(text)
+        widget:set_x(0, number.x)
+        widget:set_y(0, number.y)
+        widget:set_visible(true)
+    end,
+
+    --- Shows the HP or MP lost or recovered by a combatant.
+    --
+    -- @param amount Positive for damage, negative for recovery.
+    -- @param mp True if it's MP.
+    number = function(self, side, index, amount, mp)
+        local colour = self.COLOUR_DAMAGE
+        if amount < 0 then colour = self.COLOUR_HEAL end
+        if mp then colour = self.COLOUR_MP end
+        self:text(side, index, tostring(math.abs(amount)), colour)
+    end,
+
+    --- Advances the effects.
+    --
+    -- @param now Game time.
+    update = function(self, now)
+        local dt = math.max(0, math.min(0.1, now - self.last_time))
+        self.last_time = now
+        for i, p in pairs(self.particles) do
+            p.age = p.age + dt
+            local widget = ui_manager:get_widget("BattleFx.P" .. tostring(i))
+            if p.age >= p.life then
+                widget:set_visible(false)
+                self.particles[i] = nil
+            else
+                p.x = p.x + p.vx * dt
+                p.y = p.y + p.vy * dt
+                p.vy = p.vy + p.gravity * dt
+                local fade = 1 - p.age / p.life
+                widget:set_alpha((p.alpha or 1) * fade)
+                widget:set_x(0, p.x - p.size / 2)
+                widget:set_y(0, p.y - p.size / 2)
+            end
+        end
+        for i, n in pairs(self.numbers) do
+            n.age = n.age + dt
+            local widget = ui_manager:get_widget("BattleFx.N" .. tostring(i))
+            if n.age >= n.life then
+                widget:set_visible(false)
+                self.numbers[i] = nil
+            else
+                -- Rise, and fade out in the last part. Text colour is baked when it's set, so the
+                -- text is set again with the new alpha.
+                local alpha = math.min(1, (n.life - n.age) / (n.life * 0.4))
+                widget:set_colour(n.colour[1], n.colour[2], n.colour[3])
+                widget:set_alpha(alpha)
+                widget:set_text(n.text)
+                widget:set_x(0, n.x)
+                widget:set_y(0, n.y - 45 * (n.age / n.life))
+            end
+        end
+        local f = self.flash_state
+        if f ~= nil then
+            f.age = f.age + dt
+            local widget = ui_manager:get_widget("BattleFx.Flash")
+            if f.age >= f.life then
+                widget:set_visible(false)
+                self.flash_state = nil
+            else
+                widget:set_alpha(f.strength * (1 - f.age / f.life))
+            end
+        end
+    end,
+}

+ 72 - 0
data/data/screens/battle/battle_fx.xml

@@ -0,0 +1,72 @@
+<screen name="BattleFx" script="screens/battle/battle_fx.lua" visible="false">
+    <!-- Battle visual effects: a screen flash, a pool of particles and a pool of floating numbers. -->
+    <sprite name="Flash" x="0" y="0" width="100%" height="100%" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P1" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P2" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P3" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P4" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P5" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P6" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P7" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P8" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P9" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P10" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P11" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P12" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P13" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P14" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P15" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P16" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P17" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P18" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P19" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P20" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P21" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P22" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P23" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P24" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P25" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P26" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P27" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P28" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P29" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P30" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P31" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P32" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P33" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P34" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P35" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P36" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P37" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P38" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P39" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P40" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P41" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P42" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P43" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P44" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P45" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P46" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P47" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P48" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P49" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P50" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P51" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P52" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P53" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P54" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P55" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P56" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P57" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P58" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P59" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <sprite name="P60" x="0" y="0" width="8" height="8" colour="1 1 1" alpha="0" visible="false" />
+    <text_area name="N1" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N2" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N3" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N4" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N5" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N6" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N7" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+    <text_area name="N8" x="0" y="0" scale="1.4 1.4" font="FFVIIFont" visible="false" />
+</screen>

+ 20 - 0
data/data/screens/fade/fade.lua

@@ -13,6 +13,26 @@ UiContainer.Fade = {
 
 
 
+    --- Transition from the field or the world map to the battle in Battle.pending_id.
+    --
+    -- The screen flashes white and the battle starts under the flash, which BattleUi.show reveals.
+    battle_transition = function( self )
+        -- The player can't move during the transition. It's unlocked when the battle ends, unless
+        -- the player was already locked.
+        Battle.locked_by_transition = not entity_manager:is_player_locked()
+        player_lock( true )
+        local fade = ui_manager:get_widget( "Fade.Screen" )
+        fade:set_colour( 1, 1, 1 )
+        fade:play_animation_stop( "BattleFlash" )
+        fade:animation_sync()
+        battle_manager:start_battle( Battle.pending_id )
+        -- The battle takes over the screen, so the cover of the field goes away.
+        fade:set_alpha( 0 )
+        fade:set_colour( 0, 0, 0 )
+        UiContainer.BattleUi.show( UiContainer.BattleUi )
+        return 0
+    end,
+
     fade_out = function( self )
         local fade = ui_manager:get_widget( "Fade.Screen" )
         fade:play_animation_stop( "FadeOut" )

+ 2 - 0
data/data/screens/fade/fade.xml

@@ -2,5 +2,7 @@
     <sprite name="Screen" x="0" y="0" width="100%" height="100%" colour="" alpha="0" visible="true">
         <animation name="FadeIn" length="1" alpha="0:1, 1:0" />
         <animation name="FadeOut" length="1" alpha="0:0, 1:1" />
+        <!-- Battle transition: the screen flashes white a few times. -->
+        <animation name="BattleFlash" length="0.9" alpha="0:0, 0.1:1, 0.2:0.15, 0.3:1, 0.4:0.15, 0.5:1, 0.9:1" />
     </sprite>
 </screen>

+ 64 - 0
data/data/screens/game_over/game_over.lua

@@ -0,0 +1,64 @@
+if UiContainer == nil then UiContainer = {} end
+
+--- The game over screen.
+--
+-- Shown when the whole party dies in battle. It fades to black, plays the game over music, and
+-- when the player confirms, goes back to the begin menu.
+UiContainer.GameOver = {
+
+    --- Music to play. Name as in the musics file (57 is the game over music of the original game).
+    MUSIC = "57",
+
+    --- Indicates that the screen is being shown.
+    active = false,
+
+    --- Indicates that the screen is fully shown and can be dismissed.
+    ready = false,
+
+    --- Shows the game over screen.
+    show = function(self)
+        self.active = true
+        self.ready = false
+        UiContainer.current_menu = "game_over"
+        ui_manager:get_widget("GameOver.Container.Text"):set_visible(false)
+        ui_manager:get_widget("GameOver"):set_visible(true)
+        audio_manager:play_music(self.MUSIC)
+        local background = ui_manager:get_widget("GameOver.Background")
+        background:play_animation_stop("FadeIn")
+        background:animation_sync()
+        ui_manager:get_widget("GameOver.Container.Text"):set_visible(true)
+        self.ready = true
+        return 0
+    end,
+
+    --- Handles button events.
+    --
+    -- Confirming, once the screen is shown, leaves it.
+    on_button = function(self, button, event)
+        if self.active == false or self.ready == false then do return 0 end end
+        if button == Config.ACTIONS.CONFIRM then self:leave() end
+        return 0
+    end,
+
+    --- Leaves the game over screen and goes back to the begin menu.
+    --
+    -- TODO: Reload the last save, as the original game does. For now the party is healed, so the
+    -- debug battles of the begin menu can be started again.
+    leave = function(self)
+        self.active = false
+        self.ready = false
+        ui_manager:get_widget("GameOver.Container.Text"):set_visible(false)
+        ui_manager:get_widget("GameOver"):set_visible(false)
+        for i = 1, Party.MAX do
+            if Party[i] ~= nil then
+                local stats = Characters[Party[i]].stats
+                stats.hp.current = stats.hp.max or stats.hp.base
+            end
+        end
+        Battle.finish()
+        MenuSettings.pause_available = false
+        UiContainer.current_menu = ""
+        script:request_end_sync(Script.UI, "BeginMenu", "show", 0)
+        return 0
+    end,
+}

+ 9 - 0
data/data/screens/game_over/game_over.xml

@@ -0,0 +1,9 @@
+<screen name="GameOver" script="screens/game_over/game_over.lua" visible="false">
+    <!-- Black background, fades in over the battle. -->
+    <sprite name="Background" x="0" y="0" width="100%" height="100%" colour="0 0 0" alpha="0" visible="true">
+        <animation name="FadeIn" length="1.5" alpha="0:0, 1.5:1" />
+    </sprite>
+    <widget name="Container" width="368" height="240" origin_x="50%" origin_y="50%" align="center" valign="middle" scale="2.61 3" visible="true">
+        <text_area name="Text" x="148" y="110" text_name="GameOverText" font="FFVIIFont" visible="false" />
+    </widget>
+</screen>

+ 1 - 0
data/data/scripts/_scripts.xml

@@ -24,6 +24,7 @@
     <script file="scripts/worldmap/triangle_triggers.lua" />
     <script file="scripts/worldmap/vehicles.lua" />
     <script file="scripts/battle_ai.lua" />
+    <script file="scripts/battle/battle_ai_vm.lua" />
     <script file="scripts/menu/menu_helpers.lua" />
 
     <!-- TODO: Debugging pourposes only -->

+ 65 - 3
data/data/scripts/battle.lua

@@ -170,19 +170,72 @@ Battle.logic_script = {
     end,
 }
 
+--- Flags set by the field opcode BTLMD (battle_manager:set_mode).
+Battle.Mode = {
+    NO_RESULTS = 0x80,
+    ARENA = 0x40,
+    NO_VICTORY_MUSIC = 0x20,
+    NO_ESCAPE = 0x08,
+    PREEMPTIVE = 0x04,
+    TIMED = 0x02,
+    NO_GAME_OVER = 0x100,
+}
+
+--- Checks if a BTLMD flag is set for the current battle.
+Battle.has_mode = function(flag)
+    return math.floor(battle_manager:get_mode() / flag) % 2 == 1
+end
+
 --- Starts a battle.
 --
 -- @param id Battle ID.
 Battle.start = function(id)
-    battle_manager:start_battle(id)
-    script:request_end_sync(Script.UI, "BattleUi", "show", 0)
+    Battle.pending_id = id
+    script:request_end_sync(Script.UI, "Fade", "battle_transition", 0)
     return 0
 end
 
+--- Starts a battle right away, with no transition. It can be called from the battle tick, so it
+-- doesn't yield. Used for battles that follow another battle.
+--
+-- @param id Battle ID.
+Battle.chain = function(id)
+    battle_manager:start_battle(id)
+    script:request(Script.UI, "BattleUi", "show", 0)
+end
+
+--- Random encounters.
+Encounter = {
+
+    --- Indicates that the current battle is a random encounter.
+    active = false,
+}
+
+--- Starts a random encounter.
+--
+-- Called by the engine when the player walks into one. It isn't a script, so it can't yield.
+-- @param id Battle ID.
+Encounter.start = function(id)
+    -- The player can't walk during the battle.
+    player_lock(true)
+    Encounter.active = true
+    Battle.pending_id = id
+    script:request(Script.UI, "Fade", "battle_transition", 0)
+end
+
 --- Ends a battle.
 Battle.finish = function()
     battle_manager:end_battle()
-    script:request_end_sync(Script.UI, "BattleUi", "hide", 0)
+    -- Random encounters give control back to the player.
+    if Encounter.active then
+        Encounter.active = false
+        player_lock(false)
+    elseif Battle.locked_by_transition then
+        player_lock(false)
+    end
+    Battle.locked_by_transition = false
+    -- Not "request_end_sync": this can be called from the battle tick, which can't yield.
+    script:request(Script.UI, "BattleUi", "hide", 0)
     return 0
 end
 
@@ -446,3 +499,12 @@ Battle.add_random_modifier_and_zero_check = function( damage )
 
     return random_damage
 end
+
+--- Opcodes of the world map scripts that deal with battles.
+SetBattleLock = function(locked)
+    world_map_module:set_encounters_locked(locked ~= 0)
+end
+
+TriggerBattle = function(id)
+    Encounter.start(id)
+end

+ 471 - 0
data/data/scripts/battle/battle_ai_vm.lua

@@ -0,0 +1,471 @@
+--- Interpreter of the enemy AI scripts of the original game.
+--
+-- The scripts are bytecode for a stack machine (see the opcode list below). Memory is addressed by
+-- bit: addresses below 0x2000 are variables of the actor that owns the script, 0x2000 to 0x3FFF
+-- are values of the battle shared by every actor and 0x4000 to 0x433F are values of each actor
+-- (HP, MP, level, statuses...), selected with an actor mask.
+--
+-- The interpreter doesn't know about the battle: it asks a host for every global and actor value,
+-- and notifies it of actions and texts. The host must have the functions:
+--
+--   host:read_global(address, size)         Value of a global address.
+--   host:write_global(address, size, value)
+--   host:read_actor(actor, field, size)     Value of an actor field (address minus 0x4000).
+--   host:write_actor(actor, field, size, value)
+--   host:action(owner, type, id)             An action was requested, see BattleAI.ACTION. The targets
+--                                            are the last value written to the global 0x2070.
+--   host.owner                               Actor running the script (set by the interpreter).
+--   host:text(owner, bytes)                 A text has to be shown (FF7 encoded text).
+--   host:mp_cost(id)                        MP cost of an attack.
+--   host:memory_bank(write, offset, value)  Access to the game memory banks (1 and 2).
+--
+-- Value sizes are 0 (bit), 1 (byte), 2 (word) and 3 (dword).
+
+BattleAI = {}
+
+--- Types of actions requested with the action opcode.
+BattleAI.ACTION = {
+    ENEMY_ATTACK = 0x20,
+    RUN_SCRIPT = 0x22,
+    SHOW_TEXT = 0x24,
+}
+
+--- Slots of the scripts of an enemy.
+BattleAI.SLOT = {
+    INIT = 0, MAIN = 1, COUNTER_GENERAL = 2, COUNTER_DEATH = 3, COUNTER_PHYSICAL = 4,
+    COUNTER_MAGIC = 5, BATTLE_END = 6, PRE_TURN = 7, EVENT_1 = 8,
+}
+
+--- Bytes used to store each value size.
+BattleAI.SIZE_BYTES = {[0] = 1, 1, 2, 4}
+
+--- Maximum number of instructions in a script run, to stop endless loops.
+BattleAI.MAX_STEPS = 20000
+
+--- Persistent variables (addresses 0x0000 to 0x03FF) of every actor, by actor index. They keep
+-- their values between scripts.
+BattleAI.variables = {}
+
+--- Clears the persistent variables, at the beginning of a battle.
+function BattleAI.reset()
+    BattleAI.variables = {}
+end
+
+--- Gets the table of bytes with the variables of an actor.
+local function variables_of(actor)
+    local vars = BattleAI.variables[actor]
+    if vars == nil then
+        vars = {}
+        BattleAI.variables[actor] = vars
+    end
+    return vars
+end
+
+--- Reads a value of memory with the byte array of the variables of an actor.
+local function read_bytes(bytes, address, size)
+    local byte_index = math.floor(address / 8)
+    if size == 0 then
+        return math.floor((bytes[byte_index] or 0) / 2 ^ (address % 8)) % 2
+    end
+    local value = 0
+    for i = BattleAI.SIZE_BYTES[size] - 1, 0, -1 do
+        value = value * 256 + (bytes[byte_index + i] or 0)
+    end
+    return value
+end
+
+--- Writes a value to the byte array of the variables of an actor.
+local function write_bytes(bytes, address, size, value)
+    local byte_index = math.floor(address / 8)
+    if size == 0 then
+        local bit = 2 ^ (address % 8)
+        local current = bytes[byte_index] or 0
+        local has = math.floor(current / bit) % 2
+        if (value ~= 0) and has == 0 then current = current + bit
+        elseif (value == 0) and has == 1 then current = current - bit
+        end
+        bytes[byte_index] = current
+        return
+    end
+    value = math.floor(value)
+    if value < 0 then value = value + 2 ^ 32 end
+    for i = 0, BattleAI.SIZE_BYTES[size] - 1 do
+        bytes[byte_index + i] = value % 256
+        value = math.floor(value / 256)
+    end
+end
+
+--- Counts the actors of a mask in order, calling a function with their indexes.
+local function each_actor(mask, fn)
+    mask = math.floor(mask)
+    for actor = 0, 31 do
+        if math.floor(mask / 2 ^ actor) % 2 == 1 then fn(actor) end
+    end
+end
+
+--- Bitwise operations on non negative integers (Lua 5.1 has none).
+local function band(a, b)
+    local result, bit = 0, 1
+    a, b = math.floor(a), math.floor(b)
+    while a > 0 and b > 0 do
+        if a % 2 == 1 and b % 2 == 1 then result = result + bit end
+        a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2
+    end
+    return result
+end
+
+local function bor(a, b)
+    local result, bit = 0, 1
+    a, b = math.floor(a), math.floor(b)
+    while a > 0 or b > 0 do
+        if a % 2 == 1 or b % 2 == 1 then result = result + bit end
+        a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2
+    end
+    return result
+end
+
+local function bnot(a, bits)
+    return 2 ^ bits - 1 - math.floor(a)
+end
+
+BattleAI.band = band
+BattleAI.bor = bor
+
+--- The context of a script run: the owner, the host and the stack.
+local Context = {}
+Context.__index = Context
+
+--- Reads the value of a memory address for an owner or an actor.
+--
+-- @param actor Actor whose actor fields or variables are read.
+function Context:read(address, size, actor)
+    if address < 0x2000 then
+        return read_bytes(variables_of(actor), address, size)
+    elseif address < 0x4000 then
+        return self.host:read_global(address, size)
+    end
+    return self.host:read_actor(actor, address - 0x4000, size)
+end
+
+--- Writes a value to a memory address, of an actor or its variables.
+function Context:write(address, size, value, actor)
+    if address < 0x2000 then
+        write_bytes(variables_of(actor), address, size, value)
+    elseif address < 0x4000 then
+        self.host:write_global(address, size, value)
+    else
+        self.host:write_actor(actor, address - 0x4000, size, value)
+    end
+end
+
+--- Gets the number a stack entry stands for.
+--
+-- Addresses give their value, and entries that stand for several actors the value of the first.
+function Context:value_of(entry)
+    if entry.kind == "value" then return entry.v end
+    if entry.kind == "address" then
+        if entry.mask ~= nil then return self:first_value(entry) end
+        return self:read(entry.address, entry.size, self.owner)
+    end
+    return self:first_value(entry)
+end
+
+--- Gets the value of the first actor of a masked entry.
+function Context:first_value(entry)
+    local result = 0
+    for actor = 0, 31 do
+        if math.floor(entry.mask / 2 ^ actor) % 2 == 1 then
+            return self:read(entry.address, entry.size, actor)
+        end
+    end
+    return result
+end
+
+--- Pushes a value.
+function Context:push(entry)
+    self.top = self.top + 1
+    self.stack[self.top] = entry
+end
+
+--- Pops an entry from the stack.
+function Context:pop()
+    local entry = self.stack[self.top]
+    if entry == nil then error("AI script: the stack is empty") end
+    self.stack[self.top] = nil
+    self.top = self.top - 1
+    return entry
+end
+
+--- Pushes a plain number.
+function Context:push_value(size, value)
+    self:push({kind = "value", size = size, v = value})
+end
+
+--- Gets the actor mask an entry stands for (values are masks, addresses give their value).
+function Context:mask_of(entry)
+    return math.floor(self:value_of(entry))
+end
+
+--- Compares two entries with an operator, for the comparison opcodes.
+--
+-- If one of them has an actor mask, the result is the mask of the actors that pass the comparison.
+-- Otherwise it's 1 or 0.
+local function compare(op, a, b)
+    if op == 0x40 then return a == b
+    elseif op == 0x41 then return a ~= b
+    elseif op == 0x42 then return a >= b
+    elseif op == 0x43 then return a <= b
+    elseif op == 0x44 then return a > b
+    elseif op == 0x45 then return a < b
+    end
+    return false
+end
+
+function Context:compare(op, second, first)
+    -- "first" is the first pop (the right operand), "second" the second pop.
+    local left_masked = second.mask ~= nil and (second.kind == "multi" or second.kind == "address")
+    local right_masked = first.mask ~= nil and (first.kind == "multi" or first.kind == "address")
+    if not left_masked and not right_masked then
+        return {kind = "value", size = 0, v = compare(op, self:value_of(second), self:value_of(first)) and 1 or 0}
+    end
+    local mask = left_masked and second.mask or first.mask
+    if left_masked and right_masked then mask = band(second.mask, first.mask) end
+    local passed = 0
+    each_actor(mask, function(actor)
+        local a = left_masked and self:read(second.address, second.size, actor) or self:value_of(second)
+        local b = right_masked and self:read(first.address, first.size, actor) or self:value_of(first)
+        if compare(op, a, b) then passed = passed + 2 ^ actor end
+    end)
+    return {kind = "value", size = 2, v = passed}
+end
+
+--- Runs a script.
+--
+-- @param code The bytecode, as a string.
+-- @param owner Index of the actor that owns the script.
+-- @param host The host of the interpreter.
+-- @return True if it finished, false if it was stopped (error or too many instructions).
+function BattleAI.run(code, owner, host)
+    -- The host needs to know who is running the script to answer about "self".
+    host.owner = owner
+    local ctx = setmetatable({owner = owner, host = host, stack = {}, top = 0}, Context)
+    local pc = 0
+    local steps = 0
+    local length = #code
+    local function byte(i) return code:byte(i + 1) end
+    local function word(i) return byte(i) + byte(i + 1) * 256 end
+    while pc < length do
+        steps = steps + 1
+        if steps > BattleAI.MAX_STEPS then return false, "too many instructions" end
+        local op = byte(pc)
+        local hi, lo = math.floor(op / 16), op % 16
+        if hi == 0 and lo <= 3 then
+            -- Push the value at an address. Actor fields remember their address, so they can be
+            -- masked with actors later.
+            local address = word(pc + 1)
+            ctx:push({kind = "value", size = lo, v = ctx:read(address, lo, owner), address = address})
+            pc = pc + 3
+        elseif hi == 1 and lo <= 3 then
+            ctx:push({kind = "address", size = lo, address = word(pc + 1)})
+            pc = pc + 3
+        elseif op >= 0x60 and op <= 0x62 then
+            local size = op - 0x5F
+            local value = 0
+            for i = size - 1, 0, -1 do value = value * 256 + byte(pc + 1 + i) end
+            ctx:push_value(size, value)
+            pc = pc + 1 + size
+        elseif op >= 0x30 and op <= 0x37 then
+            if op == 0x37 then
+                local a = ctx:pop()
+                ctx:push_value(2, bnot(ctx:value_of(a), 16))
+            else
+                local b = ctx:pop()
+                local a = ctx:pop()
+                local x, y = ctx:value_of(a), ctx:value_of(b)
+                local r
+                if op == 0x30 then r = x + y
+                elseif op == 0x31 then r = x - y
+                elseif op == 0x32 then r = x * y
+                elseif op == 0x33 then r = y ~= 0 and math.floor(x / y) or 0
+                elseif op == 0x34 then r = y ~= 0 and x % y or 0
+                elseif op == 0x35 then r = band(x, y)
+                else r = bor(x, y)
+                end
+                ctx:push_value(math.max(a.size or 2, b.size or 2, 2), r)
+            end
+            pc = pc + 1
+        elseif op >= 0x40 and op <= 0x45 then
+            local first = ctx:pop()
+            local second = ctx:pop()
+            ctx:push(ctx:compare(op, second, first))
+            pc = pc + 1
+        elseif op >= 0x50 and op <= 0x52 then
+            if op == 0x52 then
+                local a = ctx:pop()
+                ctx:push_value(0, ctx:value_of(a) == 0 and 1 or 0)
+            else
+                local b = ctx:pop()
+                local a = ctx:pop()
+                local x, y = ctx:value_of(a) ~= 0, ctx:value_of(b) ~= 0
+                local r
+                if op == 0x50 then r = x and y else r = x or y end
+                ctx:push_value(0, r and 1 or 0)
+            end
+            pc = pc + 1
+        elseif op == 0x70 then
+            -- Jump if the value is zero.
+            local a = ctx:pop()
+            if ctx:value_of(a) == 0 then pc = word(pc + 1) else pc = pc + 3 end
+        elseif op == 0x71 then
+            -- Jump if the value is different from the top of the stack (switch cases).
+            local a = ctx:pop()
+            local top = ctx.stack[ctx.top]
+            if top == nil or ctx:value_of(a) ~= ctx:value_of(top) then pc = word(pc + 1) else pc = pc + 3 end
+        elseif op == 0x72 then
+            pc = word(pc + 1)
+        elseif op == 0x73 then
+            return true
+        elseif op == 0x74 or op == 0x75 then
+            ctx:pop()
+            pc = pc + 1
+        elseif op >= 0x80 and op <= 0x87 then
+            if op == 0x80 then
+                -- The first pop is masked by the second: an address or a value of an actor field
+                -- is for those actors, two plain values are ANDed.
+                local first = ctx:pop()
+                local second = ctx:pop()
+                local mask = ctx:mask_of(second)
+                if first.kind == "address" then
+                    ctx:push({kind = "address", size = first.size, address = first.address, mask = mask})
+                elseif first.address ~= nil and (first.address >= 0x4000 or first.address < 0x2000) then
+                    ctx:push({kind = "multi", size = first.size, address = first.address, mask = mask})
+                else
+                    ctx:push_value(math.max(first.size or 2, 2), band(ctx:value_of(first), mask))
+                end
+            elseif op == 0x81 then
+                ctx:push_value(2, host:random(65536))
+            elseif op == 0x82 then
+                -- A random one of the bits that are on.
+                local mask = ctx:mask_of(ctx:pop())
+                local bits = {}
+                each_actor(mask, function(actor) table.insert(bits, actor) end)
+                if #bits == 0 then ctx:push_value(2, 0)
+                else ctx:push_value(2, 2 ^ bits[host:random(#bits) + 1])
+                end
+            elseif op == 0x83 then
+                local a = ctx:pop()
+                local value = math.floor(ctx:value_of(a))
+                if a.size == 1 or a.size == 0 then
+                    local count = 0
+                    each_actor(value, function() count = count + 1 end)
+                    ctx:push_value(1, count)
+                else
+                    -- The first bit that is on, as a value with only that bit.
+                    local result = 0
+                    for bit = 0, 31 do
+                        if math.floor(value / 2 ^ bit) % 2 == 1 then result = 2 ^ bit break end
+                    end
+                    ctx:push_value(2, result)
+                end
+            elseif op == 0x84 or op == 0x85 then
+                -- The actors with the highest (or lowest) value of a masked entry.
+                local a = ctx:pop()
+                local best, mask = nil, 0
+                if a.mask ~= nil then
+                    each_actor(a.mask, function(actor)
+                        local value = ctx:read(a.address, a.size, actor)
+                        if best == nil or (op == 0x84 and value > best) or (op == 0x85 and value < best) then
+                            best, mask = value, 2 ^ actor
+                        elseif value == best then
+                            mask = mask + 2 ^ actor
+                        end
+                    end)
+                end
+                ctx:push_value(2, mask)
+            elseif op == 0x86 then
+                local a = ctx:pop()
+                ctx:push_value(2, host:mp_cost(math.floor(ctx:value_of(a))))
+            else
+                local a = ctx:pop()
+                ctx:push_value(2, 2 ^ math.floor(ctx:value_of(a)))
+            end
+            pc = pc + 1
+        elseif op == 0x90 then
+            -- Store the value on top of the address below it. Entries with an actor mask store
+            -- the value in every actor of the mask.
+            local value = ctx:pop()
+            local target = ctx:pop()
+            if target.kind ~= "address" then
+                -- Some scripts push the value first: swap if so.
+                if value.kind == "address" then value, target = target, value end
+            end
+            if target.kind == "address" then
+                local v = ctx:value_of(value)
+                if target.mask ~= nil then
+                    each_actor(target.mask, function(actor) ctx:write(target.address, target.size, v, actor) end)
+                else
+                    ctx:write(target.address, target.size, v, owner)
+                end
+            end
+            pc = pc + 1
+        elseif op == 0x91 then
+            ctx:pop()
+            pc = pc + 1
+        elseif op == 0x92 then
+            local id = ctx:pop()
+            local kind = ctx:pop()
+            host:action(owner, math.floor(ctx:value_of(kind)), math.floor(ctx:value_of(id)))
+            pc = pc + 1
+        elseif op == 0x93 then
+            -- A text, in the FF7 encoding, finished with 0xFF.
+            local stop = pc + 1
+            while stop < length and byte(stop) ~= 0xFF do stop = stop + 1 end
+            host:text(owner, code:sub(pc + 2, stop))
+            pc = stop + 1
+        elseif op == 0x94 then
+            -- Copy the state of some actors to others. Not used by the battle yet.
+            ctx:pop()
+            ctx:pop()
+            pc = pc + 1
+        elseif op == 0x95 then
+            -- Access to the memory banks 1 and 2 of the game: read or write, at an offset.
+            local offset = ctx:pop()
+            local write = ctx:pop()
+            host:memory_bank(ctx:value_of(write) == 1, ctx:value_of(offset))
+            pc = pc + 1
+        elseif op == 0x96 then
+            ctx:pop()
+            ctx:pop()
+            ctx:push_value(2, 0)
+            pc = pc + 1
+        elseif op == 0xA0 then
+            -- Debug text, with one value for each "%d". Skipped.
+            local stop = pc + 2
+            while stop < length and byte(stop) ~= 0 do stop = stop + 1 end
+            local text = code:sub(pc + 3, stop)
+            for _ in text:gmatch("%%d") do ctx:pop() end
+            pc = stop + 1
+        elseif op == 0xA1 then
+            ctx:pop()
+            ctx:pop()
+            pc = pc + 1
+        else
+            -- Anything else does nothing.
+            pc = pc + 1
+        end
+    end
+    return true
+end
+
+--- Runs a script protected from errors.
+--
+-- @return True if it finished correctly.
+function BattleAI.run_safe(code, owner, host)
+    local ok, result, reason = pcall(BattleAI.run, code, owner, host)
+    if not ok then
+        print("AI script error: " .. tostring(result))
+        return false
+    end
+    if result == false then print("AI script stopped: " .. tostring(reason)) end
+    return result
+end

Fichier diff supprimé car celui-ci est trop grand
+ 2422 - 211
data/data/scripts/battle/battle_ui.lua


+ 42 - 0
data/data/scripts/debug_data.lua

@@ -66,6 +66,48 @@ for i = 1, 24 do -- E. Skill list
         Characters[0].armor.materia[2].skills[i] = false
     end
 end
+-- Magic for testing. Materia IDs: Fire 49, Ice 50, Earth 51, Lightning 52, Restore 53, Heal 54,
+-- Revive 55, Poison 60. AP is enough to reach the highest level.
+Characters[0].weapon.materia[1] = {id = 49, ap = 100000}
+Characters[0].armor.materia[1] = {id = 53, ap = 100000}
+Characters[2].weapon.materia[0] = {id = 50, ap = 100000}
+Characters[2].weapon.materia[1] = {id = 52, ap = 100000}
+Characters[2].armor.materia[0] = {id = 54, ap = 100000}
+Characters[2].armor.materia[1] = {id = 55, ap = 100000}
+Characters[3].weapon.materia[0] = {id = 53, ap = 100000}
+Characters[3].weapon.materia[1] = {id = 49, ap = 100000}
+Characters[3].armor.materia[0] = {id = 60, ap = 100000}
+Characters[3].armor.materia[1] = {id = 51, ap = 100000}
+-- Status magic: Seal 56 (Sleepel, Silence), Mystify 57 (Confu, Berserk), Transform 58 (Mini, Toad),
+-- Time 65 (Haste, Slow, Stop), Destruct 68 (DeBarrier, DeSpell, Death). Cloud already has Barrier
+-- (62) in his armor.
+Characters[0].weapon.materia[2] = {id = 65, ap = 100000}
+Characters[0].weapon.materia[3] = {id = 56, ap = 100000}
+Characters[2].armor.materia[2] = {id = 68, ap = 100000}
+Characters[2].armor.materia[3] = {id = 57, ap = 100000}
+Characters[3].armor.materia[2] = {id = 58, ap = 100000}
+-- Summons: Choco/Mog 74, Shiva 75, Ifrit 76, Ramuh 77, Titan 78, Odin 79, Leviathan 80, Bahamut 81,
+-- Kjata 82, Alexander 83, Phoenix 84, Neo Bahamut 85, Hades 86, Typhoon 87, Bahamut ZERO 88,
+-- Knights of Round 89, Master Summon 90. They cost between 14 and 250 MP.
+Characters[0].weapon.materia[4] = {id = 75, ap = 100000} -- Shiva
+Characters[0].weapon.materia[5] = {id = 76, ap = 100000} -- Ifrit
+Characters[2].weapon.materia[2] = {id = 77, ap = 100000} -- Ramuh
+Characters[2].weapon.materia[3] = {id = 74, ap = 100000} -- Choco/Mog
+Characters[3].weapon.materia[2] = {id = 84, ap = 100000} -- Phoenix
+Characters[3].weapon.materia[3] = {id = 81, ap = 100000} -- Bahamut
+-- Items to try: Potion, Hi-Potion, Ether, Phoenix Down, Antidote, Echo Screen, Hyper, Tranquilizer,
+-- Remedy, Speed Drink, Grenade, Shrapnel, Molotov.
+for _, item in ipairs({{0, 6}, {1, 4}, {3, 4}, {7, 4}, {8, 3}, {12, 3}, {13, 2}, {14, 2}, {15, 2}, {17, 3}, {20, 5}, {21, 3}, {53, 3}}) do
+    Inventory.add_item(item[1], item[2])
+end
+-- Uncomment to have enough MP to cast anything, summons included.
+--[[
+for _, c in ipairs({0, 2, 3}) do
+    Characters[c].stats.mp.base = 999
+    Characters[c].stats.mp.max = 999
+    Characters[c].stats.mp.current = 999
+end
+]]
 Materia.add(4)
 Materia.add(44)
 Materia.add(12)

+ 1 - 1
data/data/scripts/menu/begin_menu.lua

@@ -101,7 +101,7 @@ if UiContainer == nil then UiContainer = {} end
                                             console("debug_walkmesh false")
                                             script:request_end_sync(Script.UI, "BeginMenu", "hide", 0)
                                             set_party(0, 2, 3)
-                                            Battle.start(220)
+                                            Battle.start(372) -- Single level 2 MP, easy test battle
                                             MenuSettings.pause_available = true
                                             end
                                             elseif button == Config.CONTROLS.DOWN then

+ 1 - 0
data/data/texts/english/menu.xml

@@ -135,5 +135,6 @@
     <text name="BattleUiMp">MP</text>
     <text name="BattleUiLimit">LIMIT</text>
     <text name="BattleUiTime">TIME</text>
+    <text name="GameOverText">GAME OVER</text>
     <text name="BattleUiDebugExit">Debug: Press 'K' to end battle.</text>
 </texts>

+ 1 - 0
src/CMakeLists.txt

@@ -655,6 +655,7 @@ set(VGEARS_SOURCE_FILES
     core/Utilites.cpp
     core/Walkmesh.cpp
     core/WorldMapManager.cpp
+    core/XmlAttackFile.cpp
     core/XmlBackground2DFile.cpp
     core/XmlBattleCharactersFile.cpp
     core/XmlBattleScenesFile.cpp

+ 35 - 1
src/core/AudioManager.cpp

@@ -101,7 +101,11 @@ void AudioManager::UpdateDebug(){}
 
 void AudioManager::OnResize(){}
 
-void AudioManager::ClearField(){tracks_.clear();}
+void AudioManager::ClearField(){
+    tracks_.clear();
+    // The battle music is set by the scripts of each field.
+    battle_track_ = -1;
+}
 
 void AudioManager::ClearBattle(){battle_track_ = -1;}
 
@@ -145,6 +149,34 @@ void AudioManager::MusicPlay(const Ogre::String& name){
     }
 }
 
+void AudioManager::ScriptPlayMusicOnce(const char* name){
+    if (!initialized_) return;
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    AudioManager::Music* music = GetMusic(Ogre::String(name));
+    if (music == nullptr){
+        LOG_ERROR("No music found with name \"" + Ogre::String(name) + "\".");
+        return;
+    }
+    // A negative loop point means no loop: the player stops at the end of the track.
+    if (module_ == Module::BATTLE){
+        music_.Pause();
+        battle_music_.SetLoop(-1);
+        battle_music_.Play(music->file);
+    }
+    else{
+        battle_music_.Stop();
+        music_.SetLoop(-1);
+        music_.Play(music->file);
+    }
+}
+
+void AudioManager::EndBattleMusic(){
+    if (!initialized_) return;
+    boost::recursive_mutex::scoped_lock lock(update_mutex_);
+    battle_music_.Stop();
+    music_.Resume();
+}
+
 void AudioManager::SoundPlay(const Ogre::String& name){
     if (initialized_){
         boost::recursive_mutex::scoped_lock lock(update_mutex_);
@@ -305,6 +337,8 @@ void AudioManager::Player::Play(const Ogre::String &file){
 
     // If the same track is already playing, don't restart.
     if (file_ == file) return;
+    // Another track is playing: stop it first, or it would keep playing with no way to stop it.
+    if (!file_.empty()) Stop();
 
     // Open vorbis file.
     if (ov_fopen(const_cast<char*>(file.c_str()), &vorbis_file_)){

+ 17 - 0
src/core/AudioManager.h

@@ -282,6 +282,23 @@ class AudioManager : public Manager, public Ogre::Singleton<AudioManager>{
          */
         void ScriptSetBattleTrack(int id);
 
+        /**
+         * Plays a music track once, without looping.
+         *
+         * To be called from Lua scripts. Used for short tracks like the victory fanfare. In battle
+         * it plays in the battle music player, replacing the battle music.
+         *
+         * @param[in] name Name of the track, as in the music index.
+         */
+        void ScriptPlayMusicOnce(const char* name);
+
+        /**
+         * Stops the battle music and lets the field or world map music continue.
+         *
+         * To be called when a battle ends, before going back to the previous module.
+         */
+        void EndBattleMusic();
+
     private:
 
         /**

+ 233 - 25
src/core/BattleManager.cpp

@@ -14,6 +14,7 @@
  */
 
 #include <algorithm>
+#include <cstdlib>
 #include <iostream>
 #include <cmath>
 #include <OgreEntity.h>
@@ -22,6 +23,7 @@
 #include "FF7Character.h"
 #include "core/AudioManager.h"
 #include "core/BattleManager.h"
+#include "modules/worldmap/WorldmapModule.h"
 #include "core/CameraManager.h"
 #include "core/Console.h"
 #include "core/DialogsManager.h"
@@ -51,6 +53,18 @@ const float BattleManager::PARTY_SCALE = 0.0039f;
 
 const float BattleManager::SCENE_SCALE = 0.0012f;
 
+const float BattleManager::PARTY_Y = 5.0f;
+
+const float BattleManager::SIDE_DISTANCE = 14.0f;
+
+// Model rotations, in degrees. Party members face the enemies at 0 degrees, and enemies face the
+// party at 180. Left and right are seen from the battle camera. If the models face the wrong
+// direction in side or pincer attacks, swap these two.
+const int BattleManager::FACING_TO_ENEMIES = 0;
+const int BattleManager::FACING_TO_PARTY = 180;
+const int BattleManager::FACING_TO_RIGHT = 90;
+const int BattleManager::FACING_TO_LEFT = -90;
+
 BattleManager::BattleManager(){
     LOG_TRIVIAL("BattleManager created.");
     scene_node_ = Ogre::Root::getSingleton().getSceneManager("Scene")
@@ -95,6 +109,10 @@ void BattleManager::ClearBattle(){
     money_ = 0;
     spoil_.clear();
     enemies_.clear();
+    enemy_entity_name_.clear();
+    party_entity_name_.clear();
+    party_animation_prefix_.clear();
+    party_model_name_.clear();
     // TODO party_.clear();
 }
 
@@ -107,11 +125,16 @@ void BattleManager::StartBattle(const unsigned int id){
         LOG_ERROR("Started battle in BattleManager, but the manager was already in battle mode.");
         return;
     }
+    // Battles can start in the world map, which is hidden and stopped while the battle lasts.
+    battle_from_world_ = EntityManager::getSingleton().IsWorldModule();
+    if (battle_from_world_) VGears::WorldmapModule::getSingleton().PauseForBattle();
     SetBattleModule();
     AudioManager::getSingleton().SetBattleModule();
     CameraManager::getSingleton().SetBattleModule();
     DialogsManager::getSingleton().SetBattleModule();
     EntityManager::getSingleton().SetBattleModule();
+    // The entities of the field must not be seen in the battle.
+    EntityManager::getSingleton().HideFieldEntities();
     EntityManager::getSingleton().GetBackground2D()->Hide();
     InputManager::getSingleton().SetBattleModule();
     ScriptManager::getSingleton().SetBattleModule();
@@ -143,6 +166,10 @@ void BattleManager::StartBattle(const unsigned int id){
           camera_.at(initial_camera_).location, camera_.at(initial_camera_).orientation
         );
     LoadParty();
+    LogCamera("Battle camera at the start of the battle");
+    // Something could change the camera right after the battle starts (it seems to happen in some
+    // battles started from field scripts), so it's set again in a few frames.
+    camera_check_frames_ = 3;
     //EndBattle();// TODO: Debug
 }
 
@@ -155,17 +182,24 @@ void BattleManager::EndBattle(){
     ClearBattle();
     CameraManager::getSingleton().EndBattleCamera();
 
+    // Stop the battle music, and let the music of the field continue.
+    AudioManager::getSingleton().EndBattleMusic();
     AudioManager::getSingleton().SetPreviousModule();
     CameraManager::getSingleton().SetPreviousModule();
     DialogsManager::getSingleton().SetPreviousModule();
     ScriptManager::getSingleton().ClearBattle();
     EntityManager::getSingleton().ClearBattle();
     EntityManager::getSingleton().GetBackground2D()->Show();
+    EntityManager::getSingleton().ShowFieldEntities();
     EntityManager::getSingleton().SetPreviousModule();
     InputManager::getSingleton().SetPreviousModule();
     ScriptManager::getSingleton().ClearBattle();
     ScriptManager::getSingleton().SetPreviousModule();
     UiManager::getSingleton().SetPreviousModule();
+    if (battle_from_world_){
+        battle_from_world_ = false;
+        VGears::WorldmapModule::getSingleton().ResumeAfterBattle();
+    }
 }
 
 std::vector<Enemy> BattleManager::GetEnemies() const{return enemies_;}
@@ -180,16 +214,41 @@ void BattleManager::AddEnemy(
         );
         return;
     }
-    Enemy* enemy = new Enemy(id, pos, front, visible, targeteable, active, cover);
-    enemies_.push_back(*enemy);
+    Enemy enemy(id, pos, front, visible, targeteable, active, cover);
+    Ogre::Vector3 position = enemy.GetPos();
+    const unsigned int index = enemies_.size();
+    // Enemy positions come from the formation: in normal battles they are on the negative Y side,
+    // facing the party. In back attacks they are on the positive Y side, with the party mirrored
+    // to the negative one. In pincer attacks they are on both sides, with the party in the middle.
+    int rotation = FACING_TO_PARTY;
+    switch (layout_){
+        case LAYOUT::PREEMPTIVE:
+            // Enemies are caught off guard, facing away from the party.
+            rotation = FACING_TO_ENEMIES;
+            break;
+        case LAYOUT::BACK_ATTACK:
+            // Enemies on the party's usual side, facing the other way.
+            rotation = FACING_TO_ENEMIES;
+            break;
+        case LAYOUT::PINCER_ATTACK:
+            // Enemies on both sides of the party, facing the center.
+            rotation = (position.y > 0 ? FACING_TO_ENEMIES : FACING_TO_PARTY);
+            break;
+        case LAYOUT::SIDE_ATTACK:
+            // Enemies stay in the middle, facing in a random direction.
+            rotation = (std::rand() % 2 == 0 ? FACING_TO_LEFT : FACING_TO_RIGHT);
+            break;
+        default:
+            break;
+    }
+    enemies_.push_back(enemy);
+    const std::string entity_name = enemy.GetName() + "_" + std::to_string(index);
+    enemy_entity_name_.push_back(entity_name);
     EntityManager::getSingleton().AddEntity(
-      enemy->GetName() + "_" + std::to_string(enemies_.size() - 1),
-      "enemies/" + enemy->GetModel() + ".mesh", enemy->GetPos(), Ogre::Degree(1),
+      entity_name, "enemies/" + enemy.GetModel() + ".mesh", position, Ogre::Degree(1),
       Ogre::Vector3(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE), Ogre::Quaternion(1, 1, 0, 0), id
     );
-    EntityManager::getSingleton().GetEntity(
-      enemy->GetName() + "_" + std::to_string(enemies_.size() - 1)
-    )->SetRotation(Ogre::Degree(180));
+    EntityManager::getSingleton().GetEntity(entity_name)->SetRotation(Ogre::Degree(rotation));
 }
 
 unsigned int BattleManager::ScriptGetEnemyCount() const{return enemies_.size();}
@@ -255,6 +314,79 @@ void BattleManager::SetLayout(const LAYOUT layout){
     }
     if (layout == LAYOUT::UNKNOWN_0 || layout == LAYOUT::UNKNOWN_1) layout_ = LAYOUT::NORMAL;
     else layout_ = layout;
+    // The field script can make the battle a preemptive attack (BTLMD, flag 0x04).
+    if ((battle_mode_ & 0x04) != 0 && layout_ == LAYOUT::NORMAL) layout_ = LAYOUT::PREEMPTIVE;
+}
+
+bool BattleManager::HasAttackData(const int id){
+    if (!attack_index_loaded_){
+        attack_index_ = XmlAttackFile::LoadIndex("./data/gamedata/attack/_index.xml");
+        attack_index_loaded_ = true;
+    }
+    return attack_index_.find(id) != attack_index_.end();
+}
+
+const AttackData* BattleManager::GetAttackData(const int id){
+    auto cached = attack_cache_.find(id);
+    if (cached != attack_cache_.end()) return &cached->second;
+    if (!attack_index_loaded_){
+        attack_index_ = XmlAttackFile::LoadIndex("./data/gamedata/attack/_index.xml");
+        attack_index_loaded_ = true;
+    }
+    auto file = attack_index_.find(id);
+    if (file == attack_index_.end()){
+        LOG_ERROR("No attack data for attack " + std::to_string(id));
+        return nullptr;
+    }
+    AttackData data;
+    if (!XmlAttackFile("./data/gamedata/attack/" + file->second).LoadAttack(data)) return nullptr;
+    return &(attack_cache_[id] = data);
+}
+
+int BattleManager::ScriptGetLayout() const{return static_cast<int>(layout_);}
+
+int BattleManager::GetNextFormationId() const{return next_formation_id_;}
+
+void BattleManager::ScriptSetMode(const int mode){battle_mode_ = mode;}
+
+int BattleManager::GetMode() const{return battle_mode_;}
+
+int BattleManager::GetFormationId() const{return formation_id_;}
+
+float BattleManager::GetEscapeDifficulty() const{return escape_difficulty_;}
+
+bool BattleManager::IsPreemptiveAllowed() const{return preemptive_;}
+
+void BattleManager::SetPreemptiveDisabled(const bool disabled){preemptive_ = !disabled;}
+
+bool BattleManager::ShowVictoryPose() const{return show_victory_pose_;}
+
+bool BattleManager::ShowSpoils() const{return show_spoils_;}
+
+std::string BattleManager::ScriptGetEnemyEntityName(const unsigned int index) const{
+    if (index >= enemy_entity_name_.size()) return "";
+    return enemy_entity_name_[index];
+}
+
+std::string BattleManager::ScriptGetPartyEntityName(const unsigned int index) const{
+    if (index >= party_entity_name_.size()) return "";
+    return party_entity_name_[index];
+}
+
+std::string BattleManager::ScriptGetPartyModelName(const unsigned int index) const{
+    if (index >= party_model_name_.size()) return "";
+    return party_model_name_[index];
+}
+
+std::string BattleManager::ScriptGetPartyAnimationPrefix(const unsigned int index) const{
+    if (index >= party_animation_prefix_.size()) return "";
+    return party_animation_prefix_[index];
+}
+
+void BattleManager::ScriptSetEnemyVisible(const unsigned int index, const bool visible){
+    if (index >= enemy_entity_name_.size()) return;
+    Entity* entity = EntityManager::getSingleton().GetEntity(enemy_entity_name_[index]);
+    if (entity != nullptr) entity->SetVisible(visible);
 }
 
 void BattleManager::SetFormationId(const int id){
@@ -327,6 +459,24 @@ void BattleManager::SetArenaBattle(const bool arena){
     arena_battle_ = arena;
 }
 
+int BattleManager::ScriptSetCamera(const int index){
+    if (index >= 0 && index < static_cast<int>(camera_.size())){
+        CameraManager::getSingleton().SetBattleCamera(
+          camera_.at(index).location, camera_.at(index).orientation
+        );
+        LOG_TRIVIAL(
+          "Battle camera " + std::to_string(index) + ": position ("
+          + std::to_string(camera_.at(index).location.x) + ", "
+          + std::to_string(camera_.at(index).location.y) + ", "
+          + std::to_string(camera_.at(index).location.z) + "), looking at ("
+          + std::to_string(camera_.at(index).orientation.x) + ", "
+          + std::to_string(camera_.at(index).orientation.y) + ", "
+          + std::to_string(camera_.at(index).orientation.z) + ")"
+        );
+    }
+    return camera_.size();
+}
+
 void BattleManager::SetInitialCamera(const unsigned int id){
     if (module_ != Module::BATTLE){
         LOG_ERROR("Tried to set the camera in the BattleManager, but it's not in battle mode.");
@@ -341,47 +491,105 @@ void BattleManager::LoadParty(){
         return;
     }
     std::vector<int> party = TextHandler::getSingleton().GetParty();
-    std::vector<int> positions {};
-    for (int i = 0; i < party.size(); i ++) positions.push_back(i);
-    Ogre::Vector3 position = Ogre::Vector3(0, 5, 0);
-    // Randomize party member positions.
-    std::random_shuffle(positions.begin(), positions.end());
+    // Party members actually in the battle, in party order.
+    std::vector<int> members {};
+    for (int i = 0; i < party.size(); i ++) if (party.at(i) != -1) members.push_back(i);
+
+    // Center of the enemies, used when the party surrounds them.
+    float enemy_center_x = 0.0f;
+    float enemy_center_y = -10.0f;
+    if (enemies_.size() > 0){
+        enemy_center_x = 0.0f;
+        enemy_center_y = 0.0f;
+        for (Enemy& enemy : enemies_){
+            enemy_center_x += enemy.GetPos().x;
+            enemy_center_y += enemy.GetPos().y;
+        }
+        enemy_center_x /= enemies_.size();
+        enemy_center_y /= enemies_.size();
+    }
 
+    party_entity_name_.assign(party.size(), "");
+    party_animation_prefix_.assign(party.size(), "");
+    party_model_name_.assign(party.size(), "");
     // TODO: Read this from data/gamedata/characters.xml
-    for (int i = 0; i < positions.size(); i ++){
-        if (party.at(positions.at(i)) == - 1) continue;
+    for (int slot = 0; slot < members.size(); slot ++){
+        const int index = members.at(slot);
         std::string name = "";
         std::string model = "";
-
         for (XmlBattleCharactersFile::BattleCharacter character : character_model_map_){
-            if (character.id == party.at(positions.at(i))){
+            if (character.id == party.at(index)){
                 model = "characters/" + character.models.at(0).model;
                 name = "party_" + character.models.at(0).name;
                 break;
             }
         }
         if (model == ""){
-            LOG_ERROR(
-              "No 3D model assigned to battle character "
-              + std::to_string(party.at(positions.at(i)))
-            );
+            LOG_ERROR("No 3D model assigned to battle character " + std::to_string(party.at(index)));
             return;
         }
+        const float line_x = (slot - (members.size() - 1) / 2.0f) * PARTY_SPACING;
+        Ogre::Vector3 position(line_x, PARTY_Y, 0);
+        int rotation = FACING_TO_ENEMIES;
+        switch (layout_){
+            case LAYOUT::BACK_ATTACK:
+                // Enemies come from behind: the party is on the other side of the field, facing
+                // away from them.
+                position.y = -PARTY_Y;
+                break;
+            case LAYOUT::SIDE_ATTACK:
+                // Party members alternate on both sides of the enemies, facing the center.
+                position.x = enemy_center_x + (slot % 2 == 0 ? -1 : 1) * SIDE_DISTANCE;
+                position.y = enemy_center_y - 2 * (slot / 2);
+                rotation = (slot % 2 == 0 ? FACING_TO_RIGHT : FACING_TO_LEFT);
+                break;
+            case LAYOUT::PINCER_ATTACK:
+                // Party in the middle, between the two enemy groups, facing one of them.
+                position.y = 0;
+                rotation = (std::rand() % 2 == 0 ? FACING_TO_ENEMIES : FACING_TO_PARTY);
+                break;
+            default:
+                break;
+        }
         EntityManager::getSingleton().AddEntity(
           name, model, position, Ogre::Degree(0),
           Ogre::Vector3(PARTY_SCALE, PARTY_SCALE, PARTY_SCALE),
-          Ogre::Quaternion(1, 1, 0, 0), 100 + i
+          Ogre::Quaternion(1, 1, 0, 0), 100 + slot
         );
-
-        // Next position in Y axis
-        position.x += ((i + 1) * (i % 2 == 0 ? 5 : -5));
-
+        EntityManager::getSingleton().GetEntity(name)->SetRotation(Ogre::Degree(rotation));
+        party_entity_name_[index] = name;
+        {
+            std::string base = model.substr(model.find_last_of("/\\") + 1);
+            party_model_name_[index] = base.substr(0, base.find_last_of('.'));
+        }
+        party_animation_prefix_[index] = model.substr(model.find_last_of("/\\") + 1).substr(0, 2);
     }
 }
 
 void BattleManager::UpdateField(){}
 
+void BattleManager::LogCamera(const std::string& title) const{
+    Ogre::Camera* camera = CameraManager::getSingleton().GetCurrentCamera();
+    if (camera == nullptr) return;
+    const Ogre::Vector3 position = camera->getPosition();
+    const Ogre::Vector3 direction = camera->getDirection();
+    LOG_TRIVIAL(
+      title + ": position (" + std::to_string(position.x) + ", " + std::to_string(position.y) + ", "
+      + std::to_string(position.z) + "), direction (" + std::to_string(direction.x) + ", "
+      + std::to_string(direction.y) + ", " + std::to_string(direction.z) + "), fov "
+      + std::to_string(camera->getFOVy().valueDegrees()) + " degrees"
+    );
+}
+
 void BattleManager::UpdateBattle(){
+    if (camera_check_frames_ > 0){
+        camera_check_frames_ --;
+        if (camera_check_frames_ == 0 && camera_.size() > 0){
+            LogCamera("Battle camera three frames after the start");
+            ScriptSetCamera(initial_camera_ < camera_.size() ? initial_camera_ : 0);
+            LogCamera("Battle camera set again");
+        }
+    }
     ScriptManager::getSingleton().RunString("UiContainer.BattleUi:tick()");
 }
 

+ 251 - 0
src/core/BattleManager.h

@@ -21,6 +21,7 @@
 #include "Manager.h"
 #include "core/XmlBattleCharactersFile.h"
 #include "core/XmlBattleScenesFile.h"
+#include "core/XmlAttackFile.h"
 
 /**
  * The battle manager.
@@ -293,6 +294,163 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         void SetLayout(const LAYOUT layout);
 
+        /**
+         * Retrieves the battle layout.
+         *
+         * To be called from Lua scripts.
+         *
+         * @return The layout, as in {@see LAYOUT}, as an integer.
+         */
+        int ScriptGetLayout() const;
+
+        /**
+         * Retrieves the formation ID of the next battle.
+         *
+         * To be called from Lua scripts.
+         *
+         * @return The next battle formation ID, -1 if there is none.
+         */
+        int GetNextFormationId() const;
+
+        /**
+         * Sets the battle mode flags for the next battles (the BTLMD field opcode).
+         *
+         * To be called from Lua scripts. The flags are: 0x80 no results screens, 0x40 arena, 0x20 no
+         * victory music, 0x08 can't escape, 0x04 preemptive attack, 0x02 timed battle and 0x100
+         * disable game over. They are kept until they are set again or the field changes.
+         *
+         * @param[in] mode The flags.
+         */
+        void ScriptSetMode(const int mode);
+
+        /**
+         * Retrieves the battle mode flags.
+         *
+         * To be called from Lua scripts.
+         *
+         * @return The flags, as in {@see ScriptSetMode}.
+         */
+        int GetMode() const;
+
+        /**
+         * Retrieves the ID of the current battle formation.
+         *
+         * To be called from Lua scripts.
+         *
+         * @return The formation ID, -1 if there is none.
+         */
+        int GetFormationId() const;
+
+        /**
+         * Retrieves the escape difficulty of the battle.
+         *
+         * To be called from Lua scripts.
+         *
+         * @return Values between 0 and 1 (both included), from easy to hard. -1 if the battle
+         * can't be escaped from.
+         */
+        float GetEscapeDifficulty() const;
+
+        /**
+         * Checks if the battle can start as preemptive.
+         *
+         * @return False if preemptive attacks are disabled for this battle.
+         */
+        bool IsPreemptiveAllowed() const;
+
+        /**
+         * Sets if preemptive attacks are disabled for the battle.
+         *
+         * @param[in] disabled True to disable preemptive attacks.
+         */
+        void SetPreemptiveDisabled(const bool disabled);
+
+        /**
+         * Checks if the victory pose must be shown.
+         *
+         * To be called from Lua scripts.
+         */
+        bool ShowVictoryPose() const;
+
+        /**
+         * Checks if the spoils screens must be shown.
+         *
+         * To be called from Lua scripts.
+         */
+        bool ShowSpoils() const;
+
+        /**
+         * Shows or hides an enemy model.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index The enemy index, 0 based.
+         * @param[in] visible True to show the enemy model, false to hide it.
+         */
+        void ScriptSetEnemyVisible(const unsigned int index, const bool visible);
+
+        /**
+         * Retrieves the name of the entity of an enemy.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index The enemy index, 0 based.
+         * @return The entity name, empty if the index is not valid.
+         */
+        std::string ScriptGetEnemyEntityName(const unsigned int index) const;
+
+        /**
+         * Retrieves the name of the entity of a party member.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index The party position, 0 based (the same as in the party).
+         * @return The entity name, empty if there is no one in that position.
+         */
+        std::string ScriptGetPartyEntityName(const unsigned int index) const;
+
+        /**
+         * Retrieves the prefix used in the animation names of a party member model.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index The party position, 0 based (the same as in the party).
+         * @return The animation prefix (the model name first two letters), empty if there is no
+         * one in that position.
+         */
+        std::string ScriptGetPartyAnimationPrefix(const unsigned int index) const;
+
+        /**
+         * Retrieves the model name of a party member, without path or extension (as in
+         * "rt_cloud").
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index The party position, 0 based (the same as in the party).
+         * @return The model name, empty if there is no one in that position.
+         */
+        std::string ScriptGetPartyModelName(const unsigned int index) const;
+
+        /**
+         * Retrieves the data of an attack.
+         *
+         * Attack files are read the first time they are needed.
+         *
+         * @param[in] id Attack ID.
+         * @return The attack data, nullptr if there is no such attack.
+         */
+        const AttackData* GetAttackData(const int id);
+
+        /**
+         * Checks if there is attack data for an attack, without logging an error if there isn't.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] id Attack ID.
+         * @return True if there is an attack file for the attack.
+         */
+        bool HasAttackData(const int id);
+
         /**
          * Sets the battle formation ID.
          *
@@ -354,6 +512,17 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         void SetInitialCamera(const unsigned int id);
 
+        /**
+         * Changes the battle camera to one of the cameras of the formation.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] index Camera index, 0 based.
+         * @return The number of cameras of the formation. The camera isn't changed if the index is
+         * not valid.
+         */
+        int ScriptSetCamera(const int index);
+
         /**
          * Adds a camera for the next battle.
          *
@@ -372,6 +541,22 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         void LoadParty();
 
+        /**
+         * Logs the state of the camera.
+         *
+         * @param[in] title Text to start the message with.
+         */
+        void LogCamera(const std::string& title) const;
+
+        /**
+         * Model rotations, in degrees, to face the party, the enemies, or the left or right side
+         * of the battle camera.
+         */
+        static const int FACING_TO_PARTY;
+        static const int FACING_TO_ENEMIES;
+        static const int FACING_TO_LEFT;
+        static const int FACING_TO_RIGHT;
+
         /**
          * Updates the manager while in the field.
          *
@@ -384,6 +569,16 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         void UpdateBattle() override;
 
+        /**
+         * Frames left until the initial battle camera is checked and set again.
+         */
+        int camera_check_frames_ = 0;
+
+        /**
+         * Battle mode flags, set by the field scripts. {@see ScriptSetMode}
+         */
+        int battle_mode_ = 0;
+
         /**
          * Updates manager while in the world map.
          *
@@ -391,6 +586,21 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         void UpdateWorld() override;
 
+        /**
+         * Depth (Y coordinate) of the party in normal battles.
+         */
+        static const float PARTY_Y;
+
+        /**
+         * Distance between party members.
+         */
+        static constexpr float PARTY_SPACING = 6.0f;
+
+        /**
+         * Distance from the center to each side in side attacks.
+         */
+        static const float SIDE_DISTANCE;
+
         /**
          * Scale factor for battle enemy models.
          */
@@ -462,6 +672,11 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         bool show_spoils_;
 
+        /**
+         * True if the current battle started in the world map.
+         */
+        bool battle_from_world_ = false;
+
         /**
          * Indicates if the battle can be preemptive (not if it will definitely be).
          */
@@ -482,6 +697,42 @@ class BattleManager : public Manager, public Ogre::Singleton<BattleManager>{
          */
         std::vector<Enemy> enemies_;
 
+        /**
+         * Names of the entities of the enemies in the battle, in the same order as the enemies.
+         */
+        std::vector<std::string> enemy_entity_name_;
+
+        /**
+         * Names of the entities of the party members, by party position. Empty if no one is in
+         * that position.
+         */
+        std::vector<std::string> party_entity_name_;
+
+        /**
+         * Animation prefixes of the party members models, by party position.
+         */
+        std::vector<std::string> party_animation_prefix_;
+
+        /**
+         * Model names of the party members, by party position, without path or extension.
+         */
+        std::vector<std::string> party_model_name_;
+
+        /**
+         * File names of the attacks by ID, loaded the first time an attack is requested.
+         */
+        std::map<int, std::string> attack_index_;
+
+        /**
+         * Indicates if {@see attack_index_} has been loaded.
+         */
+        bool attack_index_loaded_ = false;
+
+        /**
+         * Attacks already read.
+         */
+        std::map<int, AttackData> attack_cache_;
+
         /**
          * Map of playable character models.
          */

+ 30 - 6
src/core/CameraManager.cpp

@@ -195,12 +195,11 @@ void CameraManager::StartBattleCamera(
       camera_->getOrientation().w, camera_->getOrientation().x,
       camera_->getOrientation().y, camera_->getOrientation().z
     );
-    camera_->setPosition(position);
-    //CameraManager::getSingleton().GetCurrentCamera()->setFixedYawAxis(true, Ogre::Vector3::UNIT_Y);
-    camera_->lookAt(orientation);
-    camera_->roll(Ogre::Angle(90));
-    // Fix: compensate for the battle UI covered section of the screen.
-    camera_->move(Ogre::Vector3(0, 0, -4));
+    // The field camera has its own field of view and a frustum moved by the 2D scroll. The battle
+    // camera needs the default ones, and a known orientation: lookAt depends on the current
+    // orientation, so the camera would look different depending on where the battle started.
+    fov_backup_ = camera_->getFOVy();
+    SetBattleCamera(position, orientation);
 
     /*Ogre::SceneNode* rootScene =
               Ogre::Root::getSingleton().getSceneManager("Scene") ->getRootSceneNode();
@@ -218,6 +217,18 @@ void CameraManager::StartBattleCamera(
       << rootScene->getOrientation().y << ", " << rootScene->getOrientation().z << std::endl;*/
 }
 
+void CameraManager::SetBattleCamera(const Ogre::Vector3 position, const Ogre::Vector3 orientation){
+    camera_->resetFrustumExtents();
+    camera_->setFOVy(Ogre::Radian(Ogre::Math::PI / 4.0f));
+    camera_->setOrientation(Ogre::Quaternion::IDENTITY);
+    camera_->setPosition(position);
+    //CameraManager::getSingleton().GetCurrentCamera()->setFixedYawAxis(true, Ogre::Vector3::UNIT_Y);
+    camera_->lookAt(orientation);
+    camera_->roll(Ogre::Angle(90));
+    // Fix: compensate for the battle UI covered section of the screen.
+    camera_->move(Ogre::Vector3(0, 0, -4));
+}
+
 void CameraManager::EndBattleCamera(){
     /*std::cout << "CAMERA BATTLE END: " << camera_->getPosition().x << ", "
       << camera_->getPosition().y << ", " << camera_->getPosition().z << ", "
@@ -229,6 +240,9 @@ void CameraManager::EndBattleCamera(){
     CameraManager::getSingleton().GetCurrentCamera()->setOrientation(Ogre::Quaternion(
       orientation_backup_.w, orientation_backup_.x, orientation_backup_.y, orientation_backup_.z
     ));
+    // Back to the field of view and 2D scroll of the field.
+    camera_->setFOVy(fov_backup_);
+    Set2DScroll(d2_scroll_);
     /*std::cout << "CAMERA BATTLE END FINISH: " << camera_->getPosition().x << ", "
       << camera_->getPosition().y << ", " << camera_->getPosition().z << ", "
       << camera_->getOrientation().w << ", "<< camera_->getOrientation().x << ", "
@@ -267,6 +281,16 @@ const Ogre::Vector3 CameraManager::ProjectPointToScreen(const Ogre::Vector3& poi
     return view;
 }
 
+std::tuple<float, float, float> CameraManager::ScriptProjectToUi(
+  const float x, const float y, const float z
+){
+    if (camera_ == nullptr || viewport_ == nullptr) return std::make_tuple(0.0f, 0.0f, 0.0f);
+    const Ogre::Vector3 point = ProjectPointToScreen(Ogre::Vector3(x, y, z));
+    // Pixels to UI units.
+    const float scale = 720.0f / static_cast<float>(viewport_->getActualHeight());
+    return std::make_tuple(point.x * scale, point.y * scale, point.z);
+}
+
 Ogre::Camera* CameraManager::GetCurrentCamera(){return camera_;}
 
 Ogre::Viewport* CameraManager::getViewport(){return viewport_;}

+ 30 - 0
src/core/CameraManager.h

@@ -155,6 +155,31 @@ class CameraManager : public Manager, public Ogre::Singleton<CameraManager>{
          */
         const Ogre::Vector3 ProjectPointToScreen(const Ogre::Vector3& point);
 
+        /**
+         * Sets the battle camera.
+         *
+         * Unlike {@see StartBattleCamera}, it doesn't save the state of the field camera, so it
+         * can be used to change the camera during a battle.
+         *
+         * @param[in] position Camera position.
+         * @param[in] orientation Point the camera looks at.
+         */
+        void SetBattleCamera(const Ogre::Vector3 position, const Ogre::Vector3 orientation);
+
+        /**
+         * Projects a point of the scene into the coordinates of the UI.
+         *
+         * To be called from Lua scripts. UI units are the ones used by the UI widgets: 720 units
+         * are the height of the screen, with the origin at the top left corner.
+         *
+         * @param[in] x X coordinate of the point.
+         * @param[in] y Y coordinate of the point.
+         * @param[in] z Z coordinate of the point.
+         * @return X and Y in UI units, and the depth of the point (negative if it's behind the
+         * camera).
+         */
+        std::tuple<float, float, float> ScriptProjectToUi(const float x, const float y, const float z);
+
         /**
          * Retrieves the camera.
          *
@@ -244,6 +269,11 @@ class CameraManager : public Manager, public Ogre::Singleton<CameraManager>{
          */
         Ogre::Vector3 position_backup_;
 
+        /**
+         * Field of view of the camera before a battle, restored when the battle ends.
+         */
+        Ogre::Radian fov_backup_;
+
         /**
          * A backup of the field or world camera orientation for when the battle camera is active.
          */

+ 62 - 2
src/core/Enemy.cpp

@@ -243,12 +243,72 @@ void Enemy::SetCover(std::string cover){cover_ = cover;}
 int Enemy::ScriptGetAttackCount(){return attacks_.size();}
 
 int Enemy::ScriptGetAttack(const unsigned int index){
-    if (attacks_.size() < index) return -1;
+    if (index >= attacks_.size()) return -1;
     return attacks_[index].id;
 }
 
+int Enemy::ScriptGetAnimationCount() const{return animations_.size();}
+
+int Enemy::ScriptGetAnimation(const unsigned int index) const{
+    if (index >= animations_.size()) return -1;
+    return animations_[index];
+}
+
+std::string Enemy::ScriptGetModelPrefix() const{return model_.substr(0, 2);}
+
+int Enemy::ScriptGetAttackAnimation(const unsigned int index) const{
+    if (index >= attacks_.size()) return -1;
+    return attacks_[index].animation;
+}
+
+int Enemy::ScriptGetElementCount() const{return elements_.size();}
+
+void Enemy::SetAiScript(const unsigned int slot, const std::string& code){
+    ai_scripts_[slot] = code;
+}
+
+std::string Enemy::ScriptGetAiScript(const unsigned int slot) const{
+    auto found = ai_scripts_.find(slot);
+    if (found == ai_scripts_.end()) return "";
+    return found->second;
+}
+
+int Enemy::ScriptGetImmunityCount() const{return immunities_.size();}
+
+int Enemy::ScriptGetImmunity(const unsigned int index) const{
+    if (index >= immunities_.size()) return -1;
+    return immunities_[index].status;
+}
+
+float Enemy::ScriptGetImmunityRate(const unsigned int index) const{
+    if (index >= immunities_.size()) return 0.0f;
+    return immunities_[index].rate;
+}
+
+int Enemy::ScriptGetElement(const unsigned int index) const{
+    if (index >= elements_.size()) return -1;
+    return elements_[index].id;
+}
+
+float Enemy::ScriptGetElementFactor(const unsigned int index) const{
+    if (index >= elements_.size()) return 1.0f;
+    return elements_[index].factor;
+}
+
+int Enemy::ScriptGetDropCount() const{return drop_.size();}
+
+int Enemy::ScriptGetDropId(const unsigned int index) const{
+    if (index >= drop_.size()) return -1;
+    return drop_[index].id;
+}
+
+float Enemy::ScriptGetDropRate(const unsigned int index) const{
+    if (index >= drop_.size()) return 0.0f;
+    return drop_[index].rate;
+}
+
 int Enemy::ScriptGetCameraForAttack(const unsigned int index){
-    if (attacks_.size() < index) return -1;
+    if (index >= attacks_.size()) return -1;
     return attacks_[index].camera;
 }
 

+ 152 - 0
src/core/Enemy.h

@@ -15,6 +15,7 @@
 
 #pragma once
 
+#include <map>
 #include <tuple>
 #include <OgreString.h>
 #include "ScriptManager.h"
@@ -63,6 +64,11 @@ class Enemy{
              * -1 to not move the camera.
              */
             int camera;
+
+            /**
+             * Animation script of the model to play for the attack. -1 if there is none.
+             */
+            int animation = -1;
         };
 
         /**
@@ -645,6 +651,147 @@ class Enemy{
          */
         int ScriptGetAttackCount();
 
+        /**
+         * Retrieves the number of items the enemy can drop.
+         *
+         * To be used from Lua scripts.
+         *
+         * @return The number of droppable items.
+         */
+        int ScriptGetDropCount() const;
+
+        /**
+         * Retrieves the animation script of an attack.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Attack index, lower than {@see ScriptGetAttackCount}.
+         * @return The animation script number of the model, -1 if there is none.
+         */
+        int ScriptGetAttackAnimation(const unsigned int index) const;
+
+        /**
+         * Retrieves the number of elements the enemy reacts to.
+         *
+         * To be used from Lua scripts.
+         */
+        int ScriptGetElementCount() const;
+
+        /**
+         * Sets an AI script.
+         *
+         * @param[in] slot Script slot (0 initialization, 1 main, 2 general counter, 3 death
+         * counter, 4 physical counter, 5 magic counter, 6 battle end, 7 pre-turn, 8 to 15 events).
+         * @param[in] code The script bytecode.
+         */
+        void SetAiScript(const unsigned int slot, const std::string& code);
+
+        /**
+         * Retrieves an AI script.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] slot Script slot, as in {@see SetAiScript}.
+         * @return The script bytecode, empty if the enemy has no script in that slot.
+         */
+        std::string ScriptGetAiScript(const unsigned int slot) const;
+
+        /**
+         * Retrieves the number of status immunities of the enemy.
+         *
+         * To be used from Lua scripts.
+         */
+        int ScriptGetImmunityCount() const;
+
+        /**
+         * Retrieves the status ID of an immunity (0 is death, 1 near death, 2 sleep, 3 poison...).
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Index, lower than {@see ScriptGetImmunityCount}.
+         * @return The status ID, -1 if the index is not valid.
+         */
+        int ScriptGetImmunity(const unsigned int index) const;
+
+        /**
+         * Retrieves the rate of an immunity.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Index, lower than {@see ScriptGetImmunityCount}.
+         * @return The immunity rate, between 0 and 1. 0 if the index is not valid.
+         */
+        float ScriptGetImmunityRate(const unsigned int index) const;
+
+        /**
+         * Retrieves the element index (0 is fire, 1 ice, 2 lightning...) of an element reaction.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Index, lower than {@see ScriptGetElementCount}.
+         * @return The element, -1 if the index is not valid.
+         */
+        int ScriptGetElement(const unsigned int index) const;
+
+        /**
+         * Retrieves the damage factor of an element reaction.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Index, lower than {@see ScriptGetElementCount}.
+         * @return The factor: 1 normal, 2 double, 0.5 half, 0 nullified, negative for absorbed.
+         * 1 if the index is not valid.
+         */
+        float ScriptGetElementFactor(const unsigned int index) const;
+
+        /**
+         * Retrieves the number of attack animations of the enemy.
+         *
+         * To be used from Lua scripts.
+         *
+         * @return The number of attack animations.
+         */
+        int ScriptGetAnimationCount() const;
+
+        /**
+         * Retrieves an attack animation index of the enemy.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Index in the enemy animation list, lower than
+         * {@see ScriptGetAnimationCount}.
+         * @return The animation number in the model (as in "XX_NN"), -1 if the index is not valid.
+         */
+        int ScriptGetAnimation(const unsigned int index) const;
+
+        /**
+         * Retrieves the model prefix used in the animation names (the first two characters of the
+         * model name).
+         *
+         * To be used from Lua scripts.
+         */
+        std::string ScriptGetModelPrefix() const;
+
+        /**
+         * Retrieves the ID of a droppable item.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Drop index, must be lower than reported by {@see ScriptGetDropCount}.
+         * @return The item ID, -1 if the index is not valid.
+         */
+        int ScriptGetDropId(const unsigned int index) const;
+
+        /**
+         * Retrieves the drop rate of a droppable item.
+         *
+         * To be used from Lua scripts.
+         *
+         * @param[in] index Drop index, must be lower than reported by {@see ScriptGetDropCount}.
+         * @return The drop rate, between 0 and 1. 0 if the index is not valid.
+         */
+        float ScriptGetDropRate(const unsigned int index) const;
+
         /**
          * Retrieves an attack.
          *
@@ -720,6 +867,11 @@ class Enemy{
          */
         std::vector<Element> elements_;
 
+        /**
+         * AI scripts by slot, as bytecode.
+         */
+        std::map<unsigned int, std::string> ai_scripts_;
+
         /**
          * Status immunities and resistances.
          */

+ 19 - 7
src/core/Entity.cpp

@@ -13,6 +13,7 @@
  * GNU General Public License for more details.
  */
 
+#include <cmath>
 #include <OgreSceneNode.h>
 #include <OgreMaterialManager.h>
 #include "core/ConfigVar.h"
@@ -71,6 +72,8 @@ Entity::Entity(const Ogre::String& name, Ogre::SceneNode* node):
   animation_default_("Idle"),
   animation_current_name_(""),
   animation_auto_play_(true),
+  animation_state_(Entity::AUTO_ANIMATION),
+  animation_play_type_(Entity::PLAY_LOOPED),
   is_character_(false),
   character_id_(-1),
   is_line_(false)
@@ -280,13 +283,14 @@ void Entity::ScriptSetRotation(const float rotation){SetRotation(Ogre::Degree(ro
 
 Ogre::Degree Entity::GetRotation() const{
     assert(model_root_node_);
-    Ogre::Quaternion q = model_root_node_->getOrientation();
-    Ogre::Degree temp;
-    // On battle module, rotate on Y axis.
-    Ogre::Vector3 vec = EntityManager::getSingleton().IsBattleModule()
-      ? Ogre::Vector3::UNIT_Z : Ogre::Vector3::UNIT_Z;
-    q.ToAngleAxis(temp, vec);
-    return temp;
+    // The rotation is around the Y axis in battle, around Z in the rest of modules (see
+    // {@see SetRotation}), so the angle is twice the angle of the component of that axis.
+    const Ogre::Quaternion q = model_root_node_->getOrientation();
+    const float axis = EntityManager::getSingleton().IsBattleModule() ? q.y : q.z;
+    float angle = Ogre::Degree(Ogre::Radian(2.0f * std::atan2(axis, q.w))).valueDegrees();
+    // Same range as set by SetRotation.
+    angle = angle - Ogre::Math::Floor(angle / 360.0f) * 360.0f;
+    return Ogre::Degree(angle);
 }
 
 float Entity::ScriptGetRotation() const{return GetRotation().valueDegrees();}
@@ -719,6 +723,14 @@ void Entity::ScriptPlayAnimationStop(const char* name, const float start, const
     PlayAnimation(Ogre::String(name), Entity::REQUESTED_ANIMATION, Entity::PLAY_ONCE, start, end);
 }
 
+float Entity::GetAnimationLength(const Ogre::String& animation) const{return 0.0f;}
+
+unsigned int Entity::GetAnimationCount() const{return 0;}
+
+float Entity::ScriptGetAnimationLength(const char* animation) const{
+    return GetAnimationLength(Ogre::String(animation));
+}
+
 void Entity::ScriptSetDefaultAnimation(const char* animation){
     animation_default_ = Ogre::String(animation);
 }

+ 26 - 0
src/core/Entity.h

@@ -441,6 +441,32 @@ class Entity{
          */
         virtual bool IsVisible() const = 0;
 
+        /**
+         * Retrieves the duration of an animation.
+         *
+         * @param[in] animation Animation name.
+         * @return The animation length in seconds, 0 if the animation doesn't exist or the entity
+         * has no animations.
+         */
+        virtual float GetAnimationLength(const Ogre::String& animation) const;
+
+        /**
+         * Retrieves the number of animations of the entity.
+         *
+         * @return The number of animations, 0 if the entity has no animations.
+         */
+        virtual unsigned int GetAnimationCount() const;
+
+        /**
+         * Retrieves the duration of an animation.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] animation Animation name.
+         * @return The animation length in seconds, 0 if it doesn't exist.
+         */
+        float ScriptGetAnimationLength(const char* animation) const;
+
         /**
          * Sets the entity's state.
          *

+ 155 - 2
src/core/EntityManager.cpp

@@ -13,6 +13,8 @@
  * GNU General Public License for more details.
  */
 
+#include <algorithm>
+#include <cstdlib>
 #include <iostream>
 #include <cmath>
 #include <limits>
@@ -37,6 +39,24 @@ template<>EntityManager *Ogre::Singleton<EntityManager>::msSingleton = nullptr;
 
 ConfigVar cv_debug_grid("debug_grid", "Draw debug grid", "false");
 ConfigVar cv_debug_axis("debug_axis", "Draw debug axis", "false");
+ConfigVar cv_encounter_multiplier(
+  "encounter_multiplier", "Multiplier of the random encounter rate (0 disables them)", "1"
+);
+ConfigVar cv_debug_encounters("debug_encounters", "Log random encounter checks", "false");
+
+/**
+ * Frames of walking (at 30 frames per second) between two encounter checks. The original game
+ * checks every 8 frames.
+ */
+static const float ENCOUNTER_CHECK_FRAMES = 8.0f;
+
+/**
+ * Value added to the encounter accumulator in each check, before dividing it by the rate of the
+ * table. The original game uses the speed of the player, which is not known here, so this is tuned
+ * to a battle every 15-30 seconds of walking in most fields. The encounter_multiplier cvar scales
+ * it.
+ */
+static const float ENCOUNTER_STEP_VALUE = 5.0f;
 
 const float EntityManager::SCENE_SCALE = 0.003f;
 
@@ -109,7 +129,12 @@ EntityManager::EntityManager():
   player_lock_(false),
   player_run_(false),
   random_encounters_(false),
-  encounter_rate_(0)
+  encounter_rate_(0),
+  encounter_table_(1),
+  encounter_distance_(0.0f),
+  encounter_last_position_(Ogre::Vector3::ZERO),
+  encounter_last_position_valid_(false),
+  encounter_safe_steps_(0)
 {
     LOG_TRIVIAL("EntityManager created.");
     scene_node_ = Ogre::Root::getSingleton().getSceneManager("Scene")
@@ -178,6 +203,8 @@ void EntityManager::Input(const VGears::Event& event){
 void EntityManager::UpdateField(){
     // Update all entity scripts
     ScriptManager::getSingleton().Update(ScriptManager::ENTITY);
+    // The player is walking if there is a move set by the input.
+    const bool walked = player_entity_ != nullptr && player_move_ != Ogre::Vector3::ZERO;
 
     // Set move point for player entity
     if (player_entity_ != nullptr && player_move_ != Ogre::Vector3::ZERO){
@@ -294,6 +321,8 @@ void EntityManager::UpdateField(){
     player_run_ = false;
     if (background_2d_.GetScrollType() != Background2D::NONE) SetNextScrollStep();
     background_2d_.Update();
+    // This can start a battle and change the module, so it's the last thing to do.
+    UpdateEncounters(walked);
     screen_fade_.Update(Timer::getSingleton().GetGameTimeDelta());
 }
 
@@ -305,7 +334,11 @@ void EntityManager::UpdateBattle(){
     }
     for (unsigned int i = 0; i < battle_entity_.size(); ++ i){
         battle_entity_[i]->Update();
-        battle_entity_[i]->PlayAnimationContinue(battle_entity_[i]->GetDefaultAnimationName());
+        // Go back to the default animation, unless an animation has been requested. Requested
+        // animations that are not looped go back to the default one by themselves when they end.
+        if (battle_entity_[i]->GetAnimationState() != Entity::REQUESTED_ANIMATION){
+            battle_entity_[i]->PlayAnimationContinue(battle_entity_[i]->GetDefaultAnimationName());
+        }
     }
     //CameraManager::getSingleton().GetCurrentCamera()->roll(Ogre::Radian(0.01));
 }
@@ -456,6 +489,8 @@ void EntityManager::UpdateDebug(){
 void EntityManager::OnResize(){background_2d_.OnResize();}
 
 void EntityManager::ClearField(){
+    ClearEncounterTables();
+    entities_hidden_by_battle_.clear();
     walkmesh_.Clear();
     background_2d_.Clear();
     screen_fade_.Clear();
@@ -736,6 +771,8 @@ void EntityManager::ScriptPlayerLock(const bool lock){
     if (lock == true) player_move_ = Ogre::Vector3::ZERO;
 }
 
+bool EntityManager::IsPlayerLocked() const{return player_lock_;}
+
 void EntityManager::SetPlayerMoveRotation(const Ogre::Radian rotation){
     if (IsBattleModule()){
         LOG_WARNING("Tried to set player move rotation the EntityManager is in battle mode.");
@@ -746,10 +783,126 @@ void EntityManager::SetPlayerMoveRotation(const Ogre::Radian rotation){
 
 bool EntityManager::GetRandomEncounters(){return random_encounters_;}
 
+float EntityManager::GetEncounterMultiplier() const{return cv_encounter_multiplier.GetF();}
+
+bool EntityManager::IsDebugEncounters() const{return cv_debug_encounters.GetB();}
+
 void EntityManager::SetRandomEncounters(bool active){random_encounters_ = active;}
 
 float EntityManager::GetEncounterRate(){return encounter_rate_;}
 
+void EntityManager::HideFieldEntities(){
+    entities_hidden_by_battle_.clear();
+    for (Entity* entity : entity_){
+        if (entity->IsVisible()){
+            entities_hidden_by_battle_.push_back(entity);
+            entity->SetVisible(false);
+        }
+    }
+}
+
+void EntityManager::ShowFieldEntities(){
+    // Only the ones that still exist: the field could have been changed during the battle.
+    for (Entity* hidden : entities_hidden_by_battle_){
+        if (std::find(entity_.begin(), entity_.end(), hidden) != entity_.end()) hidden->SetVisible(true);
+    }
+    entities_hidden_by_battle_.clear();
+}
+
+void EntityManager::ClearEncounterTables(){
+    encounter_tables_.clear();
+    encounter_table_ = 1;
+    encounter_distance_ = 0.0f;
+    encounter_last_position_valid_ = false;
+    encounter_accumulator_ = 0;
+    encounter_last_battle_ = -1;
+}
+
+void EntityManager::AddEncounterTable(
+  const int id, const int rate, const std::vector<std::pair<int, int>>& battles,
+  const std::vector<std::pair<int, int>>& specials
+){
+    EncounterTable table;
+    table.rate = rate;
+    table.battles = battles;
+    table.specials = specials;
+    encounter_tables_[id] = table;
+}
+
+void EntityManager::ScriptSetEncounterTable(const int id){encounter_table_ = id;}
+
+int EntityManager::ScriptRollEncounterBattle() const{
+    auto found = encounter_tables_.find(encounter_table_);
+    if (found == encounter_tables_.end()) return -1;
+    const EncounterTable& table = found->second;
+    // Like the original game: a roll out of 64 is checked against the special battles one after
+    // the other (each one adds its probability). If none is hit, a new roll chooses a standard
+    // battle, the last one being the default.
+    int roll = std::rand() % 64;
+    int sum = 0;
+    for (const auto& special : table.specials){
+        sum += special.second;
+        if (roll < sum) return special.first;
+    }
+    if (table.battles.empty()) return -1;
+    roll = std::rand() % 64;
+    sum = 0;
+    for (const auto& battle : table.battles){
+        sum += battle.second;
+        if (roll < sum) return battle.first;
+    }
+    return table.battles.back().first;
+}
+
+void EntityManager::UpdateEncounters(const bool walked){
+    if (player_entity_ == nullptr){
+        encounter_last_position_valid_ = false;
+        return;
+    }
+    const Ogre::Vector3 position = player_entity_->GetPosition();
+    const float distance
+      = encounter_last_position_valid_ ? (position - encounter_last_position_).length() : 0.0f;
+    encounter_last_position_ = position;
+    encounter_last_position_valid_ = true;
+    // Only walking counts: not scripted movements, and not jumps to another place.
+    if (!walked || player_lock_ || distance > 2.0f) return;
+    if (!random_encounters_ || encounter_tables_.empty()) return;
+    auto found = encounter_tables_.find(encounter_table_);
+    if (found == encounter_tables_.end()) return;
+    const float multiplier = cv_encounter_multiplier.GetF();
+    if (multiplier <= 0.0f) return;
+    // Same as the original game. Every few frames of walking the accumulator grows by a value
+    // divided by the rate (the lower the rate, the more often the encounters), and an encounter
+    // starts if a random number is lower than the accumulator.
+    const int rate = found->second.rate;
+    if (rate <= 0) return;
+    encounter_distance_ += Timer::getSingleton().GetGameTimeDelta() * 30.0f;
+    while (encounter_distance_ >= ENCOUNTER_CHECK_FRAMES){
+        encounter_distance_ -= ENCOUNTER_CHECK_FRAMES;
+        encounter_accumulator_ += static_cast<int>(ENCOUNTER_STEP_VALUE * multiplier * 256.0f) / rate;
+        const int roll = std::rand() % 256 * 256;
+        if (cv_debug_encounters.GetB()){
+            LOG_TRIVIAL(
+              "Encounter check: rate " + std::to_string(rate) + ", accumulator "
+              + std::to_string(encounter_accumulator_ / 256.0f) + "/256, roll "
+              + std::to_string(roll / 256)
+            );
+        }
+        if (roll >= encounter_accumulator_) continue;
+        int battle = ScriptRollEncounterBattle();
+        // The same battle is not repeated: it's rolled again once.
+        if (battle == encounter_last_battle_) battle = ScriptRollEncounterBattle();
+        if (battle < 0) return;
+        encounter_distance_ = 0.0f;
+        encounter_accumulator_ = 0;
+        encounter_last_battle_ = battle;
+        LOG_TRIVIAL("Random encounter: battle " + std::to_string(battle));
+        // The battle is started by a script, as it has to change the module of every manager.
+        ScriptManager::getSingleton().RunString("Encounter.start(" + std::to_string(battle) + ")");
+        return;
+    }
+}
+
 void EntityManager::SetEncounterRate(float rate){
     if (rate >= 1.0f) encounter_rate_ = 1.0f;
     else if (rate <= 0.0f) encounter_rate_ = 0.0f;

+ 146 - 0
src/core/EntityManager.h

@@ -15,6 +15,9 @@
 
 #pragma once
 
+#include <map>
+#include <utility>
+#include <vector>
 #include <OgreSingleton.h>
 #include "Background2D.h"
 #include "Entity.h"
@@ -366,6 +369,13 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         void ScriptPlayerLock(const bool lock);
 
+        /**
+         * Checks if the player movement is locked.
+         *
+         * @return True if it is locked.
+         */
+        bool IsPlayerLocked() const;
+
         /**
          * Sets the baseline rotation for the player controlled entity.
          *
@@ -383,6 +393,20 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         bool GetRandomEncounters();
 
+        /**
+         * Gets the multiplier of the random encounter rate (the encounter_multiplier cvar).
+         *
+         * @return The multiplier. 0 prevents random encounters.
+         */
+        float GetEncounterMultiplier() const;
+
+        /**
+         * Checks if the random encounter checks are logged (the debug_encounters cvar).
+         *
+         * @return True if they are logged.
+         */
+        bool IsDebugEncounters() const;
+
         /**
          * Enables or disables random encounters in the field.
          *
@@ -404,6 +428,55 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          */
         void SetEncounterRate(float rate);
 
+        /**
+         * Removes the random encounter tables of the field.
+         */
+        void ClearEncounterTables();
+
+        /**
+         * Hides the entities of the field or world map, while a battle takes place.
+         *
+         * Only the visible ones are hidden. They are shown again with
+         * {@see ShowFieldEntities}.
+         */
+        void HideFieldEntities();
+
+        /**
+         * Shows the entities hidden by {@see HideFieldEntities}.
+         */
+        void ShowFieldEntities();
+
+        /**
+         * Adds a random encounter table to the field.
+         *
+         * @param[in] id Table ID (1 or 2).
+         * @param[in] rate Encounter rate, as in the original game (24 is very low, 240 is high).
+         * @param[in] battles Standard encounters, pairs of battle ID and probability (out of 64).
+         * @param[in] specials Special encounters, with the same format.
+         */
+        void AddEncounterTable(
+          const int id, const int rate, const std::vector<std::pair<int, int>>& battles,
+          const std::vector<std::pair<int, int>>& specials
+        );
+
+        /**
+         * Selects the encounter table to use.
+         *
+         * To be called from Lua scripts.
+         *
+         * @param[in] id Table ID (1 or 2). If it doesn't exist, no encounters happen.
+         */
+        void ScriptSetEncounterTable(const int id);
+
+        /**
+         * Picks the battle of a random encounter from the current encounter table.
+         *
+         * To be called from Lua scripts, also used by the engine.
+         *
+         * @return The battle ID, -1 if the field has no encounters.
+         */
+        int ScriptRollEncounterBattle() const;
+
         /**
          * Checks if a key is being pressed.
          *
@@ -793,4 +866,77 @@ class EntityManager : public Manager, public Ogre::Singleton<EntityManager>{
          * The encounter rate of the map.
          */
         float encounter_rate_;
+
+        /**
+         * Random encounter table of a field.
+         */
+        struct EncounterTable{
+
+            /**
+             * Encounter rate, as in the original game.
+             */
+            int rate;
+
+            /**
+             * Standard encounters: battle ID and probability (out of 64).
+             */
+            std::vector<std::pair<int, int>> battles;
+
+            /**
+             * Special encounters: battle ID and probability (out of 64).
+             */
+            std::vector<std::pair<int, int>> specials;
+        };
+
+        /**
+         * Random encounter tables of the field, by ID.
+         */
+        std::map<int, EncounterTable> encounter_tables_;
+
+        /**
+         * Entities that were visible before a battle, hidden while it lasts.
+         */
+        std::vector<Entity*> entities_hidden_by_battle_;
+
+        /**
+         * ID of the encounter table in use.
+         */
+        int encounter_table_;
+
+        /**
+         * Distance walked by the player since the last step.
+         */
+        float encounter_distance_;
+
+        /**
+         * Position of the player in the last frame, to know how much it has walked.
+         */
+        Ogre::Vector3 encounter_last_position_;
+
+        /**
+         * Indicates if {@see encounter_last_position_} is valid.
+         */
+        bool encounter_last_position_valid_;
+
+        /**
+         * Steps that must be taken before encounters can happen again.
+         */
+        int encounter_safe_steps_;
+
+        /**
+         * Accumulated chance of a random encounter, out of 256 * 256. It grows while walking.
+         */
+        int encounter_accumulator_ = 0;
+
+        /**
+         * Last random battle, which is not repeated twice in a row. -1 if none.
+         */
+        int encounter_last_battle_ = -1;
+
+        /**
+         * Counts the steps of the player and starts random encounters.
+         *
+         * @param[in] walked True if the player was moved by the player's input in this frame.
+         */
+        void UpdateEncounters(const bool walked);
 };

+ 12 - 0
src/core/EntityModel.cpp

@@ -61,6 +61,18 @@ void EntityModel::SetVisible(const bool visible){model_->setVisible(visible);}
 
 bool EntityModel::IsVisible() const{return model_->isVisible();}
 
+float EntityModel::GetAnimationLength(const Ogre::String& animation) const{
+    Ogre::AnimationStateSet* states = model_->getAllAnimationStates();
+    if (states == nullptr || states->hasAnimationState(animation) == false) return 0.0f;
+    return states->getAnimationState(animation)->getLength();
+}
+
+unsigned int EntityModel::GetAnimationCount() const{
+    Ogre::AnimationStateSet* states = model_->getAllAnimationStates();
+    if (states == nullptr) return 0;
+    return states->getAnimationStates().size();
+}
+
 Ogre::Entity* EntityModel::GetModel(){return model_;}
 
 void EntityModel::PlayAnimation(

+ 4 - 0
src/core/EntityModel.h

@@ -60,6 +60,10 @@ class EntityModel : public Entity{
          */
         virtual bool IsVisible() const;
 
+        virtual float GetAnimationLength(const Ogre::String& animation) const;
+
+        virtual unsigned int GetAnimationCount() const;
+
         Ogre::Entity* GetModel();
 
         /**

+ 10 - 8
src/core/Manager.cpp

@@ -55,18 +55,20 @@ void Manager::SetBattleModule(){SetModule(Module::BATTLE);}
 void Manager::SetWorldModule(){SetModule(Module::WORLD);}
 
 void Manager::SetPreviousModule(){
-    if (module_ != Module::BATTLE)
+    if (module_ != Module::BATTLE){
         LOG_WARNING(
-          "Called Manager::SetPreviousModule without battle module active. Doing nothing"
-          + "Current module is " + std::to_string(module_)
+          std::string("Called Manager::SetPreviousModule without battle module active. ")
+          + "Doing nothing. Current module is " + std::to_string(module_)
         );
-    else if(module_ = prev_module_)
+        return;
+    }
+    if (prev_module_ == Module::BATTLE){
         LOG_WARNING(
-          "Called Manager::SetPreviousModule but there is no previous module. Doing nothing"
-          + "Current module is " + std::to_string(module_)
+          "Called Manager::SetPreviousModule but there is no previous module. Doing nothing."
         );
-    else module_ = prev_module_;
-    return;
+        return;
+    }
+    module_ = prev_module_;
 }
 
 void Manager::Update(){

+ 81 - 6
src/core/ScriptManagerBinds.h

@@ -55,6 +55,9 @@ void ScriptMap(const char* text){
   VGears::WorldmapModule::getSingleton().Suspend();
   EntityManager::getSingleton().SetFieldModule();
     EntityManager::getSingleton().Clear();
+    // The battle music and mode are set by the scripts of each field.
+    AudioManager::getSingleton().ScriptSetBattleTrack(-1);
+    BattleManager::getSingleton().ScriptSetMode(0);
     XmlMapsFile xml("./data/fields/_fields.xml");
     Ogre::String file_name = xml.GetMapFileNameByName(text);
     XmlMapFile xml_map("./data/fields/" + file_name);
@@ -155,6 +158,8 @@ void ScriptManager::InitBinds(){
           &Entity::ScriptPlayAnimationStop
         )
       ),
+      "get_animation_length", &Entity::ScriptGetAnimationLength,
+      "get_animation_count", &Entity::GetAnimationCount,
       "set_default_animation", &Entity::ScriptSetDefaultAnimation
     );
     sol::table entity_type = lua["Entity"];
@@ -197,7 +202,10 @@ void ScriptManager::InitBinds(){
       "get_player_entity", &EntityManager::ScriptGetPlayerEntity,
       "unset_player_entity", &EntityManager::ScriptUnsetPlayerEntity,
       "player_lock", &EntityManager::ScriptPlayerLock,
+      "is_player_locked", &EntityManager::IsPlayerLocked,
       "random_encounters_on", &EntityManager::SetRandomEncounters,
+      "set_encounter_table", &EntityManager::ScriptSetEncounterTable,
+      "roll_encounter_battle", &EntityManager::ScriptRollEncounterBattle,
       "is_key_on", &EntityManager::IsKeyOn,
       "is_key_off", &EntityManager::IsKeyOff,
       "set_entity_to_character", &EntityManager::SetEntityToCharacter
@@ -210,7 +218,57 @@ void ScriptManager::InitBinds(){
       "end_battle", &BattleManager::EndBattle,
       "get_enemy_count", &BattleManager::ScriptGetEnemyCount,
       "get_enemy", &BattleManager::ScriptGetEnemy,
-      "add_attack_model", &BattleManager::ScriptAddAttackModel
+      "add_attack_model", &BattleManager::ScriptAddAttackModel,
+      "has_attack", &BattleManager::HasAttackData,
+      "set_camera", &BattleManager::ScriptSetCamera,
+      "get_layout", &BattleManager::ScriptGetLayout,
+      "get_attack", [](BattleManager& manager, const int id, sol::this_state state) -> sol::object{
+          sol::state_view view(state);
+          const AttackData* attack = manager.GetAttackData(id);
+          if (attack == nullptr) return sol::make_object(view, sol::nil);
+          sol::table result = view.create_table();
+          result["id"] = attack->id;
+          result["name"] = attack->name;
+          result["mp"] = attack->mp;
+          result["reflectable"] = attack->reflectable;
+          sol::table damage = view.create_table();
+          for (const auto& p : attack->damage) damage[p.first] = p.second;
+          result["damage"] = damage;
+          sol::table target = view.create_table();
+          for (const auto& p : attack->target) target[p.first] = p.second;
+          result["target"] = target;
+          sol::table animation = view.create_table();
+          for (const auto& p : attack->animation) animation[p.first] = p.second;
+          result["animation"] = animation;
+          sol::table effects = view.create_table();
+          int e = 1;
+          for (const auto& effect : attack->effects){
+              sol::table entry = view.create_table();
+              entry["id"] = effect.id;
+              entry["chance"] = effect.chance;
+              entry["mode"] = effect.mode;
+              effects[e ++] = entry;
+          }
+          result["effects"] = effects;
+          sol::table elements = view.create_table();
+          int n = 1;
+          for (int element : attack->elements) elements[n ++] = element;
+          result["elements"] = elements;
+          return result;
+      },
+      "get_next_formation_id", &BattleManager::GetNextFormationId,
+      "set_mode", &BattleManager::ScriptSetMode,
+      "get_mode", &BattleManager::GetMode,
+      "get_formation_id", &BattleManager::GetFormationId,
+      "get_escape_difficulty", &BattleManager::GetEscapeDifficulty,
+      "is_preemptive_allowed", &BattleManager::IsPreemptiveAllowed,
+      "show_victory_pose", &BattleManager::ShowVictoryPose,
+      "show_spoils", &BattleManager::ShowSpoils,
+      "set_enemy_visible", &BattleManager::ScriptSetEnemyVisible,
+      "get_enemy_entity_name", &BattleManager::ScriptGetEnemyEntityName,
+      "get_party_entity_name", &BattleManager::ScriptGetPartyEntityName,
+      "get_party_animation_prefix", &BattleManager::ScriptGetPartyAnimationPrefix,
+      "get_party_model_name", &BattleManager::ScriptGetPartyModelName
     );
 
     // Commands for enemy handling.
@@ -241,11 +299,25 @@ void ScriptManager::InitBinds(){
       "is_active", &Enemy::IsActive,
       "get_attack_count", &Enemy::ScriptGetAttackCount,
       "get_attack", &Enemy::ScriptGetAttack,
-      "get_camera_for_attack", &Enemy::ScriptGetCameraForAttack
+      "get_camera_for_attack", &Enemy::ScriptGetCameraForAttack,
+      "get_drop_count", &Enemy::ScriptGetDropCount,
+      "get_animation_count", &Enemy::ScriptGetAnimationCount,
+      "get_attack_animation", &Enemy::ScriptGetAttackAnimation,
+      "get_element_count", &Enemy::ScriptGetElementCount,
+      "get_ai_script", &Enemy::ScriptGetAiScript,
+      "get_immunity_count", &Enemy::ScriptGetImmunityCount,
+      "get_immunity", &Enemy::ScriptGetImmunity,
+      "get_immunity_rate", &Enemy::ScriptGetImmunityRate,
+      "get_element", &Enemy::ScriptGetElement,
+      "get_element_factor", &Enemy::ScriptGetElementFactor,
+      "get_animation", &Enemy::ScriptGetAnimation,
+      "get_model_prefix", &Enemy::ScriptGetModelPrefix,
+      "get_model", &Enemy::GetModel,
+      "get_drop_id", &Enemy::ScriptGetDropId,
+      "get_drop_rate", &Enemy::ScriptGetDropRate
       // TODO: Implement cover flags.
       // TODO: Implement immunities
       // TODO: Implement elements
-      // TODO: Implement item drops
       // TODO: Implement stealable items
       // TODO: Implement manipulability status and attacks.
       // TODO: Implement morph item
@@ -262,13 +334,15 @@ void ScriptManager::InitBinds(){
       "play_sounds", &AudioManager::ScriptPlaySounds,
       "get_track_id", &AudioManager::ScriptGetTrack,
       "get_battle_track_id", &AudioManager::ScriptGetBattleTrack,
-      "set_battle_track_id", &AudioManager::ScriptSetBattleTrack
+      "set_battle_track_id", &AudioManager::ScriptSetBattleTrack,
+      "play_music_once", &AudioManager::ScriptPlayMusicOnce
     );
 
     // Commands for the camera manager.
     lua.new_usertype<CameraManager>(
       "CameraManager",
-      "set_camera", &CameraManager::ScriptSetCamera
+      "set_camera", &CameraManager::ScriptSetCamera,
+      "project_to_ui", &CameraManager::ScriptProjectToUi
     );
 
     // Commands for the savemap handler.
@@ -495,7 +569,8 @@ void ScriptManager::InitBinds(){
       "add_vehicle_terrain", &VGears::WorldmapModule::AddVehicleTerrain,
       "get_terrain_type_under_entity", &VGears::WorldmapModule::GetTerrainTypeUnderEntity,
       "get_terrain_triangle_under_entity", &VGears::WorldmapModule::GetTerrainTriangleUnderEntity,
-      "unboard_vehicle", &VGears::WorldmapModule::UnboardVehicle
+      "unboard_vehicle", &VGears::WorldmapModule::UnboardVehicle,
+      "set_encounters_locked", &VGears::WorldmapModule::SetEncountersLocked
     );
 
     // Register all command handlers

+ 78 - 0
src/core/XmlAttackFile.cpp

@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#include "core/XmlAttackFile.h"
+#include "core/Logger.h"
+
+XmlAttackFile::XmlAttackFile(const Ogre::String& file): XmlFile(file){}
+
+XmlAttackFile::~XmlAttackFile(){}
+
+void XmlAttackFile::ReadParameters(TiXmlNode* node, std::map<std::string, float>& parameters) const{
+    for (TiXmlNode* child = node->FirstChild(); child != nullptr; child = child->NextSibling()){
+        if (child->Type() != TiXmlNode::TINYXML_ELEMENT || child->ValueStr() != "Parameter") continue;
+        parameters[GetString(child, "id")] = GetFloat(child, "value");
+    }
+}
+
+bool XmlAttackFile::LoadAttack(AttackData& attack){
+    TiXmlNode* root = file_.RootElement();
+    if (root == nullptr || root->ValueStr() != "Attack"){
+        LOG_ERROR(file_.ValueStr() + " is not a valid attack file! No <Attack> in root.");
+        return false;
+    }
+    attack.id = GetInt(root, "id");
+    attack.name = GetString(root, "name");
+    attack.mp = GetInt(root, "mp");
+    attack.reflectable = GetInt(root, "reflectable") == 1;
+    for (TiXmlNode* node = root->FirstChild(); node != nullptr; node = node->NextSibling()){
+        if (node->Type() != TiXmlNode::TINYXML_ELEMENT) continue;
+        if (node->ValueStr() == "Damage") ReadParameters(node, attack.damage);
+        else if (node->ValueStr() == "Target") ReadParameters(node, attack.target);
+        else if (node->ValueStr() == "Animation") ReadParameters(node, attack.animation);
+        else if (node->ValueStr() == "Effects"){
+            for (TiXmlNode* e = node->FirstChild(); e != nullptr; e = e->NextSibling()){
+                if (e->Type() != TiXmlNode::TINYXML_ELEMENT || e->ValueStr() != "Effect") continue;
+                AttackData::Effect effect;
+                effect.id = GetInt(e, "id");
+                effect.chance = GetFloat(e, "chance");
+                effect.mode = GetString(e, "mode");
+                attack.effects.push_back(effect);
+            }
+        }
+        else if (node->ValueStr() == "Elements"){
+            for (TiXmlNode* e = node->FirstChild(); e != nullptr; e = e->NextSibling()){
+                if (e->Type() != TiXmlNode::TINYXML_ELEMENT || e->ValueStr() != "Element") continue;
+                attack.elements.push_back(GetInt(e, "id"));
+            }
+        }
+    }
+    return true;
+}
+
+std::map<int, std::string> XmlAttackFile::LoadIndex(const Ogre::String& file){
+    std::map<int, std::string> index;
+    XmlAttackFile xml(file);
+    TiXmlNode* root = xml.file_.RootElement();
+    if (root == nullptr || root->ValueStr() != "attacks"){
+        LOG_ERROR(file + " is not a valid attack index! No <attacks> in root.");
+        return index;
+    }
+    for (TiXmlNode* node = root->FirstChild(); node != nullptr; node = node->NextSibling()){
+        if (node->Type() != TiXmlNode::TINYXML_ELEMENT || node->ValueStr() != "attack") continue;
+        index[xml.GetInt(node, "id")] = xml.GetString(node, "file");
+    }
+    return index;
+}

+ 125 - 0
src/core/XmlAttackFile.h

@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 The V-Gears Team
+ *
+ * This file is part of V-Gears
+ *
+ * V-Gears is free software: you can redistribute it and/or modify it under
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation, version 3.0 (GPLv3) of the License.
+ *
+ * V-Gears is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ */
+
+#pragma once
+
+#include <map>
+#include <string>
+#include <vector>
+#include "XmlFile.h"
+
+/**
+ * Data of an attack, as stored in the attack XML files.
+ */
+struct AttackData{
+
+    /**
+     * Status effect applied by the attack.
+     */
+    struct Effect{
+
+        /**
+         * Status ID.
+         */
+        int id;
+
+        /**
+         * Chance to apply the effect, between 0 and 1.
+         *
+         * Negative values are the chance of the opposite (ie, to remove the effect).
+         */
+        float chance;
+
+        /**
+         * Effect mode: "add", "remove" or "toggle".
+         */
+        std::string mode;
+    };
+
+    int id = -1;
+    std::string name;
+    int mp = 0;
+    bool reflectable = false;
+
+    /**
+     * Damage parameters ({@see XmlAttackFile} for the names): power, formula, modifier, accuracy,
+     * affected_by_darkness, restore, damage_mp, drain_hp, drain_mp, ignore_status_defense,
+     * always_critical.
+     */
+    std::map<std::string, float> damage;
+
+    /**
+     * Target parameters: selection_enabled, default_enemy, default_multiple, toggle_multiple,
+     * fixed_row, short_range, all_rows, random, only_for_dead, no_retarget.
+     */
+    std::map<std::string, float> target;
+
+    /**
+     * Animation parameters: effect, impact_effect, sound, hurt_anim, camera_single,
+     * camera_multiple.
+     */
+    std::map<std::string, float> animation;
+
+    std::vector<Effect> effects;
+
+    /**
+     * Element indexes (0 is fire, 1 is ice, 2 is lightning...).
+     */
+    std::vector<int> elements;
+};
+
+/**
+ * Reads an attack XML file.
+ */
+class XmlAttackFile : public XmlFile{
+
+    public:
+
+        /**
+         * Constructor.
+         *
+         * @param[in] file Path to the attack XML file.
+         */
+        explicit XmlAttackFile(const Ogre::String& file);
+
+        /**
+         * Destructor.
+         */
+        virtual ~XmlAttackFile();
+
+        /**
+         * Parses the file.
+         *
+         * @param[out] attack The attack data.
+         * @return True if the file was read, false if it's not valid.
+         */
+        bool LoadAttack(AttackData& attack);
+
+        /**
+         * Reads the index of attack files by attack ID.
+         *
+         * @param[in] file Path to the index XML file.
+         * @return The file names (without path) by attack ID.
+         */
+        static std::map<int, std::string> LoadIndex(const Ogre::String& file);
+
+    private:
+
+        /**
+         * Reads the Parameter children of a node into a map.
+         */
+        void ReadParameters(TiXmlNode* node, std::map<std::string, float>& parameters) const;
+
+};

+ 48 - 14
src/core/XmlEnemyFile.cpp

@@ -54,41 +54,75 @@ void XmlEnemyFile::LoadEnemy(Enemy& enemy){
             }
         }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "elements"){
-            TiXmlNode* node_elm = node->FirstChild();
-            while (node_elm != nullptr && node->ValueStr() == "element"){
+            for (TiXmlNode* node_elm = node->FirstChild(); node_elm != nullptr; node_elm = node_elm->NextSibling()){
+                if (node_elm->Type() != TiXmlNode::TINYXML_ELEMENT || node_elm->ValueStr() != "element") continue;
                 Enemy::Element elm;
-                elm.id = GetInt(node_elm, "element");
+                elm.id = GetInt(node_elm, "id");
                 elm.factor = GetFloat(node_elm, "factor");
                 enemy.AddElement(elm);
-                node_elm = node_elm->NextSibling();
             }
         }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "immunities"){
-            TiXmlNode* node_imm = node->FirstChild();
-            while (node_imm != nullptr && node->ValueStr() == "element"){
+            for (TiXmlNode* node_imm = node->FirstChild(); node_imm != nullptr; node_imm = node_imm->NextSibling()){
+                if (node_imm->Type() != TiXmlNode::TINYXML_ELEMENT || node_imm->ValueStr() != "immunity") continue;
                 Enemy::Immunity imm;
-                imm.status = GetInt(node_imm, "immunity");
+                imm.status = GetInt(node_imm, "status");
                 imm.rate = GetFloat(node_imm, "rate");
                 enemy.AddImmunity(imm);
-                node_imm = node_imm->NextSibling();
             }
         }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "attacks"){
-            // TODO
+            for (TiXmlNode* node_atk = node->FirstChild(); node_atk != nullptr; node_atk = node_atk->NextSibling()){
+                if (node_atk->Type() != TiXmlNode::TINYXML_ELEMENT || node_atk->ValueStr() != "attack") continue;
+                Enemy::Attack atk;
+                atk.id = GetInt(node_atk, "id");
+                atk.animation = GetInt(node_atk, "animation", -1);
+                atk.camera = GetInt(node_atk, "camera", -1);
+                enemy.AddAttack(atk);
+            }
         }
         else if (
           node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "manipulate"
         ){
             // TODO
         }
-        else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "steal"){
-            // TODO
+        else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "ai"){
+            // AI scripts, as hexadecimal bytecode.
+            for (TiXmlNode* script = node->FirstChild(); script != nullptr; script = script->NextSibling()){
+                if (script->Type() != TiXmlNode::TINYXML_ELEMENT || script->ValueStr() != "script") continue;
+                const std::string hex = GetString(script, "data");
+                std::string code;
+                for (size_t i = 0; i + 1 < hex.size(); i += 2){
+                    code.push_back(static_cast<char>(std::stoi(hex.substr(i, 2), nullptr, 16)));
+                }
+                enemy.SetAiScript(GetInt(script, "slot"), code);
+            }
         }
-        else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "drops"){
-            // TODO
+        else if (
+          node->Type() == TiXmlNode::TINYXML_ELEMENT
+          && (node->ValueStr() == "steal" || node->ValueStr() == "drop" || node->ValueStr() == "drops")
+        ){
+            const bool is_steal = node->ValueStr() == "steal";
+            TiXmlNode* node_item = node->FirstChild();
+            while (node_item != nullptr){
+                if (node_item->Type() == TiXmlNode::TINYXML_ELEMENT && node_item->ValueStr() == "item"){
+                    Enemy::Item item;
+                    item.id = GetInt(node_item, "id");
+                    item.rate = GetFloat(node_item, "rate");
+                    if (is_steal) enemy.AddSteal(item);
+                    else enemy.AddDrop(item);
+                }
+                node_item = node_item->NextSibling();
+            }
         }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "animations"){
-            // TODO
+            TiXmlNode* node_anim = node->FirstChild();
+            while (node_anim != nullptr){
+                if (node_anim->Type() == TiXmlNode::TINYXML_ELEMENT){
+                    enemy.AddAnimation(GetInt(node_anim, "id"));
+                }
+                node_anim = node_anim->NextSibling();
+            }
         }
         node = node->NextSibling();
     }

+ 4 - 0
src/core/XmlFormationFile.cpp

@@ -29,6 +29,10 @@ void XmlFormationFile::LoadFormation(){
     }
     BattleManager::getSingleton().SetFormationId(GetInt(node, "id"));
     BattleManager::getSingleton().SetNextFormationId(GetInt(node, "next"));
+    BattleManager::getSingleton().SetLayout(
+      static_cast<BattleManager::LAYOUT>(GetInt(node, "layout"))
+    );
+    BattleManager::getSingleton().SetPreemptiveDisabled(GetInt(node, "preemptive_disabled") == 1);
     if (GetInt(node, "escapable") == 1)
         BattleManager::getSingleton().SetEscapeability(GetFloat(node, "escape_difficulty"));
     else BattleManager::getSingleton().SetEscapeability(-1.0f);

+ 19 - 0
src/core/XmlMapFile.cpp

@@ -50,6 +50,25 @@ void XmlMapFile::LoadMap(){
             Ogre::Degree rotation = Ogre::Degree(GetFloat(node, "degree", 0));
             EntityManager::getSingleton().SetPlayerMoveRotation(Ogre::Radian(rotation));
         }
+        else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "encounters"){
+            // Random encounter tables. Encounters are enabled until a script disables them.
+            EntityManager::getSingleton().ClearEncounterTables();
+            for (TiXmlNode* table = node->FirstChild(); table != nullptr; table = table->NextSibling()){
+                if (table->Type() != TiXmlNode::TINYXML_ELEMENT || table->ValueStr() != "table") continue;
+                std::vector<std::pair<int, int>> battles;
+                std::vector<std::pair<int, int>> specials;
+                for (TiXmlNode* entry = table->FirstChild(); entry != nullptr; entry = entry->NextSibling()){
+                    if (entry->Type() != TiXmlNode::TINYXML_ELEMENT) continue;
+                    const std::pair<int, int> encounter(GetInt(entry, "id"), GetInt(entry, "probability"));
+                    if (entry->ValueStr() == "battle") battles.push_back(encounter);
+                    else if (entry->ValueStr() == "special") specials.push_back(encounter);
+                }
+                EntityManager::getSingleton().AddEncounterTable(
+                  GetInt(table, "id", 1), GetInt(table, "rate"), battles, specials
+                );
+            }
+            EntityManager::getSingleton().SetRandomEncounters(true);
+        }
         else if (node->Type() == TiXmlNode::TINYXML_ELEMENT && node->ValueStr() == "background2d"){
             Ogre::String name = GetString(node, "file_name");
             if (name != ""){

+ 6 - 0
src/data/VGearsFLevelFile.cpp

@@ -142,6 +142,12 @@ namespace VGears{
 
     void FLevelFile::SetRawScript(const std::vector<u8>& script_data){raw_script_ = script_data;}
 
+    const std::vector<u8>& FLevelFile::GetRawEncounter() const{return raw_encounter_;}
+
+    void FLevelFile::SetRawEncounter(const std::vector<u8>& encounter_data){
+        raw_encounter_ = encounter_data;
+    }
+
     void FLevelFile::SetBackground(const BackgroundFilePtr &background){background_ = background;}
 
     const CameraMatrixFilePtr& FLevelFile::GetCameraMatrix(void) const{return camera_matrix_;}

+ 24 - 0
src/data/VGearsFLevelFile.h

@@ -80,6 +80,18 @@ namespace VGears{
              */
             const std::vector<u8>& GetRawScript() const;
 
+            /**
+             * Retrieves the raw encounter data section.
+             *
+             * It has two encounter tables of 24 bytes. Each one has a byte indicating if it's
+             * enabled, the encounter rate, six standard encounters and four special encounters.
+             * Encounters are 16 bit values, with the battle ID in the lower 10 bits and the
+             * probability (out of 64) in the upper 6 bits.
+             *
+             * @return The raw encounter data, empty if the file has no such section.
+             */
+            const std::vector<u8>& GetRawEncounter() const;
+
             /**
              * Retrieves the level background.
              *
@@ -129,6 +141,13 @@ namespace VGears{
              */
             void SetRawScript(const std::vector<u8>& script_data);
 
+            /**
+             * Sets the raw encounter data section.
+             *
+             * @param[in] encounter_data The raw section.
+             */
+            void SetRawEncounter(const std::vector<u8>& encounter_data);
+
             /**
              * Sets the background for the level.
              *
@@ -272,6 +291,11 @@ namespace VGears{
              */
             std::vector<u8> raw_script_;
 
+            /**
+             * Raw encounter data section.
+             */
+            std::vector<u8> raw_encounter_;
+
             /**
              * The background texture loader.
              */

+ 5 - 1
src/data/VGearsFLevelFileSerializer.cpp

@@ -164,7 +164,11 @@ namespace VGears{
                 // TODO
                 break;
             case SECTION_ENCOUNTER:
-                // TODO
+                {
+                    std::vector<u8> buffer(stream->size());
+                    stream->read(buffer.data(), buffer.size());
+                    dest->SetRawEncounter(buffer);
+                }
                 break;
             case SECTION_TRIGGER:
                 ReadTriggers(stream, dest);

+ 88 - 25
src/installer/BattleDataInstaller.cpp

@@ -48,23 +48,33 @@ unsigned int BattleDataInstaller::InitializeScenes(){
     scenes_.clear();
     enemies_.clear();
     attacks_.clear();
-    // Get all scenes from scene.bin.
-    for (int i = 0; i < 32; i ++){
-        File scene_bin_block(&scene_bin_, i * 0x2000, 0x2000);
-        for (int o = 0; o < 10; o ++){
-            scene_bin_block.SetOffset(4 * o);
-            u32 start_offset = scene_bin_block.readU32LE() * 4;
-            u32 end_offset = scene_bin_block.readU32LE() * 4;
-            if (end_offset > start_offset + 0x2000) end_offset = start_offset + 0x2000;
-            if (start_offset >= 0 && start_offset < 0x2000){
-                File scene_bin_fragment(&scene_bin_block, start_offset, end_offset - start_offset - 1);
-                File* extracted = ExtractGZipScene(scene_bin_fragment);
-                if (extracted != NULL){
-                    scenes_uncompressed_.push_back(extracted);
-                    total_scenes_ ++;
-                }
-            }
-            else break;
+    scenes_uncompressed_.clear();
+    total_scenes_ = 0;
+    // scene.bin is a sequence of 0x2000 bytes blocks (33 in the original game). Each block starts
+    // with up to 16 pointers (in 4-byte units) to gzipped scenes, terminated by 0xFFFFFFFF. A scene
+    // ends where the next one starts, or at the end of the block. There are 256 scenes in total.
+    // Scene indexes are positional: every slot must be kept, even if it can't be extracted,
+    // because battle IDs (scene * 4 + setup) are derived from them.
+    const u32 block_size = 0x2000;
+    const u32 blocks = scene_bin_.GetFileSize() / block_size;
+    for (u32 i = 0; i < blocks; i ++){
+        File scene_bin_block(&scene_bin_, i * block_size, block_size);
+        u32 offsets[16];
+        u32 count = 0;
+        for (; count < 16; count ++){
+            scene_bin_block.SetOffset(4 * count);
+            u32 pointer = scene_bin_block.readU32LE();
+            if (pointer == 0xFFFFFFFF || pointer * 4 >= block_size) break;
+            offsets[count] = pointer * 4;
+        }
+        for (u32 o = 0; o < count; o ++){
+            u32 start_offset = offsets[o];
+            u32 end_offset = (o + 1 < count) ? offsets[o + 1] : block_size;
+            if (end_offset <= start_offset) end_offset = block_size;
+            File scene_bin_fragment(&scene_bin_block, start_offset, end_offset - start_offset);
+            // NULL if it can't be extracted. Kept to preserve scene indexes.
+            scenes_uncompressed_.push_back(ExtractGZipScene(scene_bin_fragment));
+            total_scenes_ ++;
         }
     }
     return total_scenes_;
@@ -120,7 +130,19 @@ unsigned int BattleDataInstaller::InitializeSpellModels(){
 
 unsigned int BattleDataInstaller::ProcessScene(){
     if (next_scene_ >= scenes_uncompressed_.size()) return total_scenes_;
-    BattleSceneFile scene(next_scene_, scenes_uncompressed_[next_scene_]);
+    File* scene_file = scenes_uncompressed_[next_scene_];
+    if (scene_file == NULL){
+        // Keep battle IDs stable: an unreadable scene still takes its 4 battle slots.
+        std::cout << "Warning: scene " << next_scene_ << " could not be extracted." << std::endl;
+        for (int f = 0; f < 4; f ++){
+            Formation empty;
+            empty.id = next_scene_ * 4 + f;
+            formations_.push_back(empty);
+        }
+        next_scene_ ++;
+        return next_scene_;
+    }
+    BattleSceneFile scene(next_scene_, *scene_file);
     scenes_.push_back(scene);
     // Add new enemies to the list.
     std::vector<Enemy> enemies = scene.GetEnemies();
@@ -138,11 +160,11 @@ unsigned int BattleDataInstaller::ProcessScene(){
         )) continue;
         attacks_.push_back(attacks[a]);
     }
-    // Add new formations to the list.
+    // Battle ID = scene index * 4 + setup index, as referenced by field scripts and encounters.
     std::vector<Formation> formations = scene.GetFormations();
-    for (Formation f : formations){
-        f.id = formations_.size(); // Assign the next ID.
-        formations_.push_back(f);
+    for (int f = 0; f < formations.size(); f ++){
+        formations[f].id = next_scene_ * 4 + f;
+        formations_.push_back(formations[f]);
     }
     next_scene_ ++;
     return next_scene_;
@@ -275,7 +297,10 @@ unsigned int BattleDataInstaller::ProcessBattleModel(){
             scene_data.id = info.numeric_id;
             scene_data.model = info.alphanumeric_id + "_" + info.name_normal + ".mesh";
             scene_data.description = info.name;
-            scene_data_.push_back(scene_data);
+            // Every texture of a scene model reaches this point, but the map needs one entry.
+            if (std::none_of(scene_data_.begin(), scene_data_.end(), [&](const SceneModel& s){
+                return s.id == scene_data.id && s.model == scene_data.model;
+            })) scene_data_.push_back(scene_data);
             path += "scenes/";
         }
         else path += "enemies/";
@@ -630,7 +655,8 @@ void BattleDataInstaller::WriteEnemies(){
         std::unique_ptr<TiXmlElement> attacks(new TiXmlElement("attacks"));
         for (Enemy::Attack attack : enemy.attacks){
             std::unique_ptr<TiXmlElement> xml_attack(new TiXmlElement("attack"));
-            xml_attack->SetAttribute("status", attack.id);
+            xml_attack->SetAttribute("id", attack.id);
+            xml_attack->SetAttribute("animation", attack.animation);
             xml_attack->SetAttribute("camera", attack.camera);
             attacks->LinkEndChild(xml_attack.release());
         }
@@ -666,6 +692,30 @@ void BattleDataInstaller::WriteEnemies(){
             animations->LinkEndChild(xml_animation.release());
         }
         container->LinkEndChild(animations.release());
+        // AI scripts, as hexadecimal bytecode.
+        {
+            static const char* AI_SLOT_NAMES[16] = {
+              "init", "main", "counter_general", "counter_death", "counter_physical", "counter_magic",
+              "battle_end", "pre_turn", "event_1", "event_2", "event_3", "event_4", "event_5",
+              "event_6", "event_7", "event_8"
+            };
+            std::unique_ptr<TiXmlElement> ai(new TiXmlElement("ai"));
+            for (size_t slot = 0; slot < enemy.ai_scripts.size() && slot < 16; slot ++){
+                if (enemy.ai_scripts[slot].empty()) continue;
+                std::string hex;
+                char digits[3];
+                for (u8 byte : enemy.ai_scripts[slot]){
+                    snprintf(digits, sizeof(digits), "%02x", byte);
+                    hex += digits;
+                }
+                std::unique_ptr<TiXmlElement> xml_script(new TiXmlElement("script"));
+                xml_script->SetAttribute("slot", static_cast<int>(slot));
+                xml_script->SetAttribute("name", AI_SLOT_NAMES[slot]);
+                xml_script->SetAttribute("data", hex);
+                ai->LinkEndChild(xml_script.release());
+            }
+            container->LinkEndChild(ai.release());
+        }
         xml.LinkEndChild(container.release());
         std::string file_name = BuildEnemyFileName(enemy);
         xml.SaveFile(output_dir_ + file_name);
@@ -697,7 +747,8 @@ void BattleDataInstaller::WriteAttacks(){
         damage->LinkEndChild(damage_modifier.release());
         std::unique_ptr<TiXmlElement> damage_acc(new TiXmlElement("Parameter"));
         damage_acc->SetAttribute("id", "accuracy");
-        damage_acc->SetAttribute("value", attack.damage.accuracy);
+        // Accuracy is a fraction between 0 and 1, it can't be written as an integer.
+        damage_acc->SetDoubleAttribute("value", attack.damage.accuracy);
         damage->LinkEndChild(damage_acc.release());
         std::unique_ptr<TiXmlElement> damage_darkness(new TiXmlElement("Parameter"));
         damage_darkness->SetAttribute("id", "affected_by_darkness");
@@ -817,6 +868,18 @@ void BattleDataInstaller::WriteAttacks(){
         xml.SaveFile(output_dir_ + file_name);
         attack_map_[attack.id] = file_name;
     }
+    // Index of the attack files by ID, as file names include the attack name.
+    TiXmlDocument index;
+    std::unique_ptr<TiXmlElement> attacks(new TiXmlElement("attacks"));
+    for (const auto& entry : attack_map_){
+        std::unique_ptr<TiXmlElement> xml_attack(new TiXmlElement("attack"));
+        xml_attack->SetAttribute("id", entry.first);
+        // The file name in the map has the gamedata/attack/ path.
+        xml_attack->SetAttribute("file", entry.second.substr(entry.second.find_last_of('/') + 1));
+        attacks->LinkEndChild(xml_attack.release());
+    }
+    index.LinkEndChild(attacks.release());
+    index.SaveFile(output_dir_ + "gamedata/attack/_index.xml");
 }
 
 void BattleDataInstaller::WriteFormations(){

+ 28 - 0
src/installer/FieldDataInstaller.cpp

@@ -587,6 +587,34 @@ void FieldDataInstaller::PcFieldToVGearsField(VGears::FLevelFilePtr& field){
         std::unique_ptr<TiXmlElement> xml_forward_direction(new TiXmlElement("movement_rotation"));
         xml_forward_direction->SetAttribute("degree", std::to_string(triggers->MovementRotation()));
         element->LinkEndChild(xml_forward_direction.release());
+        // Random encounters. The section has two tables of 24 bytes: enabled (1 byte), rate
+        // (1 byte), six standard and four special encounters (16 bits each, battle ID in the lower
+        // 10 bits and probability, out of 64, in the upper 6), and 2 bytes of padding.
+        {
+            const std::vector<u8>& raw_encounter = field->GetRawEncounter();
+            std::unique_ptr<TiXmlElement> xml_encounters(new TiXmlElement("encounters"));
+            bool any_table = false;
+            for (int table = 0; table < 2 && raw_encounter.size() >= static_cast<size_t>(24 * (table + 1)); table ++){
+                const u8* data = raw_encounter.data() + 24 * table;
+                if (data[0] == 0) continue;
+                std::unique_ptr<TiXmlElement> xml_table(new TiXmlElement("table"));
+                xml_table->SetAttribute("id", table + 1);
+                xml_table->SetAttribute("rate", static_cast<int>(data[1]));
+                for (int e = 0; e < 10; e ++){
+                    const unsigned int value = data[2 + 2 * e] | (data[3 + 2 * e] << 8);
+                    const int battle = value & 0x3FF;
+                    const int probability = value >> 10;
+                    if (probability == 0) continue;
+                    std::unique_ptr<TiXmlElement> xml_encounter(new TiXmlElement(e < 6 ? "battle" : "special"));
+                    xml_encounter->SetAttribute("id", battle);
+                    xml_encounter->SetAttribute("probability", probability);
+                    xml_table->LinkEndChild(xml_encounter.release());
+                }
+                xml_encounters->LinkEndChild(xml_table.release());
+                any_table = true;
+            }
+            if (any_table) element->LinkEndChild(xml_encounters.release());
+        }
         // Get this fields Id.
         const size_t this_field_id = GetFieldId(field->getName(), map_list_);
         // Use the ID to find the pre-computed list of gateways in all other fields that link to

+ 23 - 41
src/installer/KernelDataInstaller.cpp

@@ -120,7 +120,7 @@ int KernelDataInstaller::ReadCommands(){
         else data.target.fixed_row = false;
         if (data.target_raw == (data.target_raw | 0x20)) data.target.short_range = true;
         else data.target.short_range = false;
-        if (data.target_raw == (data.target_raw | 0x40)) data.target.default_enemy = true;
+        if (data.target_raw == (data.target_raw | 0x40)) data.target.all_rows = true;
         else data.target.all_rows = false;
         if (data.target_raw == (data.target_raw | 0x80)) data.target.random = true;
         else data.target.random = false;
@@ -267,9 +267,11 @@ int KernelDataInstaller::ReadAttacks(){
         else data.target.random = false;
         data.damage_formula = data.damage_raw >> 4; // Upper nybble.
         data.damage_modifier = data.damage_raw & 15; // Lower nybble.
-        data.status.chance = 63 - (data.status_change_raw ^ 63); // Out of 63;
-        if (data.status_change_raw == (data.status_change_raw | 0x00000080)) data.status.mode = TOGGLE;
-        else if (data.status_change_raw == (data.status_change_raw | 0x00000040)) data.status.mode = CURE;
+        // Status attack byte: the lower 6 bits are the chance out of 63, and the upper bits the
+        // mode (0x80 toggles the statuses, 0x40 cures them, otherwise they are inflicted).
+        data.status.chance = data.status_change_raw & 0x3F;
+        if ((data.status_change_raw & 0x80) != 0) data.status.mode = TOGGLE;
+        else if ((data.status_change_raw & 0x40) != 0) data.status.mode = CURE;
         else data.status.mode = INFLICT;
         switch (data.condition_raw){
             case 0: data.restore_type = RESTORE_HP; break;
@@ -278,42 +280,13 @@ int KernelDataInstaller::ReadAttacks(){
             default: data.restore_type = RESTORE_NONE;
         }
         // In KERNEL.BIN, items that don't induce statuses actually have all statuses marked, and
-        // a chance of 255. If it's the case, skip statuses altogether.
-        if (data.status.chance <= 64){
-            // TODO: Im fairly sure that this can be done with a single-line loop...
-            // ... but I don't know how
-            if (data.status_raw == (data.status_raw | 0x00000001)) data.status.status.push_back(DEATH);
-            if (data.status_raw == (data.status_raw | 0x00000002)) data.status.status.push_back(NEAR_DEATH);
-            if (data.status_raw == (data.status_raw | 0x00000004)) data.status.status.push_back(SLEEP);
-            if (data.status_raw == (data.status_raw | 0x00000008)) data.status.status.push_back(POISONED);
-            if (data.status_raw == (data.status_raw | 0x00000010)) data.status.status.push_back(SADNESS);
-            if (data.status_raw == (data.status_raw | 0x00000020)) data.status.status.push_back(FURY);
-            if (data.status_raw == (data.status_raw | 0x00000040)) data.status.status.push_back(CONFU);
-            if (data.status_raw == (data.status_raw | 0x00000080)) data.status.status.push_back(SILENCE);
-            if (data.status_raw == (data.status_raw | 0x00000100)) data.status.status.push_back(HASTE);
-            if (data.status_raw == (data.status_raw | 0x00000200)) data.status.status.push_back(SLOW);
-            if (data.status_raw == (data.status_raw | 0x00000400)) data.status.status.push_back(STOP);
-            if (data.status_raw == (data.status_raw | 0x00000800)) data.status.status.push_back(FROG);
-            if (data.status_raw == (data.status_raw | 0x00001000)) data.status.status.push_back(SMALL);
-            if (data.status_raw == (data.status_raw | 0x00002000)) data.status.status.push_back(SLOW_NUMB);
-            if (data.status_raw == (data.status_raw | 0x00004000)) data.status.status.push_back(PETRIFY);
-            if (data.status_raw == (data.status_raw | 0x00008000)) data.status.status.push_back(REGEN);
-            if (data.status_raw == (data.status_raw | 0x00010000)) data.status.status.push_back(BARRIER);
-            if (data.status_raw == (data.status_raw | 0x00020000)) data.status.status.push_back(M_BARRIER);
-            if (data.status_raw == (data.status_raw | 0x00040000)) data.status.status.push_back(REFLECT);
-            if (data.status_raw == (data.status_raw | 0x00080000)) data.status.status.push_back(DUAL);
-            if (data.status_raw == (data.status_raw | 0x00100000)) data.status.status.push_back(SHIELD);
-            if (data.status_raw == (data.status_raw | 0x00200000)) data.status.status.push_back(D_SENTENCE);
-            if (data.status_raw == (data.status_raw | 0x00400000)) data.status.status.push_back(MANIPULATE);
-            if (data.status_raw == (data.status_raw | 0x00800000)) data.status.status.push_back(BERSERK);
-            if (data.status_raw == (data.status_raw | 0x01000000)) data.status.status.push_back(PEERLESS);
-            if (data.status_raw == (data.status_raw | 0x02000000)) data.status.status.push_back(PARALYSIS);
-            if (data.status_raw == (data.status_raw | 0x04000000)) data.status.status.push_back(DARKNESS);
-            if (data.status_raw == (data.status_raw | 0x08000000)) data.status.status.push_back(DUAL_DRAIN);
-            if (data.status_raw == (data.status_raw | 0x10000000)) data.status.status.push_back(DEATH_FORCE);
-            if (data.status_raw == (data.status_raw | 0x20000000)) data.status.status.push_back(RESIST);
-            if (data.status_raw == (data.status_raw | 0x40000000)) data.status.status.push_back(LUCKY_GIRL);
-            if (data.status_raw == (data.status_raw | 0x80000000)) data.status.status.push_back(IMPRISONED);
+        // a chance of 255. If it's the case, skip statuses altogether. The status IDs are the bit
+        // positions of the original game: 0 death, 1 near death, 2 sleep, 3 poison, 4 fury,
+        // 5 sadness, 6 confusion, 7 silence, 8 haste, 9 slow, 10 stop...
+        if (data.status_change_raw != 0xFF && data.status_raw != 0xFFFFFFFF){
+            for (int bit = 0; bit < 32; bit ++){
+                if ((data.status_raw & (1u << bit)) != 0) data.status.status.push_back(bit);
+            }
         }
         // Get elements
         if (data.element_raw == (data.element_raw | 0x0001)) data.elements.push_back(FIRE);
@@ -372,7 +345,16 @@ void KernelDataInstaller::WriteAttacks(std::string file_name){
           << ", fixed_row = " << attack.target.fixed_row
           << ", short_range = " << attack.target.short_range
           << ", all_rows = " << attack.target.all_rows
-          << ", random = " << attack.target.random << "}\n}\n";
+          << ", random = " << attack.target.random << "},\n"
+          << "    elements = {";
+        for (int element : attack.elements) file << element << ", ";
+        file << "},\n    status = {";
+        // Status effects: ID (bit position), mode (0 inflict, 1 cure, 2 toggle) and chance out of 63.
+        for (int status : attack.status.status){
+            file << "{status = " << status << ", mode = " << attack.status.mode
+              << ", chance = " << attack.status.chance << "}, ";
+        }
+        file << "}\n}\n";
     }
     file.close();
 }

+ 60 - 0
src/installer/WorldInstaller.cpp

@@ -649,6 +649,59 @@ bool WorldInstaller::ProcessMap(){
     else return false;
 }
 
+void WorldInstaller::ExportEncounters(
+  File& archive_file, const VGears::LGPArchive::FileEntry& entry
+){
+    // ENC_W.BIN: 160 bytes of chocobo encounters and battle data, then 64 records of 16 words.
+    // Record = region * 4 + terrain group. Word 0: enabled (low byte bit 0) and rate (high byte,
+    // the lower the more often). Words 1-6: standard battles, 7-10: special battles
+    // (back attack x2, side, pincer) and 11-15: others. A word is a battle ID (10 bits) and a
+    // probability out of 64 (6 bits).
+    static const unsigned int HEADER_SIZE = 0xA0;
+    static const unsigned int RECORD_COUNT = 64;
+    static const unsigned int RECORD_SIZE = 32;
+    if (entry.data_size < HEADER_SIZE + RECORD_COUNT * RECORD_SIZE){
+        std::cerr << "ENC_W.BIN has an unexpected size." << std::endl;
+        return;
+    }
+    std::vector<u8> data(entry.data_size);
+    File file(&archive_file, entry.data_offset, entry.data_size);
+    file.GetFileBuffer(data.data(), 0, entry.data_size);
+    // Walkability types that select each of the 4 terrain groups of every region. This table is
+    // in the executable of the game, not in the data files. The region of a triangle is
+    // (location & 0x1F), and a region over 15 is 15.
+    static const unsigned int TERRAIN_TYPES[16][4] = {
+      {0, 9, 0, 17}, {0, 0, 0, 17}, {0, 9, 1, 17}, {0, 20, 0, 17},
+      {0, 9, 8, 17}, {0, 0, 25, 17}, {0, 9, 19, 17}, {0, 0, 1, 17},
+      {0, 0, 1, 17}, {0, 9, 14, 17}, {0, 9, 25, 17}, {0, 10, 9, 17},
+      {0, 9, 25, 17}, {0, 0, 8, 11}, {0, 0, 8, 0}, {0, 0, 1, 0}
+    };
+    boost::filesystem::create_directories(output_dir_ + "/gamedata");
+    TiXmlDocument doc;
+    std::unique_ptr<TiXmlElement> root(new TiXmlElement("world_encounters"));
+    for (unsigned int region = 0; region < 16; ++ region){
+        for (unsigned int group = 0; group < 4; ++ group){
+            const unsigned int index = region * 4 + group;
+            const unsigned int offset = HEADER_SIZE + index * RECORD_SIZE;
+            std::unique_ptr<TiXmlElement> record(new TiXmlElement("record"));
+            record->SetAttribute("region", region);
+            record->SetAttribute("terrain", TERRAIN_TYPES[region][group]);
+            record->SetAttribute("enabled", data[offset] & 0x01);
+            record->SetAttribute("rate", data[offset + 1]);
+            std::string words;
+            for (unsigned int w = 1; w < 16; ++ w){
+                const unsigned int word = data[offset + w * 2] | (data[offset + w * 2 + 1] << 8);
+                words += (w > 1 ? " " : "") + std::to_string(word);
+            }
+            record->SetAttribute("words", words);
+            root->LinkEndChild(record.release());
+        }
+    }
+    doc.LinkEndChild(new TiXmlDeclaration("1.0", "", ""));
+    doc.LinkEndChild(root.release());
+    doc.SaveFile(output_dir_ + "/gamedata/world_encounters.xml");
+}
+
 bool WorldInstaller::GenerateScripts(
     const std::vector<std::string>& field_names,
     const std::map<u16, float>& field_scale_factors,
@@ -674,6 +727,7 @@ bool WorldInstaller::GenerateScripts(
     const VGears::LGPArchive::FileList& archive_entries = archive.GetFiles();
     std::vector<const VGears::LGPArchive::FileEntry*> event_entries;
     const VGears::LGPArchive::FileEntry* field_table_entry = nullptr;
+    const VGears::LGPArchive::FileEntry* encounter_entry = nullptr;
     bool has_wm0 = false;
     for (const VGears::LGPArchive::FileEntry& entry : archive_entries){
         std::string lower_name = entry.file_name;
@@ -696,6 +750,12 @@ bool WorldInstaller::GenerateScripts(
         else if (lower_name == "field.tbl"){
             field_table_entry = &entry;
         }
+        else if (lower_name == "enc_w.bin"){
+            encounter_entry = &entry;
+        }
+    }
+    if (encounter_entry != nullptr){
+        ExportEncounters(archive_file, *encounter_entry);
     }
     if (event_entries.empty() || !has_wm0 || field_table_entry == nullptr){
         error_message = "WM0.EV, other world event files, or FIELD.TBL were not found inside WORLD_US.LGP.";

+ 9 - 0
src/installer/WorldInstaller.h

@@ -21,6 +21,7 @@
 #include <common/TypeDefine.h>
 #include "DiskImage.h"
 #include "common/File.h"
+#include "data/VGearsLGPArchive.h"
 
 /**
  * The world map data installer.
@@ -76,6 +77,14 @@ class WorldInstaller{
          */
         bool ProcessMap();
 
+        /**
+         * Exports the random encounters of the world map to gamedata/world_encounters.xml.
+         *
+         * @param[in] archive_file World map archive file.
+         * @param[in] entry Entry of ENC_W.BIN in the archive.
+         */
+        void ExportEncounters(File& archive_file, const VGears::LGPArchive::FileEntry& entry);
+
                 bool GenerateScripts(
                     const std::vector<std::string>& field_names,
                     const std::map<u16, float>& field_scale_factors,

+ 93 - 422
src/installer/data/BattleSceneFile.cpp

@@ -148,7 +148,50 @@ void BattleSceneFile::Read(File file){
     }
     for (int a = 0; a < 32; a ++) scene_.attack_ids[a] = file.readU16LE();
     for (int a = 0; a < 32; a ++) for (int b = 0; b < 32; b ++) scene_.attack_names[a][b] = file.readU8();
-    // TODO: Keep reading for AI scripts.
+    // AI scripts. The AI block is at a fixed offset, and starts with an offset for each of the 3
+    // enemies of the scene (0xFFFF if the enemy has none). Each enemy block has 16 offsets
+    // (0xFFFF for slots without a script) followed by the scripts, which end with the opcode 0x73.
+    for (int e = 0; e < 3; e ++) ai_scripts_[e].assign(16, std::vector<u8>());
+    {
+        const u32 ai_base = 0x0E80;
+        const u32 file_size = file.GetFileSize();
+        if (file_size > ai_base + 6){
+            std::vector<u8> bytes(file_size);
+            file.GetFileBuffer(bytes.data(), 0, file_size);
+            u16 block_offsets[3];
+            for (int e = 0; e < 3; e ++) block_offsets[e] = bytes[ai_base + 2 * e] | (bytes[ai_base + 2 * e + 1] << 8);
+            for (int e = 0; e < 3; e ++){
+                if (block_offsets[e] == 0xFFFF) continue;
+                const u32 block_start = ai_base + block_offsets[e];
+                // The block ends where the next enemy block starts, or at the end of the file.
+                u32 block_end = file_size;
+                for (int n = e + 1; n < 3; n ++){
+                    if (block_offsets[n] != 0xFFFF){
+                        block_end = ai_base + block_offsets[n];
+                        break;
+                    }
+                }
+                if (block_start + 32 > file_size || block_end > file_size) continue;
+                u16 entries[16];
+                for (int k = 0; k < 16; k ++) entries[k] = bytes[block_start + 2 * k] | (bytes[block_start + 2 * k + 1] << 8);
+                for (int k = 0; k < 16; k ++){
+                    if (entries[k] == 0xFFFF) continue;
+                    // A script ends where the next one starts, or at the end of the block.
+                    u32 script_end = block_end - block_start;
+                    for (int n = 0; n < 16; n ++){
+                        if (entries[n] != 0xFFFF && entries[n] > entries[k] && entries[n] < script_end) script_end = entries[n];
+                    }
+                    if (entries[k] >= script_end || block_start + script_end > file_size) continue;
+                    std::vector<u8> script(
+                      bytes.begin() + block_start + entries[k], bytes.begin() + block_start + script_end
+                    );
+                    // Padding at the end.
+                    while (!script.empty() && script.back() == 0xFF) script.pop_back();
+                    ai_scripts_[e][k] = script;
+                }
+            }
+        }
+    }
 
     // All the data is read, now format it.
 
@@ -187,6 +230,22 @@ void BattleSceneFile::Read(File file){
             if (scene_.enemy_data[e].animation_index[a] != 0xFF)
                 enemy.animations.push_back(scene_.enemy_data[e].animation_index[a]);
         }
+        // Attacks. Each of the 16 slots has an attack ID, the animation script of the model to play
+        // and the camera to use.
+        // The AI scripts of the enemy.
+        enemy.ai_scripts = ai_scripts_[e];
+        for (int a = 0; a < 16; a ++){
+            if (scene_.enemy_data[e].attacks[a] == 0xFFFF) continue;
+            Enemy::Attack attack;
+            attack.id = scene_.enemy_data[e].attacks[a];
+            attack.animation
+              = (scene_.enemy_data[e].animation_index[a] != 0xFF)
+              ? scene_.enemy_data[e].animation_index[a] : -1;
+            attack.camera
+              = (scene_.enemy_data[e].attack_camera[a] != 0xFFFF)
+              ? scene_.enemy_data[e].attack_camera[a] : -1;
+            enemy.attacks.push_back(attack);
+        }
         for (int l = 0; l < 8; l ++){
             if (scene_.enemy_data[e].elements[l] != 0xFF && scene_.enemy_data[e].element_rates[l] != 0xFF ){
                 Enemy::Element element;
@@ -203,196 +262,13 @@ void BattleSceneFile::Read(File file){
                 enemy.elements.push_back(element);
             }
         }
+        // Immunities are a bitmask: the bit position is the status ID, as in the original game
+        // (0 death, 1 near death, 2 sleep, 3 poison, 4 sadness, 5 fury, 6 confusion, 7 silence...).
         if (scene_.enemy_data[e].immunities != 0xFFFFFFFF){
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000001)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::DEATH;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000002)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::NEAR_DEATH;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000004)){
+            for (int bit = 0; bit < 32; bit ++){
+                if ((scene_.enemy_data[e].immunities & (1u << bit)) == 0) continue;
                 Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SLEEP;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000008)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::POISONED;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000010)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SADNESS;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000020)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::FURY;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000040)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::CONFU;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000080)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SILENCE;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000100)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::HASTE;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000200)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SLOW;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000400)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::STOP;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00000800)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::FROG;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00001000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SMALL;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00002000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SLOW_NUMB;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00004000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::PETRIFY;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00008000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::REGEN;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00010000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::BARRIER;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00020000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::M_BARRIER;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00040000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::REFLECT;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00080000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::DUAL;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00100000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::SHIELD;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00200000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::D_SENTENCE;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00400000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::MANIPULATE;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x00800000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::BERSERK;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x01000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::PEERLESS;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x02000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::PARALYSIS;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x04000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::DARKNESS;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x08000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::DUAL_DRAIN;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x10000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::DEATH_FORCE;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x20000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::RESIST;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x40000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::LUCKY_GIRL;
-                immunity.rate = 1.0f;
-                enemy.immunities.push_back(immunity);
-            }
-            if (scene_.enemy_data[e].immunities == (scene_.enemy_data[e].immunities | 0x80000000)){
-                Enemy::Immunity immunity;
-                immunity.status = GameData::STATUS::IMPRISONED;
+                immunity.status = bit;
                 immunity.rate = 1.0f;
                 enemy.immunities.push_back(immunity);
             }
@@ -471,242 +347,31 @@ void BattleSceneFile::Read(File file){
             case 2: attack.damage.restore = GameData::RestoreType::STATUS; break;
             default: attack.damage.restore = GameData::RestoreType::NONE;
         }
-        int status_chance_raw = 63 - (scene_.attack_data[a].status_change ^ 63); // Out of 63;
-        int status_mode;
-        if (scene_.attack_data[a].status_change == (scene_.attack_data[a].status_change | 0x00000080))
-            status_mode = GameData::STATUS_EFFECT_MODE::TOGGLE;
-        else if (scene_.attack_data[a].status_change == (scene_.attack_data[a].status_change | 0x00000040))
-            status_mode = GameData::STATUS_EFFECT_MODE::TOGGLE;
-        else status_mode = GameData::STATUS_EFFECT_MODE::TOGGLE;
+        // Status attack byte: the lower 6 bits are the chance out of 63, and the upper 2 bits the
+        // mode (0x80 toggles the statuses, 0x40 cures them, otherwise they are inflicted).
+        // The status bitmask has the status IDs as bit positions, as in the original game.
+        const int status_chance_raw = scene_.attack_data[a].status_change & 0x3F;
+        int status_mode = GameData::STATUS_EFFECT_MODE::INFLICT;
+        if ((scene_.attack_data[a].status_change & 0x80) != 0) status_mode = GameData::STATUS_EFFECT_MODE::TOGGLE;
+        else if ((scene_.attack_data[a].status_change & 0x40) != 0) status_mode = GameData::STATUS_EFFECT_MODE::CURE;
         if (scene_.attack_data[a].status != 0xFFFFFFFF){
-            float status_chance = status_chance_raw / 63.0f;
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000001)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::DEATH;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000002)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::NEAR_DEATH;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000004)){
+            const float status_chance = status_chance_raw / 63.0f;
+            for (int bit = 0; bit < 32; bit ++){
+                if ((scene_.attack_data[a].status & (1u << bit)) == 0) continue;
                 Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SLEEP;
+                effect.id = bit;
                 effect.chance = status_chance;
                 effect.type = status_mode;
                 attack.status_effects.push_back(effect);
             }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000008)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::POISONED;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000010)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SADNESS;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000020)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::FURY;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000040)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::CONFU;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000080)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SILENCE;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000100)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::HASTE;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000200)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SLOW;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000400)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::STOP;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00000800)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::FROG;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00001000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SMALL;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00002000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SLOW_NUMB;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00004000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::PETRIFY;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00008000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::REGEN;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00010000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::BARRIER;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00020000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::M_BARRIER;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00040000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::REFLECT;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00080000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::DUAL;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00100000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::SHIELD;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00200000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::D_SENTENCE;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00400000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::MANIPULATE;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x00800000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::BERSERK;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x01000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::PEERLESS;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x02000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::PARALYSIS;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x04000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::DARKNESS;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x08000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::DUAL_DRAIN;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x10000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::DEATH_FORCE;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x20000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::RESIST;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x40000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::LUCKY_GIRL;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
-            }
-            if (scene_.attack_data[a].status == (scene_.attack_data[a].status | 0x80000000)){
-                Attack::StatusEffect effect;
-                effect.id = GameData::STATUS::IMPRISONED;
-                effect.chance = status_chance;
-                effect.type = status_mode;
-                attack.status_effects.push_back(effect);
+        }
+        // Attack elements are a bitmask (0x0001 is fire, 0x0002 ice...), while enemies elements are
+        // indexes (0 is fire, 1 is ice...). Elements are stored as indexes.
+        if (scene_.attack_data[a].element != 0xFFFF){
+            for (int bit = 0; bit < 16; bit ++){
+                if ((scene_.attack_data[a].element & (1 << bit)) != 0) attack.elements.push_back(bit);
             }
         }
-        if (scene_.attack_data[a].element >= 0 && scene_.attack_data[a].element <= 0x0F)
-            attack.elements.push_back(scene_.attack_data[a].element);
         attack.damage.damage_mp = (scene_.attack_data[a].special != (scene_.attack_data[a].special | 0x0001));
         attack.damage.affected_by_darkness = (scene_.attack_data[a].special != (scene_.attack_data[a].special | 0x0004));
         if ((scene_.attack_data[a].special != (scene_.attack_data[a].special | 0x0010))){
@@ -737,11 +402,17 @@ void BattleSceneFile::Read(File file){
         Formation formation;
         if (scene_.setup[f].next_battle != 0xFFFF) formation.next_formation = scene_.setup[f].next_battle;
         formation.escape_counter = (scene_.setup[f].escape_counter / 64.0f);
-        // TODO: What is flag 0x01? Multilimb enemies?
-        formation.escapable = (scene_.setup[f].flags == scene_.setup[f].flags | 0x02);
-        formation.skip_victory_pose = (scene_.setup[f].flags != scene_.setup[f].flags | 0x04);
-        formation.skip_spoils = (scene_.setup[f].flags != scene_.setup[f].flags | 0x08);
-        formation.preemptive_disabled = (scene_.setup[f].flags != scene_.setup[f].flags | 0x10);
+        // Observed in the original scene.bin (1024 setups): the upper byte is always 0xFF and
+        // bit 0x02 is clear in most random battles, so it means "can't escape" (inverted).
+        // TODO: What is flag 0x01?
+        formation.escapable = ((scene_.setup[f].flags & 0x02) == 0);
+        // TODO: Bits 0x04, 0x08 and 0x10 are set in almost every setup, so they can't be read as
+        // positive "skip victory pose", "skip spoils" and "preemptive disabled" flags (the previous
+        // code, due to operator precedence, always set all three). Use defaults until their
+        // meaning is verified against known battles.
+        formation.skip_victory_pose = false;
+        formation.skip_spoils = false;
+        formation.preemptive_disabled = false;
         // TODO: What is flag 0x20?
         // TODO: What is flag 0x40?
         // TODO: What is flag 0x80?

+ 5 - 0
src/installer/data/BattleSceneFile.h

@@ -637,6 +637,11 @@ class BattleSceneFile{
          */
         std::vector<Enemy> enemies_;
 
+        /**
+         * AI scripts of the 3 enemies of the scene, 16 slots each.
+         */
+        std::vector<std::vector<u8>> ai_scripts_[3];
+
         /**
          * Formations found in the scene.
          */

+ 46 - 95
src/installer/data/DaFile.cpp

@@ -18,7 +18,9 @@
 #include <cstdint>
 #include "data/DaFile.h"
 
-const float DaFile::OFFSET_SCALE = 0.0070312473867f;
+// Root offsets are in the same units as the model geometry (the root height of an idle Cloud is 466,
+// against leg bones of 277 and 253), so they are not scaled.
+const float DaFile::OFFSET_SCALE = 1.0f;
 
 DaFile::DaFile(File file): da_file_(file){
     animations_.clear();
@@ -42,7 +44,7 @@ std::vector<std::string> DaFile::GenerateAFiles(std::string model_id, std::strin
          * animation frame has three floats (of 4 bytes) for root rotation, three for root offset,
          * and after them, the bone rotation data.
          */
-        std::string file_index_name = std::to_string(anim);
+        std::string file_index_name = std::to_string(animations_[anim].index);
         while (file_index_name.size() < 2) file_index_name = "0" + file_index_name;
         std::string file_name = path + model_id + "_" + file_index_name + ".a";
         std::ofstream a(file_name, std::ios::out | std::ios::binary);
@@ -79,24 +81,13 @@ std::vector<std::string> DaFile::GenerateAFiles(std::string model_id, std::strin
 }
 
 int DaFile::ExtendSignInteger(int val, int len){
-    int result, aux;
-    if ((val & (int) pow(2, (len - 1))) != 0){
-        aux = (int) pow(2, 16) - 1;
-        aux ^= (int) (pow(2, len) - 1);
-        aux |= val;
-        result = aux;
-    }
-    else result = val;
-    return result;
+    // Two's complement value of "len" bits to a signed integer.
+    if (len > 0 && (val & (1 << (len - 1))) != 0) return val - (1 << len);
+    return val;
 }
 
 int DaFile::GetSignExtendedShort(int val, int len){
-    int result = 0;
-    if (len > 0){
-        if (len < 16) result = ExtendSignInteger(val, len);
-        else result = val;
-    }
-    return result;
+    return ExtendSignInteger(val, len);
 }
 
 void DaFile::Read(){
@@ -108,6 +99,7 @@ void DaFile::Read(){
     //for (int a = 0; a < 1; a ++){ // TODO DEBUG
         int start_offset = bit_offset_ / 8; // In bytes.
         Animation anim = ReadAnimation();
+        anim.index = a;
         if (anim.frame_count > 0) animations_.push_back(anim);
         // Manually go to the next animation
         bit_offset_ = (start_offset + anim.size + 12) * 8; // + 12 is for skipping the main header.
@@ -146,12 +138,13 @@ DaFile::Animation DaFile::ReadAnimation(){
 DaFile::Frame DaFile::ReadFirstFrame(u32 bones, u8 key){
     Frame frame;
     // Read root offset. In the first frame, always 16 bytes each, as short.
-    frame.offset.s_x = (int16_t) GetBitsS(16);
-    frame.offset.s_y = (int16_t) GetBitsS(16);
-    frame.offset.s_z = (int16_t) GetBitsS(16);
+    frame.offset.s_x = GetBitsS(16);
+    frame.offset.s_y = GetBitsS(16);
+    frame.offset.s_z = GetBitsS(16);
     // Turn offset to float. Any scaling that needs to be done would be done here.
     frame.offset.f_x = static_cast<float>(frame.offset.s_x) * OFFSET_SCALE;
-    frame.offset.f_y = static_cast<float>(frame.offset.s_y) * OFFSET_SCALE;
+    // Y points down in the original data, up in the converted models.
+    frame.offset.f_y = static_cast<float>(frame.offset.s_y) * -OFFSET_SCALE;
     frame.offset.f_z = static_cast<float>(frame.offset.s_z) * OFFSET_SCALE;
     // Read bones.
     for (int i = 0; i < bones; i ++){
@@ -159,13 +152,13 @@ DaFile::Frame DaFile::ReadFirstFrame(u32 bones, u8 key){
         // Now get each bone rotation (the first bone is actually the root, not part of the
         // skeleton). During the first frame, the rotations are always (12 - key) bits. The values
         // are shifted by key to align it to 12 bits.
-        bone.s_x = GetBitsS(12 - key) << key;
-        bone.s_y = GetBitsS(12 - key) << key;
-        bone.s_z = GetBitsS(12 - key) << key;
-        // Store the INT version as the absolute value of the SHORT version.
-        bone.i_x = (bone.s_x < 0) ? bone.s_x + 0x1000 : bone.s_x;
-        bone.i_y = (bone.s_y < 0) ? bone.s_y + 0x1000 : bone.s_y;
-        bone.i_z = (bone.s_z < 0) ? bone.s_z + 0x1000 : bone.s_z;
+        bone.s_x = GetBitsS(12 - key) * (1 << key);
+        bone.s_y = GetBitsS(12 - key) * (1 << key);
+        bone.s_z = GetBitsS(12 - key) * (1 << key);
+        // Store the INT version as the angle in the 0-4095 range.
+        bone.i_x = bone.s_x & 0xFFF;
+        bone.i_y = bone.s_y & 0xFFF;
+        bone.i_z = bone.s_z & 0xFFF;
         // Convert to float.
         bone.f_x = static_cast<float>(bone.i_x) / 4096.0f * 360.0f;
         bone.f_y = static_cast<float>(bone.i_y) / 4096.0f * 360.0f;
@@ -178,98 +171,56 @@ DaFile::Frame DaFile::ReadFirstFrame(u32 bones, u8 key){
 DaFile::Frame DaFile::ReadFrame(u32 bones, u8 key, Frame prev){
     Frame frame;
 
-    // Get dynamic root offset and add the previous frame values.
-    // Note: in compressed frames, offset delta is a signed integer, so skip the short value
+    // Get the root offset delta and add it to the previous frame values. Deltas are in the same
+    // units as the first frame offset, so they get the same scale.
     frame.offset.s_x = 0;
     frame.offset.s_y = 0;
     frame.offset.s_z = 0;
     frame.offset.i_x = GetDynamicOffsetFromStream();
     frame.offset.i_y = GetDynamicOffsetFromStream();
     frame.offset.i_z = GetDynamicOffsetFromStream();
-    frame.offset.f_x = static_cast<float>(frame.offset.i_x) + prev.offset.f_x;
-    frame.offset.f_y = static_cast<float>(frame.offset.i_y) * -1 + prev.offset.f_y; // Invert Y.
-    frame.offset.f_z = static_cast<float>(frame.offset.i_z) + prev.offset.f_z;
+    frame.offset.f_x = static_cast<float>(frame.offset.i_x) * OFFSET_SCALE + prev.offset.f_x;
+    frame.offset.f_y = static_cast<float>(frame.offset.i_y) * -OFFSET_SCALE + prev.offset.f_y;
+    frame.offset.f_z = static_cast<float>(frame.offset.i_z) * OFFSET_SCALE + prev.offset.f_z;
 
     for (int i = 0; i < bones; i ++){
         TriValue bone;
-        // The same applies here.  Add the offsets and convert to INT form, adding 0x1000 if it is
-        // less than 0. When Final Fantasy VII loads these animations, it is possible for the value
-        // to sneak up above the 4095 boundary through a series of positive offsets.
+        // Rotations are 12 bit angles (4096 is a full turn). Deltas are added to the previous
+        // value, already shifted to 12 bits, and the result wraps around a full turn.
         bone.s_x = GetCompressedDeltaFromStream(key) + prev.bones[i].s_x;
         bone.s_y = GetCompressedDeltaFromStream(key) + prev.bones[i].s_y;
         bone.s_z = GetCompressedDeltaFromStream(key) + prev.bones[i].s_z;
-        bone.i_x = (bone.s_x < 0) ? bone.s_x + 0x1000 : bone.s_x;
-        bone.i_y = (bone.s_y < 0) ? bone.s_y + 0x1000 : bone.s_y;
-        bone.i_z = (bone.s_z < 0) ? bone.s_z + 0x1000 : bone.s_z;
-        bone.f_x = static_cast<float>(bone.i_x) / pow(2, 12 - key) * 360.0f;
-        bone.f_y = static_cast<float>(bone.i_y) / pow(2, 12 - key) * 360.0f;
-        bone.f_z = static_cast<float>(bone.i_z) / pow(2, 12 - key) * 360.0f;
+        bone.i_x = bone.s_x & 0xFFF;
+        bone.i_y = bone.s_y & 0xFFF;
+        bone.i_z = bone.s_z & 0xFFF;
+        bone.f_x = static_cast<float>(bone.i_x) / 4096.0f * 360.0f;
+        bone.f_y = static_cast<float>(bone.i_y) / 4096.0f * 360.0f;
+        bone.f_z = static_cast<float>(bone.i_z) / 4096.0f * 360.0f;
         frame.bones.push_back(bone);
     }
     return frame;
 }
 
 int DaFile::GetDynamicOffsetFromStream(){
-    int value;
-    int control_bit = GetBitsS(1);
-    if (control_bit == 1) value = GetBitsS(16);
-    else value = GetBitsS(7);
-    if (value > 0x7fff) value = (value - 0xffff);
-    return value;
+    // The control bit is an unsigned flag: 1 for a 16 bit value, 0 for a 7 bit one.
+    if (GetBitsU(1) == 1) return GetBitsS(16);
+    return GetBitsS(7);
 }
 
 int DaFile::GetBitsU(int bits){
+    // Bits are read most significant first.
     int value = 0;
-    int base_byte, bi, res, num_bytes, unaligned_by_bits;
-    int first_aligned_byte, last_aligned_byte, end_bits;
-    bool is_aligned, clean_end;
-    if (bits < 1) return value;
-    base_byte = bit_offset_ / 8;
-    unaligned_by_bits = bit_offset_ % 8;
-
-    if (unaligned_by_bits + bits > 8){
-        is_aligned = (unaligned_by_bits == 0);
-        end_bits = (bit_offset_ + bits) % 8;
-        clean_end = (end_bits == 0);
-        num_bytes =
-          (bits - (is_aligned ? 0 : 8 - unaligned_by_bits) - (clean_end ? 0 : end_bits)) / 8
-          + (is_aligned ? 0 : 1) + (clean_end ? 0 : 1);
-        last_aligned_byte = num_bytes - (clean_end ? 0 : 1) - 1;
-        first_aligned_byte = 0;
-        res = 0;
-        // Unaligned prefix: stored at the beginning of the byte
-        if (!is_aligned){
-            res = da_bytes_[base_byte];
-            res &= (int) (pow(2, (8 - unaligned_by_bits)) - 1);
-            first_aligned_byte = 1;
-        }
-        // Aligned bytes
-        for (bi = first_aligned_byte; bi <= last_aligned_byte; bi ++){
-            res *= 256;
-            res |= da_bytes_[base_byte + bi];
-        }
-        // Suffix: Stored at the end of the byte
-        if (!clean_end){
-            res *= (int) pow(2, end_bits);
-            res |= (
-              (da_bytes_[base_byte + last_aligned_byte + 1]) / (int) (pow(2, 8 - end_bits))
-              & (int) (pow(2, end_bits) - 1)
-            );
-        }
-    }
-    else{
-        res = da_bytes_[base_byte];
-        res /= (int) pow(2, 8 - (unaligned_by_bits + bits));
-        res &= (int) (pow(2, bits) - 1);
+    for (int i = 0; i < bits; i ++){
+        const int byte = da_bytes_[bit_offset_ / 8];
+        const int bit = (byte >> (7 - (bit_offset_ % 8))) & 1;
+        value = (value << 1) | bit;
+        bit_offset_ ++;
     }
-    value = (short)res;
-    bit_offset_ += bits;
     return value;
 }
 
 int DaFile::GetBitsS(int bits){
-    int tmp = GetBitsU(bits);
-    return GetSignExtendedShort(tmp, bits);
+    return GetSignExtendedShort(GetBitsU(bits), bits);
 }
 
 int DaFile::GetCompressedDeltaFromStream(int lowered_precision_bits){
@@ -285,7 +236,7 @@ int DaFile::GetCompressedDeltaFromStream(int lowered_precision_bits){
             case 1: case 2: case 3: case 4: case 5: case 6:
                 bits = key;
                 // Read a corresponding number of bits from the stream.
-                temp = (int16_t) GetBitsS(bits);
+                temp = GetBitsS(bits);
                 //  Invert the value of the last bit
                 sign = (int) pow(2, bits - 1);
                 // Set sign.
@@ -297,7 +248,7 @@ int DaFile::GetCompressedDeltaFromStream(int lowered_precision_bits){
             case 7:
                 // Read an uncompressed value from the stream (at requested precision),
                 // and return.
-                temp = (int16_t) GetBitsS(12 - lowered_precision_bits);
+                temp = GetBitsS(12 - lowered_precision_bits);
                 return (temp << lowered_precision_bits);
         }
     }

+ 13 - 3
src/installer/data/DaFile.h

@@ -15,6 +15,8 @@
 
 #pragma once
 
+#include <cstdint>
+
 #include <vector>
 #include "common/File.h"
 #include "common/TypeDefine.h"
@@ -79,17 +81,17 @@ class DaFile{
             /**
              * X value, short format.
              */
-            char16_t s_x;
+            int16_t s_x;
 
             /**
              * Y value, short format.
              */
-            char16_t s_y;
+            int16_t s_y;
 
             /**
              * Z value, short format
              */
-            char16_t s_z;
+            int16_t s_z;
 
             /**
              * X value, integer format.
@@ -140,6 +142,14 @@ class DaFile{
 
         struct Animation{
 
+            /**
+             * Index of the animation in the original file.
+             *
+             * Animations too small to be real ones are skipped, but the others keep their original
+             * index, as it's the number used by the animation scripts.
+             */
+            u32 index;
+
             /**
              * Number of bones in the animation.
              */

+ 15 - 0
src/installer/data/Enemy.h

@@ -59,6 +59,12 @@ struct Enemy{
          * -1 to not move the camera.
          */
         int camera;
+
+        /**
+         * Animation script of the model to play for the attack, as in the model animation scripts
+         * ("scriptN"). -1 if there is none.
+         */
+        int animation;
     };
 
     /**
@@ -196,6 +202,15 @@ struct Enemy{
      */
     std::vector<Attack> attacks;
 
+    /**
+     * Enemy AI scripts, as bytecode.
+     *
+     * There are 16 slots: 0 initialization, 1 main (when it's the enemy turn), 2 general counter,
+     * 3 death counter, 4 physical counter, 5 magic counter, 6 battle end, 7 pre-turn, and 8 to 15
+     * custom event scripts. Empty vectors are slots without script.
+     */
+    std::vector<std::vector<u8>> ai_scripts;
+
     /**
      * List of attacks that can be used while manipulated.
      *

+ 14 - 12
src/installer/data/FF7Data.h

@@ -2635,7 +2635,7 @@ class FF7Data{
             else if("pi" == alphanumeric_id){
                 info.numeric_id = 0x1c;
                 info.name = "Sector 7 Support Pillar Top";
-                info.name_normal = "sector_7_pillar top";
+                info.name_normal = "sector_7_pillar_top";
                 info.is_scene = true;
             }
             else if("pj" == alphanumeric_id){
@@ -2830,48 +2830,50 @@ class FF7Data{
                 info.name_normal = "blank_2";
                 info.is_scene = true;
             }
+            // Battle location IDs follow the alphabetical order of the model IDs (og = 0, oh = 1...).
+            // Unverified labels for qp-qv were matched against the location names in formations.
             else if("qp" == alphanumeric_id){
-                info.numeric_id = 0x3d;
-                info.name = "unkonwon";
-                info.name_normal = "unknown_qp";
-                info.is_scene = true;
-            }
-            else if("qq" == alphanumeric_id){
                 info.numeric_id = 0x3d;
                 info.name = "Corel Railway - Canyon";
                 info.name_normal = "railway_canyon";
                 info.is_scene = true;
             }
-            else if("qr" == alphanumeric_id){
+            else if("qq" == alphanumeric_id){
                 info.numeric_id = 0x3e;
                 info.name = "Whirlwind Maze - Crater";
                 info.name_normal = "whirlwind_maze_crater";
                 info.is_scene = true;
             }
-            else if("qs" == alphanumeric_id){
+            else if("qr" == alphanumeric_id){
                 info.numeric_id = 0x3f;
                 info.name = "Corel Railway - Rollercoaster";
                 info.name_normal = "railway_rollercoaster";
                 info.is_scene = true;
             }
-            else if("qt" == alphanumeric_id){
+            else if("qs" == alphanumeric_id){
                 info.numeric_id = 0x40;
                 info.name = "Wooden Bridge";
                 info.name_normal = "wooden_bridge";
                 info.is_scene = true;
             }
-            else if("qu" == alphanumeric_id){
+            else if("qt" == alphanumeric_id){
                 info.numeric_id = 0x41;
                 info.name = "Da Chao";
                 info.name_normal = "da_chao";
                 info.is_scene = true;
             }
-            else if("qv" == alphanumeric_id){
+            else if("qu" == alphanumeric_id){
                 info.numeric_id = 0x42;
                 info.name = "Fort Condor";
                 info.name_normal = "fort_condor";
                 info.is_scene = true;
             }
+            else if("qv" == alphanumeric_id){
+                info.numeric_id = 0x43;
+                info.name = "Dirt Wasteland";
+                info.name_normal = "dirt_wasteland";
+                info.is_scene = true;
+            }
             else if("qw" == alphanumeric_id){
                 info.numeric_id = 0x44;
                 info.name = "Bizarro Battle - Right Side";

+ 1 - 1
src/installer/data/Formation.h

@@ -73,7 +73,7 @@ struct Formation{
          * Like a normal one, but all players in the front row. Change command
          * disabled.
          */
-        LOCKED
+        LOCKED = 8
     };
 
     /**

+ 8 - 1
src/installer/decompiler/field/instruction/FieldMediaInstruction.cpp

@@ -38,7 +38,7 @@ void FieldMediaInstruction::ProcessInst(
         case OPCODES::MUSVT: code_gen->WriteTodo(md.GetEntityName(), "MUSVT"); break;
         case OPCODES::MUSVM: code_gen->WriteTodo(md.GetEntityName(), "MUSVM"); break;
         case OPCODES::MULCK: ProcessMULCK(code_gen); break;
-        case OPCODES::BMUSC: code_gen->WriteTodo(md.GetEntityName(), "BMUSC"); break;
+        case OPCODES::BMUSC: ProcessBMUSC(code_gen); break;
         case OPCODES::CHMPH: code_gen->WriteTodo(md.GetEntityName(), "CHMPH"); break;
         case OPCODES::PMVIE: ProcessPMVIE(code_gen); break;
         case OPCODES::MOVIE: ProcessMOVIE(code_gen); break;
@@ -77,6 +77,13 @@ void FieldMediaInstruction::ProcessAKAO2(CodeGenerator* code_gen){
     ).str());
 }
 
+void FieldMediaInstruction::ProcessBMUSC(CodeGenerator* code_gen){
+    // Battle music: one of the music tracks of the field, used by the battles of the field.
+    code_gen->AddOutputLine(
+      (boost::format("audio_manager:set_battle_track_id(%1%)") % params_[0]->GetUnsigned()).str()
+    );
+}
+
 void FieldMediaInstruction::ProcessMUSIC(CodeGenerator* code_gen){
     code_gen->AddOutputLine(
       (boost::format("play_map_music(%1%)") % params_[0]->GetUnsigned()).str()

+ 2 - 0
src/installer/decompiler/field/instruction/FieldMediaInstruction.h

@@ -43,6 +43,8 @@ class FieldMediaInstruction : public KernelCallInstruction{
 
         void ProcessAKAO2(CodeGenerator* code_gen);
 
+        void ProcessBMUSC(CodeGenerator* code_gen);
+
         void ProcessMUSIC(CodeGenerator* code_gen);
 
         void ProcessSOUND(CodeGenerator* code_gen);

+ 8 - 1
src/installer/decompiler/field/instruction/FieldModuleInstruction.cpp

@@ -81,7 +81,7 @@ void FieldModuleInstruction::ProcessInst(
         case OPCODES::LSTMP: code_gen->WriteTodo(md.GetEntityName(), "LSTMP"); break;
         case OPCODES::BATTLE: ProcessBATTLE(code_gen); break;
         case OPCODES::BTLON: ProcessBTLON(code_gen); break;
-        case OPCODES::BTLMD: code_gen->WriteTodo(md.GetEntityName(), "BTLMD"); break;
+        case OPCODES::BTLMD: ProcessBTLMD(code_gen); break;
         case OPCODES::MPJPO:
             // Gateway function will do nothing if this is set to true
             code_gen->AddOutputLine(
@@ -113,6 +113,13 @@ void FieldModuleInstruction::ProcessBATTLE(CodeGenerator* code_gen){
     code_gen->AddOutputLine((boost::format("Battle.start(%1%)") % battle_id).str());
 }
 
+void FieldModuleInstruction::ProcessBTLMD(CodeGenerator* code_gen){
+    // Battle mode: flags for the next battles, see BattleManager::ScriptSetMode.
+    code_gen->AddOutputLine(
+      (boost::format("battle_manager:set_mode(%1%)") % params_[0]->GetUnsigned()).str()
+    );
+}
+
 void FieldModuleInstruction::ProcessBTLON(CodeGenerator* code_gen){
     code_gen->AddOutputLine((
       boost::format("entity_manager:random_encounters_on(%1%)")

+ 2 - 0
src/installer/decompiler/field/instruction/FieldModuleInstruction.h

@@ -85,6 +85,8 @@ class FieldModuleInstruction : public KernelCallInstruction{
          *
          * @param[in,out] code_gen Code generator to append lines.
          */
+        void ProcessBTLMD(CodeGenerator* code_gen);
+
         void ProcessBTLON(CodeGenerator* code_gen);
 
         /**

+ 154 - 6
src/modules/worldmap/WorldmapModule.cpp

@@ -29,6 +29,9 @@
 #include <set>
 #include <string>
 #include <vector>
+#include <sstream>
+#include <cstdlib>
+#include <tinyxml.h>
 #include <OgreEntity.h>
 #include <OgreSceneManager.h>
 #include <OgreManualObject.h>
@@ -41,6 +44,7 @@
 #include <OgreResourceGroupManager.h>
 #include "data/VGearsTexFile.h"
 #include "core/InputManager.h"
+#include "core/Logger.h"
 #include "core/EntityManager.h"
 #include "core/EntityModel.h"
 #include "core/DialogsManager.h"
@@ -595,7 +599,12 @@ namespace VGears {
       active_function_id_(-1),
       initialized_(false),
       frame_listener_attached_(false),
-      cancel_was_pressed_(false)
+      cancel_was_pressed_(false),
+      encounter_frames_(0),
+      encounter_accumulator_(0),
+      encounter_last_battle_(-1),
+      encounters_locked_(false),
+      paused_for_battle_(false)
     {}
 
     WorldmapModule::~WorldmapModule(){
@@ -658,6 +667,8 @@ namespace VGears {
                     terrain_triangle.function_id = triangle.function_id;
                     terrain_triangle.mesh_x = static_cast<int>(map_x * 4 + mesh_x);
                     terrain_triangle.mesh_y = static_cast<int>(map_y * 4 + mesh_y);
+                    // The 5 lower bits of the location are the region of the random encounters.
+                    terrain_triangle.region = std::min<int>(triangle.Location & 0x1F, 15);
                     terrain_triangles_.push_back(terrain_triangle);
 
                     if (unavailable_texture_ids.find(triangle.TextureInfo)
@@ -1096,7 +1107,7 @@ namespace VGears {
       }
 
     bool WorldmapModule::frameStarted(const Ogre::FrameEvent& event){
-      if (!initialized_ || player_entity_ == nullptr) return true;
+      if (!initialized_ || player_entity_ == nullptr || paused_for_battle_) return true;
 
         ::Entity* active_entity = EntityManager::getSingleton().ScriptGetPlayerEntity();
       if (active_entity == player_entity_){
@@ -1139,10 +1150,16 @@ namespace VGears {
               ? submarine_entity_ : tiny_bronco_entity_))));
 
         Ogre::Vector3 movement = Ogre::Vector3::ZERO;
-        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_LEFT)) movement.x -= 1.0f;
-        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_RIGHT)) movement.x += 1.0f;
-        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_UP)) movement.z -= 1.0f;
-        if (InputManager::getSingleton().IsButtonPressed(OIS::KC_DOWN)) movement.z += 1.0f;
+        // Scripts lock the player during events and transitions.
+        if (EntityManager::getSingleton().IsPlayerLocked()){
+            // Nothing: no movement.
+        }
+        else{
+            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_LEFT)) movement.x -= 1.0f;
+            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_RIGHT)) movement.x += 1.0f;
+            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_UP)) movement.z -= 1.0f;
+            if (InputManager::getSingleton().IsButtonPressed(OIS::KC_DOWN)) movement.z += 1.0f;
+        }
         bool is_walking = false;
         if (!movement.isZeroLength()){
           movement.normalise();
@@ -1182,6 +1199,9 @@ namespace VGears {
                         active_function_mesh_index_ = -1;
                         active_function_id_ = -1;
                     }
+                    if (current_vehicle_ == VehicleType::ON_FOOT){
+                        UpdateEncounters(event.timeSinceLastFrame, triangle);
+                    }
                     const Ogre::Vector3 model_forward(0.0f, 0.0f, 1.0f);
                     const Ogre::Vector3 move_direction(movement.x, 0.0f, movement.z);
                     const Ogre::Quaternion heading
@@ -1222,6 +1242,133 @@ namespace VGears {
         camera->lookAt(target);
       }
 
+    void WorldmapModule::LoadEncounters(){
+        encounter_records_.clear();
+        TiXmlDocument document("./data/gamedata/world_encounters.xml");
+        if (!document.LoadFile()){
+            LOG_WARNING("World map encounters were not found. There will be no random battles.");
+            return;
+        }
+        TiXmlHandle handle(&document);
+        for (
+          TiXmlElement* record = handle.FirstChildElement("world_encounters")
+            .FirstChildElement("record").ToElement();
+          record != nullptr; record = record->NextSiblingElement("record")
+        ){
+            EncounterRecord data;
+            int enabled = 0;
+            data.rate = 0;
+            data.terrain = 0;
+            record->QueryIntAttribute("enabled", &enabled);
+            record->QueryIntAttribute("rate", &data.rate);
+            record->QueryIntAttribute("terrain", &data.terrain);
+            data.enabled = enabled != 0;
+            for (unsigned int i = 0; i < 15; ++ i) data.words[i] = 0;
+            std::istringstream words(record->Attribute("words") ? record->Attribute("words") : "");
+            for (unsigned int i = 0; i < 15; ++ i){
+                unsigned int word = 0;
+                if (!(words >> word)) break;
+                data.words[i] = static_cast<uint16>(word);
+            }
+            encounter_records_.push_back(data);
+        }
+        LOG_TRIVIAL("World map encounters loaded: " + std::to_string(encounter_records_.size()));
+    }
+
+    int WorldmapModule::RollEncounterBattle(const EncounterRecord& record) const{
+        // Same order as the original game: the special battles first, then the standard ones. The
+        // chance of a word is its probability out of 64, as the roll is a 16 bit value and the
+        // probability sits in the 6 highest bits.
+        const uint16* w = record.words;
+        int battle = -1;
+        int roll = (std::rand() % 256) << 8;
+        // Words 7 and 8 (indices 6 and 7): back attacks.
+        if (w[6] > roll) battle = w[6] & 0x3FF;
+        else if (w[6] + w[7] > roll) battle = w[7] & 0x3FF;
+        // Word 9: side attack. Needs a party of more than one member.
+        if (battle < 0){
+            roll = (std::rand() % 256) << 8;
+            if (w[8] > roll) battle = w[8] & 0x3FF;
+        }
+        // Word 10: pincer attack.
+        if (battle < 0){
+            roll = (std::rand() % 256) << 8;
+            if (w[9] > roll) battle = w[9] & 0x3FF;
+        }
+        // Words 1-6: standard battles. The last battle is not repeated (one reroll).
+        for (unsigned int attempt = 0; battle < 0 && attempt < 2; ++ attempt){
+            roll = (std::rand() % 256) << 8;
+            int sum = 0;
+            for (unsigned int i = 0; i < 6; ++ i){
+                sum += w[i];
+                if (sum > roll){
+                    battle = w[i] & 0x3FF;
+                    break;
+                }
+            }
+            if (battle == encounter_last_battle_ && attempt == 0) battle = -1;
+        }
+        return battle;
+    }
+
+    void WorldmapModule::UpdateEncounters(const float delta_time, const TerrainTriangle& triangle){
+        if (encounters_locked_ || encounter_records_.size() < 64) return;
+        // The original game checks every 16 frames of walking, at 30 frames per second.
+        encounter_frames_ += delta_time * 30.0f;
+        if (encounter_frames_ < 16.0f) return;
+        encounter_frames_ -= 16.0f;
+
+        // Region and terrain group. The terrain types 0x10 and 0x18 count as 0 and 8.
+        const int region = std::min<int>(triangle.region, 15);
+        int terrain = triangle.walkability_type;
+        if (terrain == 0x10) terrain = 0;
+        else if (terrain == 0x18) terrain = 8;
+        int group = 0;
+        for (int i = 0; i < 4; ++ i){
+            if (encounter_records_[region * 4 + i].terrain == terrain){
+                group = i;
+                break;
+            }
+        }
+        const EncounterRecord& record = encounter_records_[region * 4 + group];
+
+        // The accumulator grows on every check, so the chance gets higher until a battle starts.
+        const float multiplier = EntityManager::getSingleton().GetEncounterMultiplier();
+        if (multiplier <= 0.0f) return;
+        const int speed = static_cast<int>(16 * multiplier);
+        encounter_accumulator_ += (record.rate != 0) ? (speed << 10) / record.rate : 0x7FFF;
+        const int check = std::rand() % 256;
+        if (EntityManager::getSingleton().IsDebugEncounters()){
+            LOG_TRIVIAL(
+              "World encounter check: region " + std::to_string(region) + ", terrain "
+              + std::to_string(terrain) + ", group " + std::to_string(group) + ", rate "
+              + std::to_string(record.rate) + ", chance " + std::to_string(encounter_accumulator_ >> 8)
+              + "/256, roll " + std::to_string(check)
+            );
+        }
+        if (check >= (encounter_accumulator_ >> 8) || !record.enabled) return;
+        encounter_accumulator_ = 0;
+        const int battle = RollEncounterBattle(record);
+        if (battle < 0) return;
+        encounter_last_battle_ = battle;
+        LOG_TRIVIAL("World random encounter: battle " + std::to_string(battle));
+        ScriptManager::getSingleton().RunString("Encounter.start(" + std::to_string(battle) + ")");
+    }
+
+    void WorldmapModule::PauseForBattle(){
+        paused_for_battle_ = true;
+        if (world_map_node_ != nullptr) world_map_node_->setVisible(false);
+    }
+
+    void WorldmapModule::ResumeAfterBattle(){
+        paused_for_battle_ = false;
+        encounter_frames_ = 0;
+        if (world_map_node_ != nullptr) world_map_node_->setVisible(true);
+        if (player_entity_ != nullptr) UpdateCamera(0, true);
+    }
+
+    void WorldmapModule::SetEncountersLocked(const bool locked){encounters_locked_ = locked;}
+
     void WorldmapModule::Init(){
       if (initialized_){
         if (frame_listener_attached_) return;
@@ -1238,6 +1385,7 @@ namespace VGears {
         CreateWorldMap(Root::getSingleton().getSceneManager("Scene"));
         ScriptManager::getSingleton().RunFile("scripts/worldmap/wm0.lua");
         CreatePlayer();
+        LoadEncounters();
         initialized_ = true;
         Root::getSingleton().addFrameListener(this);
       frame_listener_attached_ = true;

+ 42 - 0
src/modules/worldmap/WorldmapModule.h

@@ -60,6 +60,23 @@ namespace VGears {
             int GetTerrainTriangleUnderEntity(const char* entity_name) const;
             bool UnboardVehicle(const char* vehicle_name);
 
+            /**
+             * Hides the world map and stops it while a battle is on.
+             */
+            void PauseForBattle();
+
+            /**
+             * Shows the world map again after a battle.
+             */
+            void ResumeAfterBattle();
+
+            /**
+             * Prevents or allows random encounters (the SetBattleLock opcode).
+             *
+             * @param[in] locked True to prevent them.
+             */
+            void SetEncountersLocked(const bool locked);
+
         private:
 
             struct TerrainTriangle{
@@ -68,6 +85,21 @@ namespace VGears {
                 uint8 function_id;
                 int mesh_x;
                 int mesh_y;
+                /// Encounter region of the triangle, 0-15.
+                uint8 region;
+            };
+
+            /**
+             * Random encounters of a region and terrain group, from ENC_W.BIN.
+             */
+            struct EncounterRecord{
+                bool enabled;
+                /// Lower values mean more frequent encounters.
+                int rate;
+                /// Walkability type that selects this record.
+                int terrain;
+                /// Words 1-15 of the record: battle ID (10 bits) and probability (6 bits).
+                uint16 words[15];
             };
 
             void CreateWorldMap(Ogre::SceneManager* scene_manager);
@@ -89,6 +121,16 @@ namespace VGears {
             ) const;
             bool IsActiveTerrainAllowed(uint8 walkability_type) const;
             void TriggerTriangleFunction(const TerrainTriangle& triangle);
+            void LoadEncounters();
+            void UpdateEncounters(const float delta_time, const TerrainTriangle& triangle);
+            int RollEncounterBattle(const EncounterRecord& record) const;
+
+            std::vector<EncounterRecord> encounter_records_;
+            float encounter_frames_;
+            int encounter_accumulator_;
+            int encounter_last_battle_;
+            bool encounters_locked_;
+            bool paused_for_battle_;
 
             std::vector<TerrainTriangle> terrain_triangles_;
             Ogre::SceneManager* scene_manager_;

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff