|
-- Copyright (C) 2024 Theros <https://github.com/therosin>
|
|
--
|
|
-- This file is part of LuaWorldStateSystem.
|
|
--
|
|
-- LuaWorldStateSystem is free software: you can redistribute it and/or modify
|
|
-- it under the terms of the GNU General Public License as published by
|
|
-- the Free Software Foundation, either version 3 of the License, or
|
|
-- (at your option) any later version.
|
|
--
|
|
-- LuaWorldStateSystem is distributed in the hope that it will be useful,
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
-- GNU General Public License for more details.
|
|
--
|
|
-- You should have received a copy of the GNU General Public License
|
|
-- along with LuaWorldStateSystem. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
--- Deep copy a table, including its metatable
|
|
--- avoids shared references between the original and the copy
|
|
---@param orig table original to copy
|
|
---@return table copy of the table
|
|
function table.deepcopy(orig)
|
|
local orig_type = type(orig)
|
|
local copy
|
|
if orig_type == 'table' then
|
|
copy = {}
|
|
for orig_key, orig_value in next, orig, nil do
|
|
copy[table.deepcopy(orig_key)] = table.deepcopy(orig_value)
|
|
end
|
|
setmetatable(copy, table.deepcopy(getmetatable(orig)))
|
|
else
|
|
copy = orig
|
|
end
|
|
return copy
|
|
end
|
|
|
|
---------------------- Constants ----------------------
|
|
|
|
--- Player Attribute, e.g. luck, strength, etc.
|
|
local DATA_TYPE_ATTRIBUTE = "attribute"
|
|
--- Player Relationship, e.g. with other characters, higher value means better relationship
|
|
--- recommended to use character names as keys, values could be 0 = neutral, negat
|
|
local DATA_TYPE_RELATIONSHIP = "relationship"
|
|
local DATA_TYPE_LORE_KEY = "loreKey"
|
|
|
|
|
|
---------------------- PlayerData Class ----------------------
|
|
|
|
--- Base Attributes Table
|
|
--- The default attribute range is from -5 to +5, with 0 being average human capability.
|
|
--- Players start with a fixed number of points to allocate up to a soft cap of +3 during character creation.
|
|
--- Attributes beyond +3, up to +5, represent levels of proficiency and mastery earned through gameplay achievements.
|
|
--- This is just an example; attributes can be added or removed as needed.
|
|
--- The default aims to provide a basic set common to RPG systems inspired by D&D 5e or similar frameworks.
|
|
-- Note: This system encourages a balanced approach to character creation, with the opportunity to develop and specialize as players progress in the game.
|
|
local base_attributes = {
|
|
strength = 0, -- Physical power
|
|
dexterity = 0, -- Agility and speed
|
|
constitution = 0, -- Health and stamina
|
|
intelligence = 0, -- Mental acuity and magic
|
|
wisdom = 0, -- Insight and perception
|
|
charisma = 0, -- Social influence
|
|
luck = 0, -- Chance events
|
|
agility = 0, -- Physical reflexes and evasion
|
|
endurance = 0, -- Sustained physical activity
|
|
perception = 0, -- Environmental awareness
|
|
magic = 0, -- Magical power
|
|
technology = 0, -- Technological skill
|
|
}
|
|
|
|
--- Base Relationships Table
|
|
--- Default Relationship Levels and Their Meanings:
|
|
--- -10 to -7: Hatred. NPCs with this level of disdain will actively seek to harm the player or obstruct their goals. Recovery from this state requires significant effort.
|
|
--- -6 to -4: Strong Dislike. Characters at this level are openly hostile or uncooperative, refusing to assist the player and may spread negative information.
|
|
--- -3 to -1: Distrust. These NPCs are skeptical of the player's intentions and offer limited help, requiring persuasion for even minor assistance.
|
|
--- 0: Neutral. The baseline relationship status. NPCs are indifferent towards the player, interactions are basic and transactional.
|
|
--- +1 to +3: Acquaintance. Initial positive impressions fall here. NPCs are slightly more open and may offer tips or minor assistance.
|
|
--- +4 to +6: Friendly. NPCs at this level like the player and will provide help within their capabilities, including sharing information or resources.
|
|
--- +7 to +8: Trusted Ally. These NPCs have a high level of trust in the player, willing to support them in risky situations and may share critical secrets.
|
|
--- +9: Close Companion. Beyond just allies, NPCs at this level have a deep personal connection with the player, ready to join in their battles and follow them into danger, showing considerable sacrifices.
|
|
--- +10: Deep Bond. The highest level of relationship, indicating a profound, possibly romantic or familial bond. NPCs here are committed to the player above all else, potentially laying down their life for them.
|
|
local base_relationships = {}
|
|
|
|
--- Base LoreKeys Table
|
|
--- LoreKeys are flags that represent significant player actions, achievements, or discoveries within the game. They serve as a versatile tool for tracking progress and branching the game's narrative or mechanics based on player choices.
|
|
--- Examples of LoreKeys include missions completed, secrets uncovered, alliances formed, enemies made, and pivotal decisions taken. These keys can unlock new storylines, affect relationships, open or close paths, and influence the world dynamically.
|
|
--- LoreKeys can be used as conditions for various in-game logic, such as unlocking dialogue options with NPCs, altering NPC behavior, triggering events, or revealing hidden areas and items.
|
|
--- This system allows for a rich, personalized gaming experience, where the consequences of player actions are tangible and meaningful.
|
|
local base_loreKeys = {
|
|
-- Example structure; actual lore keys would be dynamically added as players progress through the game.
|
|
-- Example: discoveredSecretCave = true, -- Indicates the player has discovered a secret cave.
|
|
-- Example: formedAllianceWithElves = true, -- Represents an alliance formed with the elves, affecting relations with other factions.
|
|
-- Example: defeatedAncientDragon = true, -- A significant achievement that might be recognized by characters within the game world.
|
|
-- Add more keys as needed to reflect the breadth of player actions and world states.
|
|
}
|
|
|
|
PlayerData = {}
|
|
PlayerData.__index = PlayerData
|
|
|
|
-- Method to get base table copies
|
|
function PlayerData.get_base_meta()
|
|
return table.deepcopy(base_attributes), table.deepcopy(base_relationships), table.deepcopy(base_loreKeys)
|
|
end
|
|
|
|
--- Create a new PlayerData instance
|
|
---@param initialAttributes? table -- Initial attributes table
|
|
---@param initialRelationships? table -- Initial relationships table
|
|
---@param initialLoreKeys? table -- Initial lore keys table
|
|
---@return table
|
|
function PlayerData.new(initialAttributes, initialRelationships, initialLoreKeys)
|
|
local self = setmetatable({}, PlayerData)
|
|
self.attributes = initialAttributes or table.deepcopy(base_attributes)
|
|
self.relationships = initialRelationships or table.deepcopy(base_relationships)
|
|
self.loreKeys = initialLoreKeys or table.deepcopy(base_loreKeys)
|
|
self.listeners = {} -- List to store change listeners
|
|
self.debug = false -- Set debug flag to false by default
|
|
return self
|
|
end
|
|
|
|
--- Update a relationship value
|
|
---@param relationship_name string -- Name of the relationship
|
|
---@param value number -- Value to add to the relationship (positive or negative integer to increase or decrease the relationship)
|
|
function PlayerData:update_relationship(relationship_name, value)
|
|
self.relationships[relationship_name] = (self.relationships[relationship_name] or 0) + value
|
|
self:trigger_change_callbacks(DATA_TYPE_RELATIONSHIP, relationship_name, self.relationships[relationship_name] - value, self.relationships[relationship_name])
|
|
end
|
|
|
|
--- Update an attribute value
|
|
---@param attribute_name string -- Name of the attribute
|
|
---@param value number -- Value to add to the attribute (positive or negative integer to increase or decrease the attribute)
|
|
function PlayerData:update_attribute(attribute_name, value)
|
|
self.attributes[attribute_name] = (self.attributes[attribute_name] or 0) + value
|
|
self:trigger_change_callbacks(DATA_TYPE_ATTRIBUTE, attribute_name, self.attributes[attribute_name] - value, self.attributes[attribute_name])
|
|
end
|
|
|
|
--- Set a lore key (will create a new key if it doesn't exist)
|
|
---@param key_name string -- Name of the lore key
|
|
---@param value boolean -- Value to set for the lore key
|
|
function PlayerData:set_lore_key(key_name, value)
|
|
self.loreKeys[key_name] = value
|
|
self:trigger_change_callbacks(DATA_TYPE_LORE_KEY, key_name, not value, value)
|
|
end
|
|
|
|
--- Trigger change callbacks
|
|
---@param data_type string -- Type of data (attribute, relationship, loreKey)
|
|
---@param key string -- Key name
|
|
---@param old_value any -- Old value
|
|
---@param new_value any -- New values
|
|
function PlayerData:trigger_change_callbacks(data_type, key, old_value, new_value)
|
|
for _, listener in ipairs(self.listeners) do
|
|
listener(data_type, key, old_value, new_value)
|
|
end
|
|
end
|
|
|
|
--- Add a change listener
|
|
---@param listener fun(data_type: string, key: string, old_value: any, new_value: any) -- Listener function
|
|
function PlayerData:add_change_listener(listener)
|
|
table.insert(self.listeners, listener)
|
|
end
|
|
|
|
local function log_change(data_type, key, old_value, new_value)
|
|
print(string.format("Change event: %s %s changed from %s to %s", data_type, key, tostring(old_value), tostring(new_value)))
|
|
end
|
|
|
|
--- Set debug mode
|
|
--- When debug mode is enabled, a debug listener is added to log all changes to attributes, relationships, and lore keys.
|
|
---@param debug boolean -- Debug mode flag
|
|
function PlayerData:set_debug_mode(debug)
|
|
self.debug = debug
|
|
|
|
if debug then
|
|
self:add_change_listener(log_change)
|
|
else
|
|
-- remove the listener if debug is false
|
|
for i, listener in ipairs(self.listeners) do
|
|
if listener == log_change then
|
|
table.remove(self.listeners, i)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Check if the player meets the criteria for a set of events
|
|
---@param eventCriteria table -- Criteria for each event
|
|
---@return table -- Results for each event
|
|
function PlayerData:meets_criteria(eventCriteria)
|
|
local eventResults = {}
|
|
for eventName, criteriaList in pairs(eventCriteria) do
|
|
local allCriteriaMet = true
|
|
for _, condition in ipairs(criteriaList) do
|
|
if condition.type == "attribute" and (not self.attributes[condition.name] or self.attributes[condition.name] < condition.value) then
|
|
allCriteriaMet = false
|
|
break
|
|
elseif condition.type == "relationship" and (not self.relationships[condition.name] or self.relationships[condition.name] < condition.value) then
|
|
allCriteriaMet = false
|
|
break
|
|
elseif condition.type == "loreKey" and (not self.loreKeys[condition.name] or self.loreKeys[condition.name] ~= condition.value) then
|
|
allCriteriaMet = false
|
|
break
|
|
end
|
|
end
|
|
eventResults[eventName] = allCriteriaMet
|
|
end
|
|
return eventResults
|
|
end
|
|
|
|
|
|
---------------------- Example Usage ----------------------
|
|
|
|
-- -- setup base attributes, relationships, and loreKeys
|
|
-- local base_attributes, base_relationships, base_loreKeys = PlayerData.get_base_meta()
|
|
|
|
-- -- set some initial values for the player
|
|
-- base_attributes.strength = 2
|
|
-- base_attributes.dexterity = 1
|
|
-- base_attributes.constitution = 1
|
|
-- base_attributes.intelligence = 0
|
|
-- base_attributes.wisdom = 0
|
|
-- base_attributes.charisma = 2
|
|
-- -- add a new attribute
|
|
-- base_attributes.generosity = 0
|
|
-- base_attributes.honesty = 0
|
|
|
|
-- -- set some initial relationships
|
|
-- base_relationships.Alex = 2
|
|
-- base_relationships.Jordan = -2
|
|
|
|
-- local player = PlayerData.new(base_attributes, base_relationships, base_loreKeys)
|
|
-- player:set_debug_mode(true)
|
|
|
|
-- -- Updating attributes and relationships
|
|
-- player:update_attribute("generosity", 5)
|
|
-- player:update_relationship("Alex", 3)
|
|
|
|
-- -- Setting a lore key
|
|
-- player:set_lore_key("discovered_secret", true)
|
|
|
|
-- -- Defining criteria for multiple events
|
|
-- local eventCriteria = {
|
|
-- event1 = {
|
|
-- {type = "attribute", name = "generosity", value = 5},
|
|
-- {type = "relationship", name = "Alex", value = 3}
|
|
-- },
|
|
-- event2 = {
|
|
-- {type = "relationship", name = "Jordan", value =3},
|
|
-- {type = "loreKey", name = "discovered_secret", value = true}
|
|
-- },
|
|
-- event3 = {
|
|
-- {type = "attribute", name = "honesty", value = 4},
|
|
-- {type = "loreKey", name = "joined_guild", value = true}
|
|
-- }
|
|
-- }
|
|
|
|
-- -- Evaluate criteria for events
|
|
-- local eventResults = player:meets_criteria(eventCriteria)
|
|
|
|
-- for eventName, met in pairs(eventResults) do
|
|
-- print(eventName .. ": " .. tostring(met))
|
|
-- end
|
|
|
|
|
|
return PlayerData |