--[[ Copyright (c) 2009 Peter "Corsix" Cawley Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --]] --! An `Entity` which occupies a single tile and is capable of moving around the map. class "Humanoid" (Entity) local TH = require "TH" local walk_animations = permanent"humanoid_walk_animations"({}) local door_animations = permanent"humanoid_door_animations"({}) local die_animations = permanent"humanoid_die_animations"({}) local mood_icons = permanent"humanoid_mood_icons"({}) local function anims(name, walkN, walkE, idleN, idleE, doorL, doorE, knockN, knockE, swingL, swingE) walk_animations[name] = { walk_east = walkE, walk_north = walkN, idle_east = idleE, idle_north = idleN, } door_animations[name] = { entering = doorE, leaving = doorL, entering_swing = swingE, leaving_swing = swingL, knock_north = knockN, knock_east = knockE, } end local function die_anims(name, fall, rise, wings, hands, fly, extra) die_animations[name] = { fall_east = fall, rise_east = rise, wings_east = wings, hands_east = hands, fly_east = fly, extra_east = extra, } end local function moods(name, iconNo, prio, alwaysOn) mood_icons[name] = {icon = iconNo, priority = prio, on_hover = alwaysOn} end -- | Walk animations | -- | Name |WalkN|WalkE|IdleN|IdleE|DoorL|DoorE|KnockN|KnockE|SwingL|SwingE| Notes -----+---------------------------+-----+-----+-----+-----+-----+-----+------+------+-------+---------+ anims("Standard Male Patient", 16, 18, 24, 26, 182, 184, 286, 288, 2040, 2042) -- 0-16, ABC anims("Gowned Male Patient", 406, 408, 414, 416) -- 0-10 anims("Stripped Male Patient", 818, 820, 826, 828) -- 0-16 anims("Alternate Male Patient", 2704, 2706, 2712, 2714, 2748, 2750, 2764, 2766) -- 0-10, ABC anims("Slack Male Patient", 1484, 1486, 1492, 1494, 1524, 1526, 2764, 1494) -- 0-14, ABC anims("Transparent Male Patient", 1064, 1066, 1072, 1074, 1104, 1106, 1120, 1074) -- 0-16, ABC anims("Standard Female Patient", 0, 2, 8, 10, 258, 260, 294, 296, 2864, 2866) -- 0-16, ABC anims("Gowned Female Patient", 2876, 2878, 2884, 2886) -- 0-8 anims("Stripped Female Patient", 834, 836, 842, 844) -- 0-16 anims("Transparent Female Patient",3012, 3014, 3020, 3022, 3052, 3054, 3068, 3070) -- 0-8, ABC anims("Chewbacca Patient", 858, 860, 866, 868, 3526, 3528, 4150, 4152) anims("Elvis Patient", 978, 980, 986, 988, 3634, 3636, 4868, 4870) anims("Invisible Patient", 1642, 1644, 1840, 1842, 1796, 1798, 4192, 4194) anims("Alien Patient", 3598, 3600, 3606, 3608, nil, nil, nil, nil, 3626, 3628) anims("Doctor", 32, 34, 40, 42, 670, 672, nil, nil, 4750, 4752) anims("Surgeon", 2288, 2290, 2296, 2298) anims("Nurse", 1206, 1208, 1650, 1652, 3264, 3266, nil, nil, 3272, 3274) anims("Handyman", 1858, 1860, 1866, 1868, 3286, 3288, nil, nil, 3518, 3520) anims("Receptionist", 3668, 3670, 3676, 3678) -- Could do with door animations anims("VIP", 266, 268, 274, 276) anims("Grim Reaper", 994, 996, 1002, 1004) -- | Die Animations | -- | Name |FallE|RiseE|WingsE|HandsE|FlyE|ExtraE| Notes ----+--------------------------------+-----+-----+-----+-----+------+------+ die_anims("Standard Male Patient", 1682, 2434, 2438, 2446, 2450) -- Always facing east or south die_anims("Alternate Male Patient", 1682, 2434, 2438, 2446, 2450) die_anims("Slack Male Patient", 1682, 2434, 2438, 2446, 2450) -- TODO: Where is slack male transformation? Uses alternate male for now. die_anims("Transparent Male Patient", 4412, 2434, 2438, 2446, 2450, 4416) -- Extra = Transformation die_anims("Standard Female Patient", 3116, 3208, 3212, 3216, 3220, 4288) -- Extra = Slack tongue die_anims("Transparent Female Patient",4420, 3208, 3212, 3216, 3220, 4428) -- Extra = Transformation die_anims("Chewbacca Patient", 4182, 2434, 2438, 2446, 2450) -- Only males die... (1222) die_anims("Elvis Patient", 974, 2434, 2438, 2446, 2450, 4186) -- Extra = Transformation die_anims("Invisible Patient", 4200, 2434, 2438, 2446, 2450) die_anims("Alien Patient", 4882, 2434, 2438, 2446, 2450) -- | Available Moods | -- | Name |Icon|Priority|Show Always| Notes -----+-----------------+----+--------+-----------+ moods("reflexion", 4020, 5) -- Some icons should only appear when the player moods("cantfind", 4050, 3) -- hover over the humanoid moods("idea1", 2464, 10) -- Higher priority is more important. moods("idea2", 2466, 11) moods("idea3", 4044, 12) moods("staff_wait", 4054, 20) moods("tired", 3990, 30) moods("pay_rise", 4576, 40) moods("thirsty", 3986, 4) moods("cold", 3994, 0, true) -- These have no priority since moods("hot", 3988, 0, true) -- they will be shown when hovering moods("queue", 4568, 70) -- no matter what other priorities. moods("poo", 3996, 5) moods("money", 4018, 30) moods("patient_wait", 5006, 40) moods("epidemy1", 4566, 38) moods("epidemy2", 4570, 40) moods("epidemy3", 4572, 40) moods("epidemy4", 4574, 40) moods("sad1", 3992, 40) moods("sad2", 4000, 41) moods("sad3", 4002, 42) moods("sad4", 4004, 43) moods("sad5", 4006, 44) moods("sad6", 4008, 45) moods("sad7", 4578, 46) moods("dead", 4046, 60) moods("cured", 4048, 60) moods("emergency", 3914, 50) moods("exit", 4052, 60) local anim_mgr = TheApp.animation_manager for anim in values(door_animations, "*.entering") do anim_mgr:setMarker(anim, 0, {-1, 0}, 3, {-1, 0}, 9, {0, 0}) end for anim in values(door_animations, "*.leaving") do anim_mgr:setMarker(anim, 1, {0, 0.4}, 4, {0, 0.4}, 7, {0, 0}, 11, {0, -1}) end for anim in values(door_animations, "*.entering_swing") do anim_mgr:setMarker(anim, 0, {-1, 0}, 8, {0, 0}) end for anim in values(door_animations, "*.leaving_swing") do anim_mgr:setMarker(anim, 0, {0.1, 0}, 9, {0, -1}) end --!param ... Arguments for base class constructor. function Humanoid:Humanoid(...) self:Entity(...) self.action_queue = {} self.last_move_direction = "east" self.attributes = {} self.attributes["warmth"] = 0.29 self.attributes["happiness"] = 1 self.active_moods = {} self.should_knock_on_doors = false end -- Function which is called when the user clicks on the `Humanoid`. --!param ui (GameUI) The UI which the user in question is using. --!param button (string) One of: "left", "middle", "right". function Humanoid:onClick(ui, button) if TheApp.config.debug then -- for debugging local name = "humanoid" if self.profile then name = self.profile.name end print("-----------------------------------") print("Clicked on ".. name) print("Class: ", self.humanoid_class) if self.humanoid_class == "Doctor" then print(string.format("Skills: (%.3f) Surgeon (%.3f) Psych (%.3f) Researcher (%.3f)", self.profile.skill or 0, self.profile.is_surgeon or 0, self.profile.is_psychiatrist or 0, self.profile.is_researcher or 0)) end print(string.format("Warmth: %.3f Happiness: %.3f Fatigue: %.3f", self.attributes["warmth"] or 0, self.attributes["happiness"] or 0, self.attributes["fatigue"] or 0)) print("") print("Actions:") for i = 1, #self.action_queue do if self.action_queue[i].room_type then print(self.action_queue[i].name .. " - " .. self.action_queue[i].room_type) elseif self.action_queue[i].object then print(self.action_queue[i].name .. " - " .. self.action_queue[i].object.object_type.id) elseif self.action_queue[i].name == "walk" then print(self.action_queue[i].name .. " - going to " .. self.action_queue[i].x .. ":" .. self.action_queue[i].y) else print(self.action_queue[i].name) end end print("-----------------------------------") end end function Humanoid:onDestroy() local x, y = self.tile_x, self.tile_y if x and y then local notify_object = self.world:getObjectToNotifyOfOccupants(x, y) if notify_object then notify_object:onOccupantChange(-1) end end return Entity.onDestroy(self) end -- Set the `Hospital` which is responsible for treating or employing the -- `Humanoid`. In single player games, this has little effect, but it is very -- important in multiplayer games. --!param hospital (Hospital, nil) The `Hospital` which should be responsible -- for the `Humanoid`. If nil, then the `Humanoid` is despawned. function Humanoid:setHospital(hospital) self.hospital = hospital if not hospital or not hospital.is_in_world then local spawn_points = self.world.spawn_points self:setNextAction{ name = "spawn", mode = "despawn", point = spawn_points[math.random(1, #spawn_points)], must_happen = true, } end end -- Function to activate/deactivate moods of a humanoid. -- If mood_name is nil it is considered a refresh only. function Humanoid:setMood(mood_name, activate) if mood_name then if activate and activate ~= "deactivate" then if self.active_moods[mood_name] then return -- No use doing anything if it already exists. end self.active_moods[mood_name] = mood_icons[mood_name] else if not self.active_moods[mood_name] then return -- No use doing anything if the mood isn't there anyway. end self.active_moods[mood_name] = nil end end local new_mood = nil -- TODO: Make equal priorities cycle, or make all moods unique for key, value in pairs(self.active_moods) do if new_mood then -- There is a mood, check priorities. if new_mood.priority < value.priority then new_mood = value end else if not value.on_hover then new_mood = value end end end self:setMoodInfo(new_mood) end -- Is the given mood in the list of active moods. function Humanoid:isMoodActive(mood) for i, _ in pairs(self.active_moods) do if i == mood then return true end end return false end function Humanoid.getIdleAnimation(humanoid_class) return assert(walk_animations[humanoid_class], "Invalid humanoid class").idle_east end function Humanoid:getCurrentMood() if self.mood_info then return self.mood_info end end local function Humanoid_startAction(self) local action = self.action_queue[1] assert(action, "Empty action queue") -- Call the action start handler TheApp.humanoid_actions[action.name](action, self) if action == self.action_queue[1] and action.todo_interrupt then local high_priority = action.todo_interrupt == "high" action.todo_interrupt = nil local on_interrupt = action.on_interrupt if on_interrupt then action.on_interrupt = nil on_interrupt(action, self, high_priority) end end end function Humanoid:setNextAction(action, high_priority) -- Aim: Cleanly finish the current action (along with any subsequent actions -- which must happen), then replace all the remaining actions with the given -- one. local i = 1 local queue = self.action_queue local interrupted = false -- Skip over any actions which must happen while queue[i] and queue[i].must_happen do interrupted = true i = i + 1 end -- Remove actions which are no longer going to happen local done_set = {} for j = #queue, i, -1 do local removed = queue[j] queue[j] = nil if removed.until_leave_queue and not done_set[removed.until_leave_queue] then removed.until_leave_queue:removeValue(self) done_set[removed.until_leave_queue] = true end if removed.object and removed.object:isReservedFor(self) then removed.object:removeReservedUser(self) end end -- Add the new action to the queue queue[i] = action -- Interrupt the current action and queue other actions to be interrupted -- when they start. if interrupted then interrupted = queue[1] for j = 2, i - 1 do queue[j].todo_interrupt = high_priority and "high" or true end local on_interrupt = interrupted.on_interrupt if on_interrupt then interrupted.on_interrupt = nil on_interrupt(interrupted, self, high_priority or false) end else -- Start the action if it has become the current action Humanoid_startAction(self) end return self end function Humanoid:queueAction(action, pos) if pos then table.insert(self.action_queue, pos + 1, action) if pos == 0 then Humanoid_startAction(self) end else self.action_queue[#self.action_queue + 1] = action end return self end function Humanoid:finishAction(action) if action ~= nil then assert(action == self.action_queue[1], "Can only finish current action") end table.remove(self.action_queue, 1) Humanoid_startAction(self) end function Humanoid:setType(humanoid_class) assert(walk_animations[humanoid_class], "Invalid humanoid class: " .. tostring(humanoid_class)) self.walk_anims = walk_animations[humanoid_class] self.door_anims = door_animations[humanoid_class] self.die_anims = die_animations[humanoid_class] self.humanoid_class = humanoid_class if #self.action_queue == 0 then self:setNextAction {name = "idle"} end self.th:setPartialFlag(self.permanent_flags or 0, false) if humanoid_class == "Invisible Patient" then -- Invisible patients do not have very many pixels to hit, box works better self.permanent_flags = DrawFlags.BoundBoxHitTest else self.permanent_flags = nil end self.th:setPartialFlag(self.permanent_flags or 0) end -- Helper function for the common case of instructing a `Humanoid` to walk to -- a position on the map. Equivalent to calling `setNextAction` with a walk -- action. --!param tile_x (integer) The X-component of the Lua tile co-ordinates of the -- tile to walk to. --!param tile_y (integer) The Y-component of the Lua tile co-ordinates of the -- tile to walk to. --!param must_happen (boolean, nil) If true, then the walk action will not be -- interrupted. function Humanoid:walkTo(tile_x, tile_y, must_happen) self:setNextAction { name = "walk", x = tile_x, y = tile_y, must_happen = must_happen, } end -- Stub functions for handling fatigue. These are overridden by the staff subclass, -- but also defined here, so we can just call it on any humanoid function Humanoid:tire(amount) end function Humanoid:wake(amount) end function Humanoid:handleRemovedObject(object) local replacement_action if self.humanoid_class and self.humanoid_class == "Receptionist" then replacement_action = {name = "meander"} elseif object.object_type.id == "bench" then replacement_action = {name = "idle", must_happen = true} end for i, action in ipairs(self.action_queue) do if (action.name == "use_object" or action.name == "staff_reception") and action.object == object then if replacement_action then self:queueAction(replacement_action, i) end if i == 1 then local on_interrupt = action.on_interrupt action.on_interrupt = nil if on_interrupt then on_interrupt(action, self, true) end else table.remove(self.action_queue, i) self.associated_desk = nil -- NB: for the other case, this is already handled in the on_interrupt function end end end end -- Adjusts one of the `Humanoid`'s attributes. --!param attribute (string) One of: "happiness", "thirst", "toilet_need", "warmth". --!param amount (number) A value between -1 and +1. This amount is added to the -- existing value for the attribute, and is then capped to be between 0 and 1. function Humanoid:changeAttribute(attribute, amount) assert(amount <= 1 and amount >= -1, "Amount must be between -1 and 1") -- Receptionist is always 100% happy if self.humanoid_class and self.humanoid_class == "Receptionist" and attribute == "happiness" then self.attributes[attribute] = 1; return true end if self.attributes[attribute] then self.attributes[attribute] = self.attributes[attribute] + amount if self.attributes[attribute] > 1 then self.attributes[attribute] = 1 elseif self.attributes[attribute] < 0 then self.attributes[attribute] = 0 end end end -- Check if it is cold or hot around the humanoid and increase/decrease the -- feeling of warmth accordingly. Returns whether the calling function should proceed. function Humanoid:tickDay() -- No use doing anything if we're going home if self.going_home then return false end local temperature = self.world.map.th:getCellTemperature(self.tile_x, self.tile_y) self.attributes.warmth = self.attributes.warmth * 0.75 + temperature * 0.25 -- If it is too hot or too cold, start to decrease happiness and -- show the corresponding icon. Otherwise we could get happier instead. if self.attributes["warmth"] then -- Cold: less than 11 degrees C if self.attributes["warmth"] < 0.22 then self:changeAttribute("happiness", -0.02 * (0.22 - self.attributes["warmth"]) / 0.14) self:setMood("cold", "activate") -- Hot: More than 18 degrees C elseif self.attributes["warmth"] > 0.36 then self:changeAttribute("happiness", -0.02 * (self.attributes["warmth"] - 0.36) / 0.14) self:setMood("hot", "activate") -- Ideal: Between 11 and 18 else self:changeAttribute("happiness", 0.005) self:setMood("cold", "deactivate") self:setMood("hot", "deactivate") end end return true end -- Helper function that finds out if there is an action queued to use the specified object function Humanoid:goingToUseObject(object_type) for i, action in ipairs(self.action_queue) do if action.object and action.object.object_type.id == object_type then return true end end return false end