Skip to content

Add townies system with occasional helpers#2129

Merged
manuq merged 17 commits into
mainfrom
townies-system
Jun 2, 2026
Merged

Add townies system with occasional helpers#2129
manuq merged 17 commits into
mainfrom
townies-system

Conversation

@manuq
Copy link
Copy Markdown
Collaborator

@manuq manuq commented Apr 17, 2026

This adds a help system to the game. Quests creators can now define a dialogue tree for retelling the adventures recently lived before offering the magical threads collected to the Eternal Loom. People from the town gathers around to listen the retelling. If the player has collected a thread that matches one of the options, that option will be highlighted. And if that option is picked, one of the villagers will stand up, and "equipped" (indicated in the HUD) for helping later in the game. The thread collected indicates the type or category of the help, which can be interpreted by level creators in any way.

Level creators can add helper characters to the level. They will appear only if the player got help, and if their helper type matches the one obtained during the retelling. When the player interacts with this helper character, a dialogue is displayed to offer the help. If the player accepts, it is expected that the level creator does something, and then consume the help (disappearing from the HUD). For example: An imagination helper at the stealth level can fix the bridge, which is a shortcut to skip the most difficult part of the level, full of guards.

Details below.

Void quest: Add helpers

Add the following helpers:

  • 1_void_quest: Memory helper that displays a placeholder dialogue.
  • 1_void_quest: Spirit helper that slows down the enemy.
  • 2_grappling_hook: Imagination helper that makes needles appear in the optional loop.

Musician quest: Add helpers

Add the following helpers:

  • 1_music_puzzle: Memory helper that triggers a dialogue saying "Lore lore lore" as placeholder.
  • 2_ink_combat: Spirit helper that fills the hard to reach inkwell.
  • 3_stealth_level: Imagination helper that fixes the bridge.

Musician and Void quests: Add retelling dialogue

The dialogue lines should be considered placeholders.

Sokobans: Remove project setting to skip the sokobans

Sokobans cleanup: Remove incorporating threads state

Frays End, Eternal Loom: Add retelling and remove sokobans

Change the interaction of the eternal loom when the player is ready to offer the
threads. Now if the quest has retelling dialogue, display it. A retelling may
result in the player obtaining help, that can be used later in the game.

After the retelling, or if no retelling happened, trigger the incorporating
threads cutscene and let the corresponding elder congratulate the player.

Remove the sokoban puzzles, which are going to be repurposed. More cleanup on
this later.

Add HelperCharacter

Node that will be used to make characters become helpers in levels, for
consuming the help obtained during the retelling at the eternal loom. There is
no actual implementation, this help could be spent in imaginative ways and is
left to the level creators. A default dialogue is provided as example.

Add RetellingManager and RetellingTownie

The RetellingManager coordinates the retelling with the EternalLoom, to avoid
cluttering that node. It does so by listening to signals and calling a method.

StoryQuestProgress: Add helper character face to HUD

Listen to the global state for doing so. If there is helper state, instantiate
the face of the character according to its seed, and set the background color to
the type of help.

Quest: Add optional retelling dialogue to the resource

This dialogue will be displayed when interacting with the eternal loom, before
the threads offering.

CharacterRandomizer: Vary the animation when randomizing

If not, when randomizing multiple characters at the same time, they all end up
moving in sync. This is the case of the retelling townies in Fray's End.

CharacterRandomizer: Allow accessing the head sprite.

By setting an unique name to this node. This will be used to show the head in
the HUD.

InventoryItem: Add color constants

The colors will be used in the HUD as background to indicate the type of help.

Add global state for helper characters

Persist the type of help, matching one of the magical threads, and a character
seed for the visual features of the character.

Add methods to obtain and consume the help. And a signal to monitor changes.

Resolve #2119

@github-actions
Copy link
Copy Markdown

Play this branch at https://play.threadbare.game/branches/endlessm/townies-system/.

(This launches the game from the start, not directly at the change(s) in this pull request.)

@manuq manuq force-pushed the townies-system branch 2 times, most recently from 0948ae6 to 1c98b25 Compare April 20, 2026 15:35
@manuq manuq changed the title Townies system Townies system "occasional helpers" exploration Apr 20, 2026
@manuq
Copy link
Copy Markdown
Collaborator Author

manuq commented Apr 20, 2026

This is implementation for an idea that @wjt and I had this morning: a townie helps you with the most difficult part of the Stealth level, by fixing a bridge that opens a shortcut.

Here is one game_state.cfg to try it:

game_state-fix_bridge.zip

Video:

recording.webm

@manuq manuq mentioned this pull request Apr 20, 2026
@manuq
Copy link
Copy Markdown
Collaborator Author

manuq commented Apr 20, 2026

Update: I added 6 townies to quests now:

In musician quest:

  • Memory helper at music_puzzle.tscn: Tells a story. Just narrative / lore.
  • Imagination helper at stealth_level.tscn: Fixes a bridge to skip the difficult part.
  • Spirit helper at ink_combat_round_3.tscn: Helps the player by automatically filling one barrel (tries the hard cyan first)

In void quest:

  • Memory helper at void_runner.tscn: Tells a story. Just narrative / lore.
  • Imagination helper at grappling_hook_needles.tscn: Appears extra needles. The only way to collect certain buttons.
  • Spirit helper at void_runner.tscn: Helps the player by slowing down the Void (this should be reused or repurposed for the difficult last chasing scene!)

Useful game states to try:

game_states.zip

@manuq manuq force-pushed the townies-system branch 2 times, most recently from 0b22abd to 075be85 Compare April 28, 2026 15:18
@manuq manuq force-pushed the townies-system branch 6 times, most recently from 0a22e2e to 10d41d1 Compare May 29, 2026 14:40
@manuq
Copy link
Copy Markdown
Collaborator Author

manuq commented May 29, 2026

I rebased this and adapted it to the flawless new game state! Here are the new game states "snapshots" I took for testing this:
saved_games.zip

I´'m now polishing this to put it for review.

@manuq manuq force-pushed the townies-system branch 4 times, most recently from 4877509 to cb1f206 Compare June 1, 2026 23:11
@manuq manuq changed the title Townies system "occasional helpers" exploration Add townies system with occasional helpers Jun 1, 2026
@manuq manuq marked this pull request as ready for review June 1, 2026 23:14
@manuq manuq requested a review from a team as a code owner June 1, 2026 23:14
manuq added 6 commits June 1, 2026 20:14
Persist the type of help, matching one of the magical threads, and a character
seed for the visual features of the character.

Add methods to obtain and consume the help.
The NONE type is needed in the enum to better identify no state, when consuming
the help.

The colors will be used in the HUD as background to indicate the type of help.
By setting an unique name to this node. This will be used to show the head in
the HUD.
If not, when randomizing multiple characters at the same time, they all end up
moving in sync. This is the case of the retelling townies in Fray's End.
This dialogue will be displayed when interacting with the eternal loom, before
the threads offering.
Listen to the global state for doing so. If there is helper state, instantiate
the face of the character according to its seed, and set the background color to
the type of help.
manuq added 8 commits June 1, 2026 20:14
The RetellingManager coordinates the retelling with the EternalLoom, to avoid
cluttering that node. It does so by listening to signals and calling a method.
Node that will be used to make characters become helpers in levels, for
consuming the help obtained during the retelling at the eternal loom. There is
no actual implementation, this help could be spent in imaginative ways and is
left to the level creators. A default dialogue is provided as example.
Change the interaction of the eternal loom when the player is ready to offer the
threads. Now if the quest has retelling dialogue, display it. A retelling may
result in the player obtaining help, that can be used later in the game.

After the retelling, or if no retelling happened, trigger the incorporating
threads cutscene and let the corresponding elder congratulate the player.

Remove the sokoban puzzles, which are going to be repurposed. More cleanup on
this later.
The dialogue lines should be considered placeholders.
Add the following helpers:
- 1_music_puzzle: Memory helper that triggers a dialogue saying "Lore lore lore" as placeholder.
- 2_ink_combat: Spirit helper that fills the hard to reach inkwell.
- 3_stealth_level: Imagination helper that fixes the bridge.
Add the following helpers:
- 1_void_quest: Memory helper that displays a placeholder dialogue.
- 1_void_quest: Spirit helper that slows down the enemy.
- 2_grappling_hook: Imagination helper that makes needles appear in the optional loop.
@manuq manuq force-pushed the townies-system branch from cb1f206 to 446ff47 Compare June 1, 2026 23:15
@manuq
Copy link
Copy Markdown
Collaborator Author

manuq commented Jun 1, 2026

This is now finally ready for review! Things left out of this change:

  • Animations and camera movement when using the help in levels.
  • All the dialogue should be considered placeholder.
  • Centering the camera around the loom when the retelling happens.

Also there is a small bug in the walking animations, when the character that gets closer to the player leaves the loom.

Copy link
Copy Markdown
Member

@wjt wjt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

I think we will want to revisit the logic that hides the thread HUD when you're not on a quest. What happens as of this branch is that you tell the townie a story, they offer their help (and appear in the HUD), but then the threads go to the Loom and then the HUD is hidden.

MEMORY,
IMAGINATION,
SPIRIT,
NONE,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you put NONE at the end because you didn't want to have to update every instance in the game. OK! Perhaps we can later reorder this and update all resources.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other option is to have GameState.global.helper be null when there is no helper. Then you would not need to introduce the NONE member here, which is a potential source of bugs if someone sets NONE on a CollectibleItem.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I couldn't find another way in the prior game state. But now with resources, it looks like I should be able to get rid of the NONE enum and use null on the whole helper state. Thanks! Let me try it.



func _on_helper_state_changed() -> void:
var has_helper := bool(GameState.global.helper.character_seed)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here you're not using the NONE state but the magic 0 value for the seed to indicate "no helper". I think having helper be null would be clearer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh… but it's so that you can connect to the changed signal. OK; but that could also be done by monitoring GameState.global.changed and emitting that when the helper property is assigned to.

Maybe it's clear enough as you have it, don't feel the need to rewrite it all.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me try without the NONE before merging.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wjt I made it work identifying null as the lack of helper, and is much better. I added a new signal GameState.global.helper_changed for monitoring the change, similar to the existing completed_quests_changed. Otherwise listening to GameState.global.changed would trigger for other things.

Sorry that it took me some time. I had to run the whole game again, because my state snapshots for quick testing didn't work anymore.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I took away from a Bippinbits talk (perhaps that you shared?) was the value of a custom "run" button that lets you choose the state of the game to run. That might be useful!

BTW you can also change the GameState of the running game in the inspector quite easily now.

helper_container.visible = has_helper
if has_helper:
var helper_character: CharacterRandomizer = TOWNIE.instantiate()
add_child(helper_character)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems weird at first glance to instantiate a scene, fish out its head, and delete it again. I guess this is because the seed is used for the townie as a whole.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, is only because of that. Do you think this could be done with a separate scene for the head?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having thought about it a bit, I think this is fine – it ensures that the HUD head is the same as the corresponding townie.

You could imagine splitting the townie scene up into a resource for the components + corresponding colours which is input for the scene... but I don't think it's worth it.

Comment on lines +6 to +8
- Learned a forgotten song {{ memory_text() }} => retell_memory
- Slipped past the guards {{ imagination_text() }} => retell_imagination
- Fought off some InkDrinkers {{ spirit_text() }} => retell_spirit
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can iterate on this but I wonder if we should make the options conditional on the thread being available. Something like this:

- Learned a forgotten song [if has_memory()] => retell_memory
- Slipped past the guards [if has_imagination()] => retell_imagination
- Fought off some InkDrinkers [if has_spirit()] => retell_spirit
- Can't really remember => END

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes from the spec by @JoniCeceri, but now I see that's not part of the text I copy/pasted in the issue. The retelling of each option should be available anyways. If the player has the corresponding thread, they receive a reward.

Currently, the 2 quests to which I added retelling will have all 3 threads, so is impossible to test at this point.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I disagree with the spec then 😀 but indeed – impossible to test without making threads optional.

Comment thread scenes/quests/lore_quests/quest_001/3_stealth_level/components/fix_bridge.gd Outdated
Comment on lines +15 to +16
var result := void_spreading_enemy.get_node_and_resource(
"NavigationFollowWalkBehavior:speeds:walk_speed"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fancy!

manuq and others added 2 commits June 2, 2026 08:00
…/fix_bridge.gd

Co-authored-by: Will Thompson <wjt@endlessaccess.org>
And add a helper_changed signal to listen only to that change.
@manuq manuq merged commit 06f2253 into main Jun 2, 2026
7 checks passed
@manuq manuq deleted the townies-system branch June 2, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prototype Townie System

2 participants