Skip to content

Rework how projectiles are customised#1890

Draft
wjt wants to merge 3 commits intomainfrom
wjt/customise-projectile-via-scene-only
Draft

Rework how projectiles are customised#1890
wjt wants to merge 3 commits intomainfrom
wjt/customise-projectile-via-scene-only

Conversation

@wjt
Copy link
Member

@wjt wjt commented Feb 6, 2026

Rework how projectiles are customised

Previously, all instances of the "fill game" used the same Projectile
scene. To customise its appearance, the ThrowingEnemy scene had a whole
series of properties that would be assigned each instantiated
projectile, in addition to assigning a label & optional modulate color
based on the target barrel.

This is not a very "Godot-ish" way to do it. Conceptually,
different projectiles should be different scenes that have similar
structures and reuse common scripts. Instead, we have had to manually
re-export properties from deep inside the projectile scene, and
propagate their values from the ThrowingEnemy scene.

Since commit 402f2ab
("ThrowingEnemy: Export the projectile scene") it has been possible to
customise which projectile scene is instantiated by any specific enemy,
but this isn't enough either for Stella or for Champ. In Stella, we
wanted the invisible enemies to throw, each time, a random choice
between a starfish (which matches the ship "barrel") or one of three
fish (which don't). But this wasn't possible. Similarly in Champ we want
the three different colours of fish to potentially be different sprites;
they currently happen to be recoloured versions of the same asset that
could be done with the projectile modulate logic but we don't want to
limit contestants to this.

So:

  1. For each StoryQuest that previously reused the generic projectile
    scene, create one (or more!) scenes specific to the quest. This does
    introduce some duplication but I decided that the scenes are small
    enough that using inherited scenes is more trouble than it's worth.

  2. Similarly, make a dedicated ink blob projectile scene for the core
    game.

  3. Move the generic projectile scene to the NO_EDIT template, so that it
    will be duplicated when creating a new StoryQuest.

  4. Remove the sprite_frames and hit_sound_stream properties from
    projectile.gd: these can be set by modifying the scene.

  5. Remove the projectile_sprite_frames, projectile_hit_sound_stream,
    projectile_small_fx_scene, projectile_big_fx_scene, and
    projectile_trail_fx_scene properties from throwing_enemy.gd: these
    can be set by modifying the projectile scene that it spawns.

  6. Add the ability to spawn different projectile scenes based on the
    target label. This is adapted from similar code in the Champ quest to
    use different sprite frames per label. Use this in Champ and Stella.
    In both cases define a custom throwing enemy scene, this time as an
    inherited scene: there are huge animations in the enemy scene so in
    this case I thought this was worth it.

  7. Break the projectile-spawning code up a little. Having done this,
    simplify some of the StoryQuest-specific projectile customisations.

Resolves #1873
Resolves #1874

wjt added 2 commits February 4, 2026 17:09
Previously it had the NO_EDIT sprite frames assigned on the root node,
but the ink blob one on the AnimatedSprite2D. At runtime, the former
overwrote the latter. This is confusing. Set the same on both.

Previously, it had a sound effect assigned on the AudioStreamPlayer2D
but not on the root node, and again the root node's null stream would be
set on the AudioStreamPlayer2D at runtime. Clear it.
We have found that this pattern can cause code to run in an unexpected
order. We now prefer to early-return from the setter, and call it again
explicitly from `_ready()`.
@github-actions
Copy link

github-actions bot commented Feb 6, 2026

Play this branch at https://play.threadbare.game/branches/endlessm/wjt/customise-projectile-via-scene-only.

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

Previously, all instances of the "fill game" used the same Projectile
scene. To customise its appearance, the ThrowingEnemy scene had a whole
series of properties that would be assigned each instantiated
projectile, in addition to assigning a label & optional modulate color
based on the target barrel.

This is not a very "Godot-ish" way to do it. Conceptually,
different projectiles should be different scenes that have similar
structures and reuse common scripts. Instead, we have had to manually
re-export properties from deep inside the projectile scene, and
propagate their values from the ThrowingEnemy scene.

Since commit 402f2ab
("ThrowingEnemy: Export the projectile scene") it has been possible to
customise which projectile scene is instantiated by any specific enemy,
but this isn't enough either for Stella or for Champ. In Stella, we
wanted the invisible enemies to throw, each time, a random choice
between a starfish (which matches the ship "barrel") or one of three
fish (which don't). But this wasn't possible. Similarly in Champ we want
the three different colours of fish to potentially be different sprites;
they currently happen to be recoloured versions of the same asset that
could be done with the projectile modulate logic but we don't want to
limit contestants to this.

So:

1. For each StoryQuest that previously reused the generic projectile
   scene, create one (or more!) scenes specific to the quest. This does
   introduce some duplication but I decided that the scenes are small
   enough that using inherited scenes is more trouble than it's worth.

2. Similarly, make a dedicated ink blob projectile scene for the core
   game.

3. Move the generic projectile scene to the NO_EDIT template, so that it
   will be duplicated when creating a new StoryQuest.

4. Remove the sprite_frames and hit_sound_stream properties from
   projectile.gd: these can be set by modifying the scene.

5. Remove the projectile_sprite_frames, projectile_hit_sound_stream,
   projectile_small_fx_scene, projectile_big_fx_scene, and
   projectile_trail_fx_scene properties from throwing_enemy.gd: these
   can be set by modifying the projectile scene that it spawns.

6. Add the ability to spawn different projectile scenes based on the
   target label. This is adapted from similar code in the Champ quest to
   use different sprite frames per label. Use this in Champ and Stella.
   In both cases define a custom throwing enemy scene, this time as an
   inherited scene: there are huge animations in the enemy scene so in
   this case I thought this was worth it.

7. Break the projectile-spawning code up a little. Having done this,
   simplify some of the StoryQuest-specific projectile customisations.

Resolves #1873
Resolves #1874
@wjt wjt force-pushed the wjt/customise-projectile-via-scene-only branch from a6ef16a to 7550a4f Compare February 6, 2026 19:06
## When launching a projectile, if the chosen label is found in this dictionary,
## the associated scene will be used. If not, the default [member projectile_scene]
## will be used.
@export var projectile_scene_for_label: Dictionary[String, PackedScene]:
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a "colo(u)r of bikeshed" situation but maybe this should be projectile_scene_per_label. I want projectile_scene to be at the start of the property name because all properties in the Projectile group must begin with projectile_ for their names to be shortened in the inspector (#1869).

Comment on lines -28 to -38
script/source = "class_name ChampFillGameLogic
extends FillGameLogic
#
#@export var lab: String = \"\"
#
#func _ready() -> void:
#var color_per_label = {\"a\": Color(.3,.5,6), \"b\": Color(.9,.1,6),\"c\": Color(.1,.8,.2)}
#for enemy: ThrowingEnemy in get_tree().get_nodes_in_group(\"throwing_enemy\"):
#enemy.allowed_labels = allowed_labels
#enemy.color_per_label = color_per_label
"
Copy link
Member Author

Choose a reason for hiding this comment

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

This embedded script doesn't do anything so I removed it in passing.

editor_draw_limits = true

[node name="ThrowingEnemies" type="Node2D" parent="OnTheGround" unique_id=1014610845]
visible = false
Copy link
Member Author

Choose a reason for hiding this comment

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

I moved this to the AnimatedSprite2D inside the enemy scene.

else:
# Default to original projectile sprite frames
projectile.sprite_frames = preload("uid://b00dcfe4dtvkh")
if projectile.label in color_per_label:
Copy link
Member Author

Choose a reason for hiding this comment

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

@felixwalberg I like this design. I moved the spirit of this into throwing_enemy.gd - but selecting a per-label scene not SpriteFrames.

Copy link
Member Author

Choose a reason for hiding this comment

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

To do:

  1. Resave every scene that references this scene to update the path
  2. Test that this this actually get duplicated & relinked correctly by the tool - I assume so but actually this will be the first PackedScene property.

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.

Fill game: Allow varying projectile (appearance) by label Change throwing enemy projectile customisation

1 participant