diff --git a/README.md b/README.md index 5eaf598..2f4408a 100644 --- a/README.md +++ b/README.md @@ -108,16 +108,23 @@ Actions - [ ] slippery floor? - [ ] entity component system - [ ] refactor - - [ ] extract all features into components - - [ ] all player actions can be performed by entities - - [ ] ai for entities to choose action - - [ ] control player movement with a spell + - [x] extract all features into components + - [x] all player actions can be performed by entities + - [ ] enemy sneak makes them not visible + - [ ] use the same move logic for everything + - [x] ai for entities to choose action + - [x] choose target + - [x] choose other actions + - [x] kick player + - [ ] control player movement with a spell - [x] enemies have inventory - [x] player and enemies drop inventory the same way - - [ ] enemies have view_distance - - [ ] target for enemies and friends - - [ ] enemies pickup items - - [ ] enemies use weapons + - [x] enemies have view_distance + - [x] target for enemies + - [ ] target for friends + - [x] enemies pickup items + - [ ] but don't pickup everything + - [x] enemies use weapons - [ ] npcs - [ ] friends - [x] enchant enemies into friends @@ -143,6 +150,8 @@ Actions - [ ] dot repeat - doesn't seem possible, as game loop sets the last change - [ ] macros + - [ ] marks not working + - [ ] undo? - [x] varied entities - [x] item and enemy generation from dictionary - [x] item and enemy variations with different colours @@ -150,6 +159,8 @@ Actions - [x] generate random map progressively - [x] have some structure to generated maps - [x] new buffer for down + - [ ] add enemies and items in a later stage + - [ ] pick a selection, not fully random - [ ] performance - [x] write buffer once per tick - [x] store map as 2D array diff --git a/lua/neohack/actions.lua b/lua/neohack/actions.lua index 124cbd3..dc5216f 100644 --- a/lua/neohack/actions.lua +++ b/lua/neohack/actions.lua @@ -56,13 +56,13 @@ M.actions = { ---comment drop = function(request) - state.player.inventory:drop({ request.object }) + state.player.inventory:drop(request.object) M.tick() end, ---comment say = function(request) - state.player.speak:say({ request.object, request.target }) + state.player.speak:say(request.object, request.target) M.tick() end, @@ -265,7 +265,7 @@ M.prompt_wait = function() local str = M.prompt_one_word("Wait how long?") if str then -- M.insert_action("wait " .. str) - M.actions.wait({ object = str }) + M.actions.wait({ action = "wait", object = str }) end end, 50) end diff --git a/lua/neohack/buffer.lua b/lua/neohack/buffer.lua index d52c685..f842cc0 100644 --- a/lua/neohack/buffer.lua +++ b/lua/neohack/buffer.lua @@ -2,6 +2,7 @@ --- local view_buffer = require("neohack.view_buffer") +local Def = require("neohack.def") ---@class Buffer --- @field bufnr integer @@ -125,11 +126,17 @@ M.tick = function(callback) M.write_buf(nil) end ---- Get the entity at a specific position in a specific buffer +--- Get the entity at a specific position in a specific buffer, including player ---@param row integer ---@param col integer +---@param skip_player boolean? ---@return Entity -M.get_entity_at_pos = function(row, col) +M.get_entity_at_pos = function(row, col, skip_player) + if not skip_player then + if state.player.movement.row == row and state.player.movement.col == col then + return state.player + end + end local bufnr = state.current_bufnr local line = M.buffers[bufnr].cells[row] if not line then @@ -156,7 +163,7 @@ end M.get_under_cursor = function() local row, col = unpack(vim.api.nvim_win_get_cursor(0)) col = col + 1 - local entity = M.get_entity_at_pos(row, col) + local entity = M.get_entity_at_pos(row, col, true) -- message.notify("under cursor " .. row .. " " .. col .. " " .. entity.movement.char) return row, col, entity end @@ -174,9 +181,12 @@ M.set_entity_at_cell = function(row, col, entity) if not buf then error("no buffer") end - -- error("set entity at " .. row .. " " .. col) - -- message.notify(vim.inspect(buf.cells[row][col])) - buf.cells[row][col] = entity + -- don't set the player char + if entity.type ~= Def.DefType.player then + -- error("set entity at " .. row .. " " .. col) + -- message.notify(vim.inspect(buf.cells[row][col])) + buf.cells[row][col] = entity + end end --- Insert an entity at a specific position in a specific buffer diff --git a/lua/neohack/components/attributes.lua b/lua/neohack/components/attributes.lua index b040139..2c335ac 100644 --- a/lua/neohack/components/attributes.lua +++ b/lua/neohack/components/attributes.lua @@ -12,6 +12,7 @@ ---@field hit_highlight string ---Decision ---@field randomness number +---@field target Entity? ---Eat ---@field eat_attribute number ---Fuse diff --git a/lua/neohack/components/combat.lua b/lua/neohack/components/combat.lua index 1405fe0..af01114 100644 --- a/lua/neohack/components/combat.lua +++ b/lua/neohack/components/combat.lua @@ -32,13 +32,16 @@ function Combat.new(parent, damage, durability, hit_rate, default_weapon) --other default_weapon = default_weapon, } - --attributes - parent.attributes.dodge_attribute = 0.5 + --as an attacker attributes + --TODO: generate these for enemies + parent.attributes.dodge_attribute = 0.1 parent.attributes.deflect_attribute = 0.01 - parent.attributes.weapon_attribute = 0.5 + parent.attributes.weapon_attribute = 0.1 + --as a weapon attribute + parent.attributes.hit_rate = hit_rate parent.attributes.damage = damage parent.attributes.durability = durability - parent.attributes.hit_rate = hit_rate + --status parent.attributes.hit_highlight = nil @@ -48,11 +51,13 @@ end function Combat:inspect() return "durability=" - .. self.parent.attributes.durability + .. (self.parent.attributes.durability or "") .. " damage=" - .. self.parent.attributes.damage + .. (self.parent.attributes.damage or "") + .. " weapon_damage=" + .. (self:get_weapon().attributes.damage or "") .. " hit_rate=" - .. self.parent.attributes.hit_rate + .. (self.parent.attributes.hit_rate or "") .. " dodge_skill=" .. self:dodge_skill() .. " weapon_skill=" @@ -87,14 +92,14 @@ end ---@return number function Combat:dodge_skill() return self.parent.attributes.dodge_attribute - + self.parent.inventory:wear_effect(state.slot_types.feet, attributes.randomness, 0.3) - + self.parent.inventory:wear_effect(state.slot_types.feet, attributes.hit_rate, 0.2) + + self.parent.inventory:wear_effect(state.slot_types.feet, attributes.randomness, 0.15) + + self.parent.inventory:wear_effect(state.slot_types.feet, attributes.hit_rate, 0.15) end function Combat:deflect_skill() return self.parent.attributes.deflect_attribute - + self.parent.inventory:wear_effect(state.slot_types.body, attributes.durability, 0.4) - + self.parent.inventory:wear_effect(state.slot_types.left_hand, attributes.durability, 0.3) + + self.parent.inventory:wear_effect(state.slot_types.body, attributes.durability, 0.08) + + self.parent.inventory:wear_effect(state.slot_types.left_hand, attributes.durability, 0.05) end function Combat:weapon_skill() @@ -207,10 +212,19 @@ function Combat:apply_damage(damage, target, weapon) end end +---comment +---@return Entity +function Combat:get_weapon() + return self.parent.inventory:get_weapon() or self.default_weapon() +end + ---comment ---@param target Combat +---@return boolean if damage was dealth function Combat:attack(target) - local weapon = self.parent.inventory:get_weapon() or self.default_weapon() + --the target will target the attacker + target.parent.attributes.target = self.parent + local weapon = self:get_weapon() local damage, dealt = self:try_damage_dealt(target, weapon) if not dealt then diff --git a/lua/neohack/components/decision.lua b/lua/neohack/components/decision.lua index f0a6a75..80f6a72 100644 --- a/lua/neohack/components/decision.lua +++ b/lua/neohack/components/decision.lua @@ -1,6 +1,13 @@ ---- making decisions +--- making decisions Entity AI --- +local Def = require("neohack.def") +local message = require("neohack.message") +local state = require("neohack.state") +local constant_defs = require("neohack.constant_defs") +local Move = require("neohack.move") +local utils = require("neohack.utils") + ---@class Decision ---@field parent Entity local Decision = {} @@ -21,7 +28,7 @@ function Decision.new(parent, randomness) end function Decision:inspect() - return "randomness=" .. self.parent.attributes.randomness + return "randomness=" .. (self.parent.attributes.randomness or "") end function Decision:attributes() return { @@ -29,4 +36,284 @@ function Decision:attributes() } end +---comment +---@param visible_targets Entity[] +function Decision:find_target(visible_targets) + --keep existing target, until not visible + --TODO: or on a timer? + local old_target = self.parent.attributes.target + if old_target then + for _, entity in ipairs(visible_targets) do + if entity == old_target then + return old_target + end + end + end + + for _, entity in ipairs(visible_targets) do + if entity.type == Def.DefType.player and entity ~= self.parent then + return entity + end + end + for _, entity in ipairs(visible_targets) do + if entity.type == Def.DefType.friend and entity ~= self.parent then + return entity + end + end + for _, entity in ipairs(visible_targets) do + if entity.type == Def.DefType.item and entity ~= self.parent then + return entity + end + end + return nil +end + +function Decision:choose_target(visible_targets) + local targets = "" + for _, entity in ipairs(visible_targets) do + targets = targets .. entity.name .. " " + end + -- message.notify(self.parent.name, "available targets", targets) + + local target = self:find_target(visible_targets) + self.parent.attributes.target = target + message.notify(self.parent.name, "targeting", (target and target.name or "nothing")) +end + +---what is the target relative to self? DefType are normally relative to the player +---@param target Entity +---@return string +function Decision:target_type(target) + if self.parent.type == Def.DefType.enemy then + if + target.type == Def.DefType.friend + or target.type == Def.DefType.player + or target.type == Def.DefType.enemy -- enemies attack each other + then + return Def.DefType.enemy + -- enemies have no friends + -- elseif target.type == Def.DefType.enemy then + -- return Def.DefType.friend + end + end + return target.type +end + +---@class Action +---@field name string +---@field weight number +---@field act fun(self: Decision): boolean + +--- available actions +---@type table +Decision.actions = { + attack_player = { + name = "attack_player", + weight = 50, + ---@param self Decision + ---@return boolean + act = function(self) + -- attack player from 1 space away regardless of entity's available moves + local can = self.parent ~= state.player + and Move.manhattan_distance( + self.parent.movement.row, + self.parent.movement.col, + state.player.movement.row, + state.player.movement.col + ) + <= 1 + if can then + state.player:hit_by(self.parent) + return true -- attack was attempted + else + return false + end + end, + }, + + wear = { + name = "wear", + weight = 10, + ---@param self Decision + ---@return boolean + act = function(self) + return self.parent.inventory:fill_slots() + end, + }, + + attack_enemies = { + name = "attack_enemies", + weight = 8, + ---@param self Decision + ---@return boolean + act = function(self) + local enemies, _ = state.scan_area( + state.current_bufnr, + self.parent.movement.row, + self.parent.movement.col, + 1 -- only right next to + ) + if #enemies > 0 then + for _, enemy in ipairs(enemies) do + if enemy ~= self.parent then + self.parent.combat:attack(enemy.combat) + return true -- attack was attempted + end + end + end + return false + end, + }, + + pickup_items = { + name = "pickup_items", + weight = 2, + ---@param self Decision + ---@return boolean + act = function(self) + local _, items = state.scan_area( + state.current_bufnr, + self.parent.movement.row, + self.parent.movement.col, + 1 -- only right next to + ) + if #items > 0 then + --TODO: they shouldn't pick everything up + local item = items[1] + return self.parent.inventory:npc_pickup_off_floor(item) + end + return false + end, + }, + + cast_spell = { + name = "cast_spell", + weight = 4, + ---@param self Decision + ---@return boolean + act = function(self) + local spells = self.parent.inventory:get_spell_items() + if #spells > 0 then + -- random spell + local spell = spells[math.random(#spells)] + local enemies, items = state.scan_area( + state.current_bufnr, + self.parent.movement.row, + self.parent.movement.col, + self.parent.vision:view_distance() + ) + -- on close random enemy or item + if #enemies > 0 then + local enemy = enemies[math.random(#enemies)] + return self.parent.speak:say(spell.speak.spell.inscription, enemy.name) + elseif #items > 0 then + local item = items[math.random(#items)] + return self.parent.speak:say(spell.speak.spell.inscription, item.name) + end + return false + end + return false + end, + }, + + move_towards_target = { + name = "move_towards_target", + weight = 30, + ---@param self Decision + ---@return boolean + act = function(self) + local targets = state.get_visible_entities(state.current_bufnr, self.parent, self.parent.vision:view_distance()) + self:choose_target(targets) + return self.parent.movement:move_closer_to_target() + end, + }, + + kick = { + name = "kick", + weight = 8, + ---@param self Decision + ---@return boolean + act = function(self) + local dir = utils.random_key(Move.directions) + return self.parent.movement:kick(dir) + end, + }, + + rename = { + name = "rename", + weight = 6, + ---@param self Decision + ---@return boolean + act = function(self) + -- write your name on an item + if #self.parent.inventory.items > 0 then + local item_index = math.random(#self.parent.inventory.items) + local item = self.parent.inventory.items[item_index] + if item_index and item then + if not utils.starts_with(item.name, item.name) then + return self.parent.inventory:rename(tostring(item_index), self.parent.name .. "s_" .. item.name) + end + end + end + return false + end, + }, + + drop = { + name = "drop", + weight = 4, + ---@param self Decision + ---@return boolean + act = function(self) + if #self.parent.inventory.items > 0 then + local item_index = math.random(#self.parent.inventory.items) + if item_index then + return self.parent.inventory:drop(tostring(item_index)) + end + end + return false + end, + }, + + -- this actions cause there to be fewer total items + --TODO: fuse? + --TODO eat? + + wait = { + name = "wait", + weight = 1, + ---@param self Decision + ---@return boolean + act = function(self) + message.notify(self.parent.name, "is bored") + return true + end, + }, +} + +---chose the action to perform and do it +function Decision:choose_action() + local list = {} + for _, action in pairs(Decision.actions) do + table.insert(list, action) + end + for i = 1, #list + 1, 1 do + local action, index = utils.weighted_random(list) + if action and action ~= Decision.actions.wait then + table.remove(list, index) -- don't try this again + if action.act(self) then + -- message.notify(self.parent.name, "chose", action.name) + return + else + -- message.notify(self.parent.name, "failed at", action.name) + end + else + -- message.notify(self.parent.name, "cannot chose") + end + end + + Decision.actions.wait.act(self) + -- message.notify(self.parent.name, "fallback to wait") +end + return Decision diff --git a/lua/neohack/components/eat.lua b/lua/neohack/components/eat.lua index 104d609..7ad971d 100644 --- a/lua/neohack/components/eat.lua +++ b/lua/neohack/components/eat.lua @@ -55,7 +55,7 @@ function Eat:eat(keys) if #loot > 0 then for _, loot_item in ipairs(loot) do message.notify("Threw up a " .. loot_item.name) - self.parent.inventory:pickup_item(loot_item) + self.parent.inventory:pickup(loot_item) end end diff --git a/lua/neohack/components/fuse.lua b/lua/neohack/components/fuse.lua index 8698a9f..1c4f74b 100644 --- a/lua/neohack/components/fuse.lua +++ b/lua/neohack/components/fuse.lua @@ -64,7 +64,7 @@ function Fuse:fuse(keys) end message.notify("Fused " .. table.concat(keys, " ") .. " into " .. new_item.name) -- all other items disappear as part of fusing - self.parent.inventory:pickup_item(new_item) + self.parent.inventory:pickup(new_item) end return Fuse diff --git a/lua/neohack/components/health.lua b/lua/neohack/components/health.lua index 0160414..18381e4 100644 --- a/lua/neohack/components/health.lua +++ b/lua/neohack/components/health.lua @@ -26,7 +26,7 @@ function Health.new(parent, health) end function Health:inspect() - return "health=" .. self.parent.attributes.health + return "health=" .. (self.parent.attributes.health or "") end function Health:attributes() return { health = self.parent.attributes.health } diff --git a/lua/neohack/components/inventory.lua b/lua/neohack/components/inventory.lua index 982fa61..d5431ac 100644 --- a/lua/neohack/components/inventory.lua +++ b/lua/neohack/components/inventory.lua @@ -6,6 +6,7 @@ local Def = require("neohack.def") local state = require("neohack.state") local message = require("neohack.message") local buffer = require("neohack.buffer") +local constant_defs = require("neohack.constant_defs") ---@class Inventory ---@field parent Entity @@ -53,23 +54,44 @@ function Inventory:can_pickup(entity) return entity.type == Def.DefType.item or entity.type == Def.DefType.floor end +---comment +---@param entity Entity +---@return boolean +function Inventory:npc_pickup_off_floor(entity) + if entity.movement.char == constant_defs.enemy_corpse and entity.type == Def.DefType.player then + return false + end + return self:pickup_off_floor(entity) +end + +---comment +---@param entity Entity +---@return boolean +function Inventory:pickup_off_floor(entity) + if self:pickup(entity) then + buffer.set_entity_at_cell( + entity.movement.row, + entity.movement.col, + state.entity_generator.new_floor(entity.movement.row, entity.movement.col) + ) + return true + end + return false +end + ---comment ---@param entity Entity function Inventory:pickup(entity) if entity.type == Def.DefType.item then - self:pickup_item(entity) + table.insert(self.items, 1, entity) + message.notify(self.parent.name, "got a", entity.name) + return true else -- do nothing + return false end end ---- pick up item ----@param item Entity -function Inventory:pickup_item(item) - table.insert(self.items, 1, item) - message.notify("Got a " .. item.name) -end - ---comment ---@return string function Inventory:get_inventory_item_with_index() @@ -101,11 +123,11 @@ function Inventory:retrieve_item_char(char) if self.items[i].movement.char == char then local entity = table.remove(self.items, i) -- TODO: deal with retrieve for action being too verbose - message.notify("Retrieved " .. entity.movement.char) + message.notify(self.parent.name, "retrieved", entity.movement.char) return entity end end - message.notify("No " .. char .. " to retrieve") + message.notify(self.parent.name("has no"), char, "to retrieve") return nil end @@ -134,7 +156,7 @@ function Inventory:retrieve_items(keys) end end if not found then - message.notify("No " .. name .. " to retrieve") + message.notify(self.parent.name("has no"), name, "to retrieve") -- get all or nothing return nil end @@ -147,7 +169,7 @@ function Inventory:retrieve_items(keys) for _, index in ipairs(indexes) do table.remove(self.items, index) end - -- message.notify("Retrieved " .. table.concat(indexes, " ")) + -- message.notify(self.parent.name,"retrieved", table.concat(indexes, " ")) return items end @@ -171,7 +193,7 @@ function Inventory:_wield(key, slot) local items = self:retrieve_items({ key }) if items and items[1] then self.slots[slot] = items[1] - message.notify("Wearing " .. items[1].name .. " on " .. state.slot_name(slot)) + message.notify(self.parent.name, "wearing", items[1].name, "on", state.slot_name(slot)) else return end @@ -191,7 +213,7 @@ function Inventory:wear(key, slot) self:_wield(key, slot) -- pickup old after wielding new, to maintain order if previous then - self:pickup_item(previous) + self:pickup(previous) end end @@ -218,8 +240,10 @@ function Inventory:get_wearing() local wearing = "" for name, char in utils.sorted_pairs(state.slot_types) do local item = self.slots[char] - local item_name = item and item.name or "" - wearing = wearing .. name .. "=" .. item_name .. " " + if item then + local item_name = item.name + wearing = wearing .. name .. "=" .. item_name .. " " + end end return wearing end @@ -234,16 +258,18 @@ end ---comment ---@param key string ---@param new_name string +---@return boolean function Inventory:rename(key, new_name) --TODO: don't pull out local items = self:retrieve_items({ key }) if not items then - return + return false end local item = items[1] - message.notify("Renaming " .. item.name .. " to " .. new_name) + message.notify(self.parent.name, "renamed", item.name, "to", new_name) item.name = new_name - self:pickup_item(item) + self:pickup(item) + return true end ---comment @@ -251,7 +277,7 @@ end function Inventory:look(keys) for i, key in ipairs(keys) do if key == "0" or key == "self" then - message.notify("Looked at self " .. self.parent:inspect()) + message.notify(self.parent.name, "looked at self", self.parent:inspect()) table.remove(keys, i) end end @@ -261,8 +287,8 @@ function Inventory:look(keys) end for _, item in ipairs(items) do if item then - self:pickup_item(item) - message.notify("Looked at " .. item:inspect()) + self:pickup(item) + message.notify(self.parent.name, "looked at", item:inspect()) end end end @@ -279,27 +305,58 @@ function Inventory:pilfer(key) local extracted = "" local loot = lootable.inventory:extract_all() for _, item in ipairs(loot) do - self:pickup_item(item) + self:pickup(item) extracted = extracted .. item.name .. " " end - self:pickup_item(lootable) - message.notify("Took", extracted, "from", lootable.name) + self:pickup(lootable) + if #loot > 0 then + message.notify(self.parent.name, "took", extracted, "from", lootable.name) + end end end end ---comment ----@param keys string[] -function Inventory:drop(keys) - local items = self:retrieve_items(keys) +---@param key string +---@return boolean +function Inventory:drop(key) + local items = self:retrieve_items({ key }) if not items then - return + return false end - local row, col = self.parent.movement.row, self.parent.movement.col + for _, item in ipairs(items) do - -- TODO: allow drop without overwrite - buffer.set_entity_at_cell(row, col, item) + local floor = self.parent.movement:find_floor() + if not floor then + message.notify(self.parent.name, "has no space to drop", item.name) + return false + end + buffer.set_entity_at_cell(floor.movement.row, floor.movement.col, item) + message.notify(self.parent.name, "dropped", item.name) + return true + end + return false +end + +function Inventory:fill_slots() + local priority = { + state.slot_types.right_hand, + state.slot_types.left_hand, + state.slot_types.body, + state.slot_types.head, + state.slot_types.feet, + } + for _, s in ipairs(priority) do + if self.slots[s] == nil then + -- empty slot + if #self.items > 0 then + -- so wear first item + self:wear("1", s) + return true + end + end end + return false end return Inventory diff --git a/lua/neohack/components/movement.lua b/lua/neohack/components/movement.lua index eb028cd..ea29457 100644 --- a/lua/neohack/components/movement.lua +++ b/lua/neohack/components/movement.lua @@ -6,7 +6,6 @@ local Move = require("neohack.move") local buffer = require("neohack.buffer") local constant_defs = require("neohack.constant_defs") local view_buffer = require("neohack.view_buffer") -local utils = require("neohack.utils") local state = require("neohack.state") local Def = require("neohack.def") local message = require("neohack.message") @@ -58,10 +57,10 @@ function Movement:inspect() end function Movement:attributes() return { - char = self.char, + -- char = self.char, moves = self.moves, - row = self.row, - col = self.col, + -- row = self.row, + -- col = self.col, visible = self.visible, seen = self.seen, scared = self.parent.attributes.scared, @@ -91,12 +90,19 @@ end function Movement:make_move(move) local new_row, new_col = self.row + move.row, self.col + move.col local entity = buffer.get_entity_at_pos(new_row, new_col) - if entity and entity.movement.char == constant_defs.floor_char and new_row > 0 and new_col > 0 then + local on_player = entity and entity.type == Def.DefType.player + local is_floor = entity and entity.movement.char == constant_defs.floor_char + if is_floor and not on_player and new_row > 0 and new_col > 0 then -- message.notify("making move " .. vim.inspect(self) .. " " .. vim.inspect(move)) - buffer.set_entity_at_cell(self.row, self.col, state.entity_generator.new_floor(self.row, self.col)) - view_buffer.set_highlight(self.row, self.col, nil) - + -- don't delete item under player + if self.parent.type ~= Def.DefType.player then + buffer.set_entity_at_cell(self.row, self.col, state.entity_generator.new_floor(self.row, self.col)) + view_buffer.set_highlight(self.row, self.col, nil) + else + buffer.highlight_cursor(constant_defs.cursor_hit_highlight) + end buffer.set_entity_at_cell(new_row, new_col, self.parent) + -- TODO: without this it leaves a hit to the enemy location if self.visible then view_buffer.set_highlight(new_row, new_col, constant_defs.enemy_highlight) @@ -109,124 +115,140 @@ function Movement:make_move(move) end ---comment ----@param target_row integer ----@param target_col integer ----@param try_move function -function Movement:move_closer_to(target_row, target_col, try_move) - local move_list = self:moves_by_distance(target_row, target_col) - local in_fear = false - if self.parent.attributes.scared > math.random() then - move_list = utils.reverse_array(move_list) - in_fear = true - end - -- message.notify("moves " .. vim.inspect(moves)) - if math.random() < self.parent.attributes.randomness then - -- randomly do a random move - -- message.notify("random move") - self:make_move(move_list[math.random(#move_list)]) - else - -- message.notify("move " .. mover.name .. vim.inspect(move_list)) - for _, move in ipairs(move_list) do - if try_move(self, move, in_fear, target_row, target_col) then - return +function Movement:move_closer_to_target() + if self.parent.attributes.target then + local target_row = self.parent.attributes.target.movement.row + local target_col = self.parent.attributes.target.movement.col + local move_list = self:moves_by_distance(target_row, target_col) + + -- run away from target if scared + -- TODO: add back + -- local in_fear = false + -- if self.parent.attributes.scared > math.random() then + -- move_list = utils.reverse_array(move_list) + -- in_fear = true + -- end + local in_fear = false + + -- message.notify("moves " .. vim.inspect(moves)) + -- makes the random move when targeting lower + if (math.random() * 0.2) < self.parent.attributes.randomness then + -- randomly do a random move + -- message.notify("random move") + return self:make_move(self.moves[math.random(#self.moves)]) + else + -- message.notify("move " .. mover.name .. vim.inspect(move_list)) + for _, move in ipairs(move_list) do + if self:try_move(move, in_fear) then + return true + end end end + else + -- move randomly if no target + return self:make_move(self.moves[math.random(#self.moves)]) end end ---comment ---@param move Move ---@param in_fear boolean ----@param target_row integer ----@param target_col integer ---@return boolean move successful -function Movement:enemy_try_move(move, in_fear, target_row, target_col) +function Movement:try_move(move, in_fear) local new_row = self.row + move.row local new_col = self.col + move.col local entity = buffer.get_entity_at_pos(new_row, new_col) - local player_row, player_col = buffer.get_under_cursor() - -- message.notify( "move " .. mover.name .. " to 'nil' " .. vim.inspect(move) .. " dist: " .. move.dist .. " r:" .. self.row + move.row .. " c:" .. self.col + move.col) - if entity then - -- message.notify("move " .. mover.name .. " to '" .. char .. "' " .. vim.inspect(move)) - -- invincible for a couple of moves - if new_row == player_row and new_col == player_col then + + if entity == nil then + return false + end + + local type = self.parent.decision:target_type(entity) + if type == Def.DefType.enemy then + if entity.type == Def.DefType.player then if state.turn_counter < 2 then + -- player is invincible for a couple of moves state.player.combat:dodge(self.parent.combat) else -- hit player - state.player:hit_by(self.parent, target_row, target_col) + state.player:hit_by(self.parent) end return true - elseif - entity - --TODO: make enemy factions to generalize friend vs enemy - and entity.type == Def.DefType.friend -- attack friends - and entity.type == Def.DefType.enemy -- attack other enemies - then - -- mover.combat:entity_attack_entity(mover, entity) - self.parent.combat:attack(entity.combat) else - self:make_move(move) - if in_fear then - message.notify(self.parent.name .. " is scared") - end - return true - end - end - return false -end - ----comment ----@param move Move ----@param in_fear boolean ----@param target_row integer ----@param target_col integer ----@return boolean move successful -function Movement:friend_try_move(move, in_fear, target_row, target_col) - local new_row = self.row + move.row - local new_col = self.col + move.col - local entity = buffer.get_entity_at_pos(new_row, new_col) - local player_row, player_col = buffer.get_under_cursor() - -- message.notify( "move " .. mover.name .. " to 'nil' " .. vim.inspect(move) .. " dist: " .. move.dist .. " r:" .. self.row + move.row .. " c:" .. self.col + move.col) - if entity then - -- message.notify("move " .. mover.name .. " to '" .. char .. "' " .. vim.inspect(move)) - -- invincible for a couple of moves - if new_row == player_row and new_col == player_col then - state.player.attributes.health = state.player.attributes.health + 0.5 - message.notify(self.parent.name .. " Hugged you") - elseif entity and entity.type == Def.DefType.enemy then -- mover.combat:entity_attack_entity(mover, entity) self.parent.combat:attack(entity.combat) - else - self:make_move(move) - if in_fear then - message.notify(self.parent.name .. " is scared") + end + elseif type == Def.DefType.friend then + entity.attributes.health = entity.attributes.health + 0.5 + message.notify(self.parent.name, "hugged", entity.name) + else + if type == Def.DefType.item then + -- if cell has an item, pick it up + if self.parent.inventory:npc_pickup_off_floor(entity) then + return true end - return true end + self:make_move(move) + if in_fear then + message.notify(self.parent.name .. " is scared") + end + return true end return false end ---comment ---@param direction string hjkl +---@return boolean if kick was attempted function Movement:kick(direction) local move = Move.directions[direction] if move == nil then - return + return false end - local player_row, player_col = buffer.get_under_cursor() - local kick_row, kick_col = player_row + move.row, player_col + move.col + local row, col = self.parent.movement.row, self.parent.movement.col + local kick_row, kick_col = row + move.row, col + move.col local target = buffer.get_entity_at_pos(kick_row, kick_col) - if target then + if target and target.type ~= Def.DefType.floor then if target.movement:make_move(move) then - message.notify("Kicked " .. target.name) + message.notify(self.parent.name, "kicked", target.name) else - message.notify("Kick failed on " .. target.name) + message.notify(self.parent.name, "kick failed on", target.name) end + return true else - message.notify("Kicked the air") + return false + end +end + +---comment +---@param row integer +---@param col integer +---@param callback fun(int,int):boolean +local function local_cells(row, col, callback) + -- try current pos first + local order = { 0, -1, 1 } + for _, delta_row in ipairs(order) do + for _, delta_col in ipairs(order) do + if callback(row + delta_row, col + delta_col) then + return + end + end end end +---comment +---@return Entity? +function Movement:find_floor() + local floor = nil + local_cells(self.parent.movement.row, self.parent.movement.col, function(row, col) + local entity = buffer.get_entity_at_pos(row, col, true) + if entity and entity.type == Def.DefType.floor then + floor = entity + return true + end + return false + end) + return floor +end + return Movement diff --git a/lua/neohack/components/speak.lua b/lua/neohack/components/speak.lua index 8112452..49a8dac 100644 --- a/lua/neohack/components/speak.lua +++ b/lua/neohack/components/speak.lua @@ -36,25 +36,29 @@ function Speak:attributes() } end -function Speak:say(words) - local word_str = table.concat(words, " ") - message.notify(self.parent.name, "said", word_str) - - local spell_names = "" +---comment +---@param word string +---@return Entity[] spells +---@return string effects +function Speak:spells_spoken(word) ---@type Entity[] local casting = {} + local effects = "" for _, spell_item in pairs(self.parent.inventory:get_spell_items()) do - if string.find(word_str, spell_item.speak.spell.inscription) ~= nil then + if word == spell_item.speak.spell.inscription then if spell_item.speak.spell.effect then table.insert(casting, spell_item) - spell_names = spell_names .. spell_item.speak.spell.effect.name .. " " + effects = effects .. spell_item.speak.spell.effect.name .. " " end end end - if #casting > 0 then - message.notify(self.parent.name, "casting", spell_names) - end + return casting, effects +end +---comment +---@param target string +---@return Entity[] entities that heard the word +function Speak:entities_that_heard(target) local enemies, items = state.scan_area( state.current_bufnr, self.parent.movement.row, @@ -70,20 +74,53 @@ function Speak:say(words) table.insert(seen, item) end + local heard = {} for _, entity in ipairs(seen) do - local cast = (#casting > 0 and " cast " .. spell_names) or "" - if string.find(word_str, entity.name) ~= nil then - if entity.type == Def.DefType.enemy then - message.notify(entity.name, "heard", self.parent.name, cast) + -- local cast = (#casting > 0 and " cast " .. spell_names) or "" + if target == entity.name then + table.insert(heard, entity) + end + end + return heard +end + +---comment +---@param word string +---@param target string +---@return boolean always true, as speaking can't fail +function Speak:say(word, target) + local casting, effects = self:spells_spoken(word) + + local entities = self:entities_that_heard(target) + + -- TODO: hearing their name could do other things not just spells + for _, entity in ipairs(entities) do + for _, spell in ipairs(casting) do + if spell.speak.spell.effect then + spell.speak.spell.effect.cast(entity, 1) end - for _, spell in ipairs(casting) do - if spell.speak.spell.effect then - spell.speak.spell.effect.cast(entity, 1) - message.notify(self.parent.name, "Cast", spell.speak.spell.effect.name, "on", entity.name) - end + end + end + + if target == "self" and #casting > 0 then + for _, spell in ipairs(casting) do + if spell.speak.spell.effect then + spell.speak.spell.effect.cast(self.parent, 1) end end + message.notify(self.parent.name, "cast", effects, "on themself") + elseif #entities > 0 and #casting > 0 then + message.notify(self.parent.name, "cast", effects, "on", #entities, target .. "s") + elseif #entities > 0 then + message.notify(self.parent.name, "said", word, "and was heard by", #entities, target .. "s") + elseif #casting > 0 then + message.notify(self.parent.name, "cast", effects, "on nothing") + elseif target == nil then + message.notify(self.parent.name, "said", word, "to themselves") + else + message.notify(self.parent.name, "said", word, "but no", target, "heard") end + return true end return Speak diff --git a/lua/neohack/components/vision.lua b/lua/neohack/components/vision.lua index b57864d..77e026d 100644 --- a/lua/neohack/components/vision.lua +++ b/lua/neohack/components/vision.lua @@ -3,6 +3,7 @@ local state = require("neohack.state") local attributes = require("neohack.attribute_getters") +local message = require("neohack.message") ---@class Vision ---@field parent Entity @@ -15,8 +16,9 @@ Vision.__name = "Vision" ---comment ---@param parent Entity ---@param vision_attribute number +---@param view_attribute number ---@return Vision -function Vision.new(parent, vision_attribute) +function Vision.new(parent, vision_attribute, view_attribute) local instance = { parent = parent, @@ -24,7 +26,7 @@ function Vision.new(parent, vision_attribute) search_threshold = 0.5, } --TODO: simplify to a single attribute - parent.attributes.view_attribute = 10 + parent.attributes.view_attribute = view_attribute parent.attributes.search_attribute = 0.5 parent.attributes.vision = vision_attribute setmetatable(instance, Vision) @@ -37,7 +39,7 @@ function Vision:inspect() .. " search_skill=" .. self:search_skill() .. " vision=" - .. self.parent.attributes.vision + .. (self.parent.attributes.vision or "") end function Vision:attributes() return { @@ -57,7 +59,9 @@ end ---comment ---@return number function Vision:view_distance() - return self.parent.attributes.view_attribute + local base_distance = self.parent.attributes.view_attribute or 1 + -- message.notify(self.parent.name, "view_distance=", base_distance) + return base_distance + self.parent.inventory:wear_effect(state.slot_types.head, attributes.vision, 4) + self.parent.inventory:wear_effect(state.slot_types.left_hand, attributes.vision, 4) end diff --git a/lua/neohack/def.lua b/lua/neohack/def.lua index 26b1e76..da9ac02 100644 --- a/lua/neohack/def.lua +++ b/lua/neohack/def.lua @@ -10,10 +10,29 @@ --- @field moves nil | Move[] -- the moves this entity can make --- @field randomness nil | number -- how chaotic entity is --- @field vision nil | number -- how well entity can see movements and attacks +--- @field view_distance number --- @field spell nil | Spell -- a spell written on the item local Def = {} Def.__index = Def +--- @class DefArgs +--- @field type "terrain" | "item" | "enemy" | "friend" | "floor" | "player" +--- @field block_vision boolean +--- @field char string +--- @field name string +--- @field health nil | number +--- @field durability nil | number +--- @field damage nil | number +--- @field hit_rate nil | number -- how accurate entity is as a weapon +--- @field moves nil | Move[] -- the moves this entity can make +--- @field randomness nil | number -- how chaotic entity is +--- @field vision nil | number -- how well entity can see movements and attacks +--- @field view_distance number +--- @field spell nil | Spell -- a spell written on the item + +---comment +---@param args DefArgs +---@return Def function Def.new(args) local instance = setmetatable({}, Def) instance.type = args.type @@ -31,6 +50,7 @@ function Def.new(args) instance.moves = args.moves instance.randomness = args.randomness instance.vision = args.vision + instance.view_distance = args.view_distance instance.spell = args.spell return instance end diff --git a/lua/neohack/defs.lua b/lua/neohack/defs.lua index 6e77ed3..4b356ab 100644 --- a/lua/neohack/defs.lua +++ b/lua/neohack/defs.lua @@ -22,6 +22,7 @@ M.item_defs = { block_vision = false, vision = 0, randomness = 0.1, + view_distance = 2, }), p = Def.new({ type = Def.DefType.item, @@ -34,6 +35,7 @@ M.item_defs = { block_vision = false, vision = 0.1, randomness = 0.5, + view_distance = 6, }), u = Def.new({ type = Def.DefType.item, @@ -45,6 +47,7 @@ M.item_defs = { block_vision = false, vision = 0.5, randomness = 0.01, + view_distance = 10, }), ["!"] = Def.new({ type = Def.DefType.item, @@ -56,6 +59,7 @@ M.item_defs = { block_vision = false, vision = 0, randomness = 0.01, + view_distance = 10, }), l = Def.new({ type = Def.DefType.item, @@ -68,6 +72,7 @@ M.item_defs = { block_vision = false, vision = 0.1, randomness = 0.7, + view_distance = 6, }), -- TODO: add wizard eye @@ -83,6 +88,7 @@ M.item_defs = { block_vision = false, vision = 0, randomness = 0.01, + view_distance = 6, }), [constant_defs.enemy_corpse] = Def.new({ @@ -96,6 +102,7 @@ M.item_defs = { block_vision = false, vision = 0.5, randomness = 0.8, + view_distance = 2, }), } @@ -108,6 +115,7 @@ M.terrain_defs = { hit_rate = 0.5, block_vision = true, vision = 1, + view_distance = 1, }), ["|"] = Def.new({ type = Def.DefType.terrain, @@ -117,6 +125,7 @@ M.terrain_defs = { hit_rate = 0.5, block_vision = true, vision = 1, + view_distance = 1, }), ["_"] = Def.new({ type = Def.DefType.terrain, @@ -126,6 +135,7 @@ M.terrain_defs = { hit_rate = 0.5, block_vision = true, vision = 1, + view_distance = 1, }), ["-"] = Def.new({ type = Def.DefType.terrain, @@ -135,6 +145,7 @@ M.terrain_defs = { hit_rate = 0.5, block_vision = true, vision = 1, + view_distance = 1, }), o = Def.new({ type = Def.DefType.terrain, @@ -144,6 +155,7 @@ M.terrain_defs = { hit_rate = 0.5, block_vision = false, vision = 1, + view_distance = 1, }), ["0"] = Def.new({ type = Def.DefType.terrain, @@ -153,13 +165,14 @@ M.terrain_defs = { hit_rate = 0.9, block_vision = false, vision = 0.7, + view_distance = 1, }), } M.enemy_defs = { s = Def.new({ type = Def.DefType.enemy, - char = "s", + char = "S", name = "snake", health = 3, moves = moves.four, @@ -168,10 +181,12 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = false, vision = 0.2, + view_distance = 8, + durability = 2, }), r = Def.new({ type = Def.DefType.enemy, - char = "r", + char = "R", name = "rat", health = 1, moves = moves.eight, @@ -180,10 +195,12 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = false, vision = 0.6, + view_distance = 12, + durability = 2, }), g = Def.new({ type = Def.DefType.enemy, - char = "g", + char = "G", name = "goblin", health = 8, moves = moves.four, @@ -192,10 +209,12 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.7, + view_distance = 9, + durability = 6, }), f = Def.new({ type = Def.DefType.enemy, - char = "f", + char = "F", name = "frog", health = 1, moves = moves.jump, @@ -204,10 +223,12 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = false, vision = 0.3, + view_distance = 3, + durability = 1, }), b = Def.new({ type = Def.DefType.enemy, - char = "b", + char = "B", name = "bat", health = 2, moves = moves.diag, @@ -216,6 +237,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = false, vision = 0.1, + view_distance = 3, + durability = 1, }), ["="] = Def.new({ type = Def.DefType.enemy, @@ -228,6 +251,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.01, + view_distance = 1, + durability = 10, }), ["("] = Def.new({ type = Def.DefType.enemy, @@ -240,6 +265,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.01, + view_distance = 1, + durability = 1, }), [")"] = Def.new({ type = Def.DefType.enemy, @@ -252,6 +279,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.01, + view_distance = 1, + durability = 1, }), ['"'] = Def.new({ type = Def.DefType.enemy, @@ -264,6 +293,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.01, + view_distance = 1, + durability = 4, }), ["*"] = Def.new({ type = Def.DefType.enemy, @@ -276,6 +307,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = false, vision = 0.01, + view_distance = 3, + durability = 2, }), A = Def.new({ type = Def.DefType.enemy, @@ -288,6 +321,8 @@ M.enemy_defs = { hit_rate = 0.5, block_vision = true, vision = 0.9, + view_distance = 5, + durability = 6, }), } @@ -297,6 +332,7 @@ M.floor = Def.new({ char = constant_defs.floor_char, block_vision = false, vision = -0.5, + view_distance = 1, }) M.down = Def.new({ type = Def.DefType.terrain, @@ -306,6 +342,7 @@ M.down = Def.new({ hit_rate = 0.0, block_vision = false, vision = 1.0, + view_distance = 1, }) M.up = Def.new({ type = Def.DefType.terrain, @@ -315,6 +352,7 @@ M.up = Def.new({ hit_rate = 0.0, block_vision = false, vision = 1.0, + view_distance = 1, }) M.tome = Def.new({ @@ -328,6 +366,7 @@ M.tome = Def.new({ block_vision = false, vision = 0, randomness = 0.99, + view_distance = 1, }) ---comment @@ -447,6 +486,7 @@ M.new_unknown = function(row, col, char) char = char, block_vision = false, damage = 0, + view_distance = 1, }) return Entity.new(def, row, col) end diff --git a/lua/neohack/edits.lua b/lua/neohack/edits.lua index f59f33d..882a0a5 100644 --- a/lua/neohack/edits.lua +++ b/lua/neohack/edits.lua @@ -137,12 +137,7 @@ M.handle_changed = function() -- message.notify("Can't pickup non items '" .. deleted_chars .. "'") else for _, entity in ipairs(entities) do - state.player.inventory:pickup(entity) - buffer.set_entity_at_cell( - entity.movement.row, - entity.movement.col, - state.entity_generator.new_floor(entity.movement.row, entity.movement.col) - ) + state.player.inventory:pickup_off_floor(entity) -- TODO: do this once for all entities -- in the real buffer too, otherwise it only updates on the next tick @@ -195,7 +190,11 @@ M.handle_yanked = function() local search_success = chance.action_success(state.player.vision:search_skill(), 0.3, 0.1, state.player.vision.corpse_threshold) if entity.movement.visible or search_success then - table.insert(names, entity.name) + local entry = entity.name + if #entity.inventory:get_wearing() > 0 then + entry = entry .. " (" .. entity.inventory:get_wearing() .. ")" + end + table.insert(names, entry) else table.insert(names, "unknown") end diff --git a/lua/neohack/entity.lua b/lua/neohack/entity.lua index 0403d17..59c77b2 100644 --- a/lua/neohack/entity.lua +++ b/lua/neohack/entity.lua @@ -50,7 +50,7 @@ function Entity.new(def, row, col) instance.sneak = Sneak.new(instance) instance.fuse = Fuse.new(instance) instance.eat = Eat.new(instance) - instance.vision = Vision.new(instance, def.vision) + instance.vision = Vision.new(instance, def.vision, def.view_distance) instance.speak = Speak.new(instance, def.spell) instance.decision = Decision.new(instance, def.randomness) diff --git a/lua/neohack/event.lua b/lua/neohack/event.lua index 34ea04a..e8fe050 100644 --- a/lua/neohack/event.lua +++ b/lua/neohack/event.lua @@ -20,8 +20,6 @@ end ---comment M.player_move = function() - state.player:set_position_from_cursor() - if M.in_sneak_move() then state.player:player_sneak_move() else @@ -30,41 +28,25 @@ M.player_move = function() end ---comment -M.move_enemies = function() - -- get player again after hitting things - local finalRow, finalCol = buffer.get_under_cursor() +M.move_entities = function() -- move enemies - local enemies = map.scan_buf(state.current_bufnr) - -- message.notify("enemies " .. vim.inspect(enemies)) - for _, enemy in ipairs(enemies) do - if enemy.health:check_dead() then - message.notify(string.gsub(enemy.name, "_corpse", "") .. " died") - else - enemy.movement:move_closer_to(finalRow, finalCol, enemy.movement.enemy_try_move) - if enemy.combat.hit_highlight then - view_buffer.highlight_hit(enemy.movement.row, enemy.movement.col, enemy.combat.hit_highlight, 400) - end - enemy.combat.hit_highlight = nil - end - end + local enemies, friends = map.scan_buf(state.current_bufnr) + M.make_moves(enemies) + M.make_moves(friends) end ---comment -M.move_friends = function() - -- get player again after hitting things - local finalRow, finalCol = buffer.get_under_cursor() - -- move friends - local _, friends = map.scan_buf(state.current_bufnr) - -- message.notify("friends " .. vim.inspect(friends)) - for _, friend in ipairs(friends) do - if friend.health:check_dead() then - message.notify(string.gsub(friend.name, "_corpse", "") .. " died") +---@param movers Entity[] +M.make_moves = function(movers) + for _, mover in ipairs(movers) do + if mover.health:check_dead() then + message.notify(string.gsub(mover.name, "_corpse", "") .. " died") else - friend.movement:move_closer_to(finalRow, finalCol, friend.movement.friend_try_move) - if friend.combat.hit_highlight then - view_buffer.highlight_hit(friend.movement.row, friend.movement.col, friend.combat.hit_highlight, 400) + mover.decision:choose_action() + if mover.combat.hit_highlight then + view_buffer.highlight_hit(mover.movement.row, mover.movement.col, mover.combat.hit_highlight, 400) end - friend.combat.hit_highlight = nil + mover.combat.hit_highlight = nil end end end diff --git a/lua/neohack/game.lua b/lua/neohack/game.lua index e2f0e91..9c15514 100644 --- a/lua/neohack/game.lua +++ b/lua/neohack/game.lua @@ -37,6 +37,8 @@ M.start_game = function() state.entity_generator.new_floor = defs.new_floor state.entity_generator.new_entity_from_char = defs.new_entity_from_char state.scan_area = map.scan_area + state.get_visible_entities = map.get_visible_entities + state.player.handle_down = M.handle_down state.player.handle_up = M.handle_up message.notify("New game started.") @@ -148,8 +150,11 @@ end M.tick = function() -- vim.api.nvim_buf_add_highlight(M.bufnr, vim.api.nvim_create_namespace("NeoHack"), "NeoHackHit", 0, 0, 10) buffer.tick(function() + --TODO: nvim crashes on long waits or after too many turns state.turn_counter = state.turn_counter + 1 - -- message.notify("tick") + message.notify("turn", state.turn_counter) + + state.player:set_position_from_cursor() -- detect deleted on every turn -- state.deleted = map.find_deleted(state.current_bufnr) @@ -165,9 +170,9 @@ M.tick = function() event.player_move() - event.move_enemies() + event.move_entities() - event.move_friends() + state.player:set_cursor_from_position() if state.player.health:is_dead() then M.end_game() diff --git a/lua/neohack/generated_defs.lua b/lua/neohack/generated_defs.lua index 8cb6a67..3aa28d9 100644 --- a/lua/neohack/generated_defs.lua +++ b/lua/neohack/generated_defs.lua @@ -146,11 +146,12 @@ M.generate_enemy = function() local hit_rate = calculate_stat_float(0.1, portions.hit_rate, 0.05) local randomness = calculate_stat_float(0.2, portions.randomness, 0.5) local vision = calculate_stat_float(0.2, portions.vision, 0.1) + local view_distance = math.random(3, 15) local block_vision = math.random() > 0.5 local new_def = Def.new({ type = Def.DefType.enemy, - char = char, + char = string.upper(char), name = name, health = health, damage = damage, @@ -159,6 +160,7 @@ M.generate_enemy = function() randomness = randomness, block_vision = block_vision, vision = vision, + view_distance = view_distance, -- TODO: random moveset moves = moves.eight, }) @@ -242,10 +244,12 @@ M.generate_effect = function() cast = function(target, amount) local old = target[attribute] local change_value = 1 * amount - if plus == 0 then - target.attributes[attribute] = target.attributes[attribute] + change_value - else - target.attributes[attribute] = target.attributes[attribute] - change_value + if target.attributes[attribute] ~= nil then + if plus == 0 then + target.attributes[attribute] = target.attributes[attribute] + change_value + else + target.attributes[attribute] = target.attributes[attribute] - change_value + end end tick_timer.add_event(time, function(tick) if tick <= 0 then diff --git a/lua/neohack/highlight.lua b/lua/neohack/highlight.lua index 1a97393..a872fb4 100644 --- a/lua/neohack/highlight.lua +++ b/lua/neohack/highlight.lua @@ -31,17 +31,17 @@ M.generate_colour = function(entity) local greenOffset = 0 local blueOffset = 0 if entity.type == Def.DefType.item then - greenOffset = 60 + greenOffset = 80 blueOffset = 20 redOffset = -10 elseif entity.type == Def.DefType.enemy then - redOffset = 60 + redOffset = 80 blueOffset = 20 greenOffset = -20 elseif entity.type == Def.DefType.terrain then - blueOffset = 60 - redOffset = 20 - greenOffset = 10 + blueOffset = 80 + redOffset = 40 + greenOffset = 30 end -- Normalize attributes to create RGB values diff --git a/lua/neohack/map.lua b/lua/neohack/map.lua index c1a1f80..b7ee456 100644 --- a/lua/neohack/map.lua +++ b/lua/neohack/map.lua @@ -76,6 +76,7 @@ end ---@param cells Entity[][] ---@return boolean M.is_visible_line_of_sight = function(row, col, target_row, target_col, cells) + local start_row, start_col = row, col -- distance local dx = math.abs(target_col - col) local dy = math.abs(target_row - row) @@ -97,7 +98,10 @@ M.is_visible_line_of_sight = function(row, col, target_row, target_col, cells) and cells[row][col].movement.char ~= defs.floor.char then -- hidden - return false + -- cannot block yourself + if row ~= start_row and col ~= start_col then + return false + end end -- keep looking local e2 = 2 * err @@ -122,7 +126,7 @@ M.manhattan_distance = function(r1, c1, r2, c2) return math.abs(r1 - r2) + math.abs(c1 - c2) end ----comment +---set the player's visible area ---@param view_distance integer M.show_visible_line_of_sight = function(bufnr, view_distance) local player_row, player_col = buffer.get_under_cursor() @@ -150,6 +154,40 @@ M.show_visible_line_of_sight = function(bufnr, view_distance) end end +--- find entities that are visible by an entity +---@param bufnr integer +---@param viewer_entity Entity +---@param view_distance integer +---@return Entity[] +M.get_visible_entities = function(bufnr, viewer_entity, view_distance) + local found = {} + local start_row, start_col = viewer_entity.movement.row, viewer_entity.movement.col + local player_row, player_col = state.player.movement.row, state.player.movement.col + local cells = buffer.buffers[bufnr].cells + for row_index, line in ipairs(cells) do + for col_index, entity in ipairs(line) do + if + M.manhattan_distance(row_index, col_index, start_row, start_col) > view_distance + or not M.is_visible_line_of_sight(start_row, start_col, row_index, col_index, cells) + then + -- not visible + else + --visible + local is_player = row_index == player_row and col_index == player_col + local is_enemy = entity.type == Def.DefType.enemy + local is_item = entity.type == Def.DefType.item + local is_self = entity == viewer_entity + if is_player then + table.insert(found, state.player) + elseif is_enemy or is_item and not is_self then + table.insert(found, entity) + end + end + end + end + return found +end + ---comment M.all_visible = function(bufnr) local cells = buffer.buffers[bufnr].cells diff --git a/lua/neohack/message.lua b/lua/neohack/message.lua index 838c8f3..eedc224 100644 --- a/lua/neohack/message.lua +++ b/lua/neohack/message.lua @@ -5,7 +5,7 @@ local M = { ---comment M.open = function() - vim.cmd("10 split") + vim.cmd("20 split") vim.cmd("enew") M.message_buf = vim.api.nvim_get_current_buf() vim.api.nvim_command("wincmd p") diff --git a/lua/neohack/player.lua b/lua/neohack/player.lua index 7a15823..1e733f0 100644 --- a/lua/neohack/player.lua +++ b/lua/neohack/player.lua @@ -31,11 +31,16 @@ function Player.new() randomness = 0.01, vision = 0.5, spell = nil, + view_distance = 10, } ---@class Player local instance = Entity.new(def, 0, 0) setmetatable(instance, Player) + instance.attributes.dodge_attribute = 0.5 + instance.attributes.deflect_attribute = 0.05 + instance.attributes.weapon_attribute = 0.5 + ---@type function instance.handle_down = nil ---@type function @@ -95,19 +100,23 @@ function Player:restore_previous_position() self:set_position_from_cursor() end ---TODO: when does the cursor need to be moved based on the entity? ie in the other direction ----updates the entity position based on the cursor function Player:set_position_from_cursor() local cursor_row, cursor_col = buffer.get_under_cursor() self.movement.row = cursor_row self.movement.col = cursor_col + -- message.notify("set position from cursor", cursor_row, cursor_col) +end + +function Player:set_cursor_from_position() + -- message.notify("set cursor from position", cursor_row, cursor_col) + view_buffer.set_cursor(self.movement.row, self.movement.col) end --- bounce off enemies, unless you kill them ---@param row integer ---@param col integer function Player:hit_enemy(row, col) - local enemy = buffer.get_entity_at_pos(row, col) + local enemy = buffer.get_entity_at_pos(row, col, true) if enemy then -- if not self.combat:try_hit_enemy(enemy) then self.combat:attack(enemy.combat) @@ -137,7 +146,7 @@ function Player:hit_terrain(row, col, terrain) message.notify("Up is blocked. On " .. state.current_floor) end elseif terrain.attributes.damage then - self:hit_by(terrain, state.prev_cursor.row, state.prev_cursor.col) + self:hit_by(terrain) else message.notify("Bumped a " .. terrain.name) end @@ -146,16 +155,13 @@ end function Player:eat_keys(keys) local died = self.eat:eat(keys) if died then - local row, col = buffer.get_under_cursor() - self:died(row, col) + self:died() end end --- player was hit by something ---@param attacker Entity ----@param row integer ----@param col integer -function Player:hit_by(attacker, row, col) +function Player:hit_by(attacker) -- local cursor_highlight = self.combat:hit_by(attacker) local hit = attacker.combat:attack(self.combat) if hit then @@ -165,7 +171,7 @@ function Player:hit_by(attacker, row, col) end if self.health:check_dead() then - self:died(row, col) + self:died() end end @@ -173,7 +179,7 @@ function Player:inspect() return Entity.inspect(self) .. " turns:" .. state.turn_counter end -function Player:died(row, col) +function Player:died() local parts = {} for _, body in pairs(state.bodies) do table.insert(parts, body.name) @@ -184,7 +190,7 @@ function Player:died(row, col) message.notify("YOU DIED") -- put the dead player's corpse at the current cursor location - buffer.set_entity_at_cell(row, col, self) + buffer.set_entity_at_cell(self.movement.row, self.movement.col, self) end return Player diff --git a/lua/neohack/spells.lua b/lua/neohack/spells.lua index 4c3d77f..3cf702f 100644 --- a/lua/neohack/spells.lua +++ b/lua/neohack/spells.lua @@ -84,8 +84,10 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - target.health = target.health - (1 * amount) - -- death with be checked on next tick + if target.attributes.health then + target.attributes.health = target.attributes.health - (1 * amount) + -- death with be checked on next tick + end end, }, @@ -95,7 +97,9 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - target.attributes.durability = target.attributes.durability - (1 * amount) + if target.attributes.durability then + target.attributes.durability = target.attributes.durability - (1 * amount) + end end, }, @@ -105,15 +109,17 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - local old = target.attributes.damage - target.attributes.damage = target.attributes.damage - (1 * amount) - tick_timer.add_event(10, function(tick) - if tick <= 0 then - target.attributes.damage = old - message.notify("sharpen wore off on " .. target.name) - return true - end - end) + if target.attributes.damage then + local old = target.attributes.damage + target.attributes.damage = target.attributes.damage - (1 * amount) + tick_timer.add_event(10, function(tick) + if tick <= 0 then + target.attributes.damage = old + message.notify("sharpen wore off on " .. target.name) + return true + end + end) + end end, }, sharpen = { @@ -122,15 +128,17 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - local old = target.attributes.damage - target.attributes.damage = target.attributes.damage + (1 * amount) - tick_timer.add_event(10, function(tick) - if tick <= 0 then - target.attributes.damage = old - message.notify("sharpen wore off on " .. target.name) - return true - end - end) + if target.attributes.damage then + local old = target.attributes.damage + target.attributes.damage = target.attributes.damage + (1 * amount) + tick_timer.add_event(10, function(tick) + if tick <= 0 then + target.attributes.damage = old + message.notify("sharpen wore off on " .. target.name) + return true + end + end) + end end, }, @@ -140,7 +148,9 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - target.attributes.hit_rate = target.attributes.hit_rate - (1 * amount) + if target.attributes.hit_rate then + target.attributes.hit_rate = target.attributes.hit_rate - (1 * amount) + end end, }, @@ -187,7 +197,9 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - target.attributes.randomness = target.attributes.randomness + (0.5 * amount) + if target.attributes.randomness then + target.attributes.randomness = target.attributes.randomness + (0.5 * amount) + end end, }, @@ -197,7 +209,9 @@ M.effects = { ---@param target Entity ---@param amount number cast = function(target, amount) - target.attributes.vision = target.attributes.vision - (0.5 * amount) + if target.attributes.vision then + target.attributes.vision = target.attributes.vision - (0.5 * amount) + end end, }, diff --git a/lua/neohack/utils.lua b/lua/neohack/utils.lua index 27d1638..f2d7b6c 100644 --- a/lua/neohack/utils.lua +++ b/lua/neohack/utils.lua @@ -154,4 +154,36 @@ M.mixin = function(target, ...) }) end +---comment +---@param str string +---@param prefix string +---@return boolean +M.starts_with = function(str, prefix) + return string.sub(str, 1, string.len(prefix)) == prefix +end + +-- Define the decisions and their weights +-- Function to select a decision based on weights +---comment +---@param actions Action[] +---@return Action? +---@return number? +M.weighted_random = function(actions) + local totalWeight = 0 + for _, action in ipairs(actions) do + totalWeight = totalWeight + action.weight + end + + local randomWeight = math.random() * totalWeight + local cumulativeWeight = 0 + + for index, action in ipairs(actions) do + cumulativeWeight = cumulativeWeight + action.weight + if randomWeight <= cumulativeWeight then + return action, index + end + end + return nil, nil +end + return M diff --git a/tests/buffer_spec.lua b/tests/buffer_spec.lua index 8c78132..c01dfd2 100644 --- a/tests/buffer_spec.lua +++ b/tests/buffer_spec.lua @@ -3,6 +3,8 @@ local buffer = require("neohack.buffer") local state = require("neohack.state") local Entity = require("neohack.entity") local message = require("neohack.message") +local Player = require("neohack.player") +local helpers = require("tests.helpers") local match = require("luassert.match") ---@diagnostic disable-next-line: undefined-field @@ -22,9 +24,9 @@ local function set_buffer(cells) } end -describe("get_entity_at_pos", function() - message.notify_func = print +helpers.setup_game() +describe("get_entity_at_pos", function() it("handles nil", function() set_buffer({ cells = { @@ -32,7 +34,7 @@ describe("get_entity_at_pos", function() }, }) - local result = buffer.get_entity_at_pos(1, 1) + local result = buffer.get_entity_at_pos(1, 1, true) eq(nil, result) end) @@ -48,4 +50,23 @@ describe("get_entity_at_pos", function() ---@diagnostic disable-next-line: need-check-nil eq("entity e", result.name) end) + + it("returns the player", function() + set_buffer({ + { newE("a"), newE("b"), newE("c") }, + { newE("d"), newE("e"), newE("f") }, + { newE("g"), newE("h"), newE("i") }, + }) + + state.player.movement.row = 2 + state.player.movement.col = 2 + + local result = buffer.get_entity_at_pos(2, 2) + not_nil(result) + ---@diagnostic disable-next-line: need-check-nil + eq("player", result.name) + + local result2 = buffer.get_entity_at_pos(2, 2, true) + eq("entity e", result2.name) + end) end) diff --git a/tests/components/combat_spec.lua b/tests/components/combat_spec.lua index 89be863..d15a8c2 100644 --- a/tests/components/combat_spec.lua +++ b/tests/components/combat_spec.lua @@ -18,7 +18,7 @@ describe("combat", function() it("dodge_skill", function() local weapon = helpers.bad_weapon local combat = new_combat("one", weapon) - eq(0.5, combat:dodge_skill()) + eq(0.1, combat:dodge_skill()) end) it("deflect_skill", function() @@ -30,7 +30,7 @@ describe("combat", function() it("weapon_skill", function() local weapon = helpers.bad_weapon local combat = new_combat("one", weapon) - eq(0.5, combat:weapon_skill()) + eq(0.1, combat:weapon_skill()) end) end) diff --git a/tests/components/decision_spec.lua b/tests/components/decision_spec.lua index 3715f31..8184ff4 100644 --- a/tests/components/decision_spec.lua +++ b/tests/components/decision_spec.lua @@ -1,5 +1,6 @@ local helpers = require("tests.helpers") local Decision = require("neohack.components.decision") +local Def = require("neohack.def") ---@diagnostic disable-next-line: undefined-field local eq = assert.is.equal @@ -11,4 +12,35 @@ describe("decision", function() local one = Decision.new(helpers.new_entity("one"), 0.5) eq(0.5, one.parent.attributes.randomness) end) + + it("choose_target", function() + local one = Decision.new(helpers.new_entity("enemy"), 0.5) + + local enemy = helpers.new_entity("one") + enemy.type = Def.DefType.enemy + local player = helpers.new_entity("player") + player.type = Def.DefType.player + local friend = helpers.new_entity("friend") + friend.type = Def.DefType.friend + local item = helpers.new_entity("item") + item.type = Def.DefType.item + + one:choose_target({ one.parent, enemy }) + eq(nil, one.parent.attributes.target) + + one:choose_target({ one.parent, enemy, item }) + eq("item", one.parent.attributes.target.type) + + one.parent.attributes.target = nil + one:choose_target({ one.parent, enemy, item, friend }) + eq("friend", one.parent.attributes.target.type) + + -- keep old target + one:choose_target({ one.parent, enemy, item, friend, player }) + eq("friend", one.parent.attributes.target.type) + + one.parent.attributes.target = nil + one:choose_target({ one.parent, enemy, item, friend, player }) + eq("player", one.parent.attributes.target.type) + end) end) diff --git a/tests/components/inventory_spec.lua b/tests/components/inventory_spec.lua index 11254e1..9a138ec 100644 --- a/tests/components/inventory_spec.lua +++ b/tests/components/inventory_spec.lua @@ -71,26 +71,27 @@ describe("slots", function() it("wear", function() local inventory = Inventory.new(helpers.new_entity("one")) - eq("body= feet= head= left_hand= right_hand= ", inventory:get_wearing()) + eq("", inventory:get_wearing()) + eq(0, #inventory:get_wearing()) inventory:wear("0", "head") - eq("body= feet= head= left_hand= right_hand= ", inventory:get_wearing()) + eq("", inventory:get_wearing()) local one = defs.new_entity_from_char("!", 1, 1) eq("long_sword", one.name) - inventory:pickup_item(one) + inventory:pickup(one) inventory:wear("1", "right_hand") - eq("body= feet= head= left_hand= right_hand=long_sword ", inventory:get_wearing()) + eq("right_hand=long_sword ", inventory:get_wearing()) eq("long_sword", inventory:get_weapon().name) local two = defs.new_entity_from_char("$", 1, 1) eq("coin", two.name) - inventory:pickup_item(two) + inventory:pickup(two) inventory:wear("coin", "right_hand") - eq("body= feet= head= left_hand= right_hand=coin ", inventory:get_wearing()) + eq("right_hand=coin ", inventory:get_wearing()) inventory:wear("0", "right_hand") - eq("body= feet= head= left_hand= right_hand= ", inventory:get_wearing()) + eq("", inventory:get_wearing()) end) end) @@ -130,7 +131,7 @@ describe("pilfer", function() one.inventory:pickup(two) one.inventory:pickup(three) one.inventory:wear("1", "right_hand") - eq("body= feet= head= left_hand= right_hand=wooden_leg ", one.inventory:get_wearing()) + eq("right_hand=wooden_leg ", one.inventory:get_wearing()) eq(1, #one.inventory.items) eq("l", one.inventory.slots[state.slot_types.right_hand].movement.char) diff --git a/tests/components/speak_spec.lua b/tests/components/speak_spec.lua index b64ebfa..eebfade 100644 --- a/tests/components/speak_spec.lua +++ b/tests/components/speak_spec.lua @@ -2,6 +2,7 @@ local helpers = require("tests.helpers") local Speak = require("neohack.components.speak") local state = require("neohack.state") local Def = require("neohack.def") +local Spell = require("neohack.spells") ---@diagnostic disable-next-line: undefined-field local eq = assert.is.equal @@ -16,7 +17,7 @@ describe("speak", function() it("say word", function() local one = Speak.new(helpers.new_entity("one")) - one:say({ "hello" }) + one:say("hello", "") end) it("say and heard", function() @@ -26,7 +27,7 @@ describe("speak", function() }, {} end local one = Speak.new(helpers.new_entity("one")) - one:say({ "hello" }) + one:say("hello", "enemy") end) it("cast", function() @@ -36,6 +37,47 @@ describe("speak", function() }, {} end local one = Speak.new(helpers.new_entity("one")) - one:say({ "hello" }) + local spell = helpers.new_entity("spell") + spell.speak.spell = { + inscription = "spell", + effect = Spell.effects.friendship, + } + one.parent.inventory:pickup(spell) + + one:say("spell", "enemy") + end) + + it("spells_spoken", function() + state.scan_area = function() + return { + { type = Def.DefType.enemy, name = "enemy" }, + }, {} + end + local one = Speak.new(helpers.new_entity("one")) + local spell = helpers.new_entity("spell") + spell.speak.spell = { + inscription = "magic_word", + effect = Spell.effects.friendship, + } + spell.type = Def.DefType.item + one.parent.inventory:pickup(spell) + eq("1:spell\n", one.parent.inventory:get_inventory_item_with_index()) + + local casting, effects = one:spells_spoken("magic_word") + eq(1, #casting) + eq("friendship ", effects) + end) + + it("entities_that_heard", function() + state.scan_area = function() + return { + { type = Def.DefType.enemy, name = "enemy" }, + }, {} + end + local one = Speak.new(helpers.new_entity("one")) + + local entities = one:entities_that_heard("enemy") + eq(1, #entities) + eq("enemy", entities[1].name) end) end) diff --git a/tests/components/vision_spec.lua b/tests/components/vision_spec.lua index 1321207..bda957c 100644 --- a/tests/components/vision_spec.lua +++ b/tests/components/vision_spec.lua @@ -10,14 +10,14 @@ describe("vision", function() describe("skills", function() it("search_skill", function() - local one = Vision.new(helpers.new_entity("one"), 1) + local one = Vision.new(helpers.new_entity("one"), 1, 10) eq(0.5, one:search_skill()) end) end) describe("view_distance", function() it("try_sneak success", function() - local one = Vision.new(helpers.new_entity("one"), 1) + local one = Vision.new(helpers.new_entity("one"), 1, 10) eq(10, one:view_distance()) local two = defs.new_entity_from_char("!", 1, 1) diff --git a/tests/entity_spec.lua b/tests/entity_spec.lua index c5d1bcf..bc1543a 100644 --- a/tests/entity_spec.lua +++ b/tests/entity_spec.lua @@ -9,7 +9,7 @@ describe("entity", function() it("inspect", function() eq( - "name=one char=o row=1 col=1 health=10 durability=10 damage=1 hit_rate=0.5 dodge_skill=0.5 weapon_skill=0.5 deflect_skill=0.01 sneak_skill=0.5 fuse_skill=0.5 eat_skill=0.1 view_distance=10 search_skill=0.5 vision=0.5 inscription= randomness=0.1 wearing: body= feet= head= left_hand= right_hand= \nitems:\n ", + "name=one char=o row=1 col=1 health=10 durability=10 damage=1 weapon_damage=1 hit_rate=0.5 dodge_skill=0.1 weapon_skill=0.1 deflect_skill=0.01 sneak_skill=0.5 fuse_skill=0.5 eat_skill=0.1 view_distance=10 search_skill=0.5 vision=0.5 inscription= randomness=0.1 wearing: \nitems:\n ", helpers.new_entity("one"):inspect() ) end) @@ -18,6 +18,6 @@ describe("entity", function() local attrs = helpers.new_entity("one"):get_attributes() eq("number", type(attrs["health"])) eq(10, attrs["health"]) - eq(22, #utils.keys(attrs)) + eq(19, #utils.keys(attrs)) end) end) diff --git a/tests/generated_defs_spec.lua b/tests/generated_defs_spec.lua index 03fa9c4..bb7146d 100644 --- a/tests/generated_defs_spec.lua +++ b/tests/generated_defs_spec.lua @@ -11,13 +11,13 @@ describe("attributes", function() helpers.setup_game() it("example_entity", function() local attrs = generated_defs.example_entity():get_attributes() - eq(23, #utils.keys(attrs)) + eq(20, #utils.keys(attrs)) eq("number", type(attrs.damage)) end) it("numeric_attributes", function() local attrs = generated_defs.numeric_attributes() - eq(17, #utils.keys(attrs)) + eq(15, #utils.keys(attrs)) end) end) diff --git a/tests/helpers.lua b/tests/helpers.lua index bdb7dad..5511f0d 100644 --- a/tests/helpers.lua +++ b/tests/helpers.lua @@ -12,6 +12,7 @@ local Movement = require("neohack.components.movement") local moves = require("neohack.moves") local generated_defs = require("neohack.generated_defs") local Entity = require("neohack.entity") +local Player = require("neohack.player") local M = {} @@ -58,6 +59,10 @@ M.setup_game = function() } generated_defs.enemy_list = M.word_list generated_defs.item_list = M.word_list + + state.player = Player.new() + state.player.movement.row = 1 + state.player.movement.col = 1 end M.good_weapon = function() @@ -109,6 +114,7 @@ M.new_entity = function(name) hit_rate = 0.5, vision = 0.5, randomness = 0.1, + view_distance = 10, } return Entity.new(def, 1, 1) end diff --git a/tests/map_spec.lua b/tests/map_spec.lua index 632181d..147b497 100644 --- a/tests/map_spec.lua +++ b/tests/map_spec.lua @@ -27,12 +27,15 @@ local function newE(name, row, col) type = Def.DefType.friend elseif name == " " then type = Def.DefType.floor + elseif name == "P" then + type = Def.DefType.player end ---@diagnostic disable-next-line: missing-fields return Entity.new({ char = name, name = "entity " .. name, type = type, + block_vision = true, }, row, col) end @@ -59,8 +62,10 @@ local function assertFound(expected, found) if found then eq(#expected, #found) for i, e in ipairs(expected) do - eq(e[1], found[i].movement.row, e[1] .. " " .. e[2]) - eq(e[2], found[i].movement.col, e[1] .. " " .. e[2]) + local row = found[i].movement.row + local col = found[i].movement.col + eq(e[1], row, "wrong row " .. row .. " expected " .. e[1] .. "," .. e[2]) + eq(e[2], col, "wrong col " .. col .. " expected " .. e[1] .. "," .. e[2]) end end end @@ -377,3 +382,30 @@ describe("scan_area", function() assertFound({ { 2, 4 }, { 3, 3 }, { 4, 4 } }, items) end) end) + +describe("get_visible_entities", function() + it("finds things", function() + state.player = newE("P", 2, 3) + local cells = { + { "I", " ", " ", "x", "E" }, + { " ", "E", " ", "I", "x" }, + { "x", " ", "I", " ", " " }, + { " ", "E", " ", "I", " " }, + { "I", " ", " ", " ", "I" }, + } + set_buffer(cells) + local entity = buffer.buffers[1].cells[3][3] + + local found1 = map.get_visible_entities(bufnr, entity, 1) + eq(1, #found1) + assertFound({ { 2, 3 } }, found1) + + local found2 = map.get_visible_entities(bufnr, entity, 2) + eq(6, #found2) + assertFound({ { 2, 2 }, { 2, 3 }, { 2, 4 }, { 3, 1 }, { 4, 2 }, { 4, 4 } }, found2) + + local found3 = map.get_visible_entities(bufnr, entity, 3) + eq(8, #found3) + assertFound({ { 1, 4 }, { 2, 2 }, { 2, 3 }, { 2, 4 }, { 2, 5 }, { 3, 1 }, { 4, 2 }, { 4, 4 } }, found3) + end) +end) diff --git a/tests/player_spec.lua b/tests/player_spec.lua index e073335..2e2219d 100644 --- a/tests/player_spec.lua +++ b/tests/player_spec.lua @@ -27,7 +27,7 @@ end) describe("corpse", function() it("drop_corpse", function() - local player = Player.new() + local player = state.player local one = defs.new_entity_from_char("$", 1, 1) local two = defs.new_entity_from_char("!", 1, 1) @@ -38,12 +38,12 @@ describe("corpse", function() player.inventory:wear("coin", "right_hand") eq(1, #player.inventory.items) eq("coin", player.inventory.slots["r"].name) - eq("body= feet= head= left_hand= right_hand=coin ", player.inventory:get_wearing()) + eq("right_hand=coin ", player.inventory:get_wearing()) eq(false, player.health:check_dead()) player.attributes.health = 0 eq(true, player.health:check_dead()) - player:died(1, 1) + player:died() local player_corpse = buffer.buffers[1].cells[1][1] eq(constant_defs.enemy_corpse, player_corpse.movement.char) @@ -58,7 +58,7 @@ end) describe("inventory", function() local player = Player.new() it("get_inventory", function() - eq("wearing: body= feet= head= left_hand= right_hand= \nitems:\n", player.inventory:get_inventory()) + eq("wearing: \nitems:\n", player.inventory:get_inventory()) end) it("get_weapon", function() @@ -77,9 +77,9 @@ describe("inspect", function() eq(0.5, player.sneak:sneak_skill()) eq(0.5, player.fuse:fuse_skill()) eq(0.1, player.eat:eat_skill()) - eq(0.01, player.combat:deflect_skill()) + eq(0.05, player.combat:deflect_skill()) eq( - "name=player char=@ row=0 col=0 health=10 durability=20 damage=1 hit_rate=0.5 dodge_skill=0.5 weapon_skill=0.5 deflect_skill=0.01 sneak_skill=0.5 fuse_skill=0.5 eat_skill=0.1 view_distance=10 search_skill=0.5 vision=0.5 inscription= randomness=0.01 wearing: body= feet= head= left_hand= right_hand= \nitems:\n turns:0", + "name=player char=@ row=0 col=0 health=10 durability=20 damage=1 weapon_damage=1 hit_rate=0.5 dodge_skill=0.5 weapon_skill=0.5 deflect_skill=0.05 sneak_skill=0.5 fuse_skill=0.5 eat_skill=0.1 view_distance=10 search_skill=0.5 vision=0.5 inscription= randomness=0.01 wearing: \nitems:\n turns:0", player:inspect() ) end)