Phoenotopia Remake - 3 - Maintenance, Inventory; Switches, Gates, and Bats
2024-11-14Automatic conversion of Tiled objects
Phoenotopia uses the Tiled map editor. Within the level, (almost) each object has a type property. Previously, I used the YATI tiled importer, and specified for each object, the type of object it should be imported as, and the path of the godot scene.


The steps to tell YATI/Tiled that a new type exists are as follows:
- 
Go to ‘View->Custom Types Editor’ in Tiled 
- 
Press ‘Add Class’ 
- 
Type the name e.g. ‘step_switch’ 
- 
Click ‘Add Member’ 
- 
Type godot_node_type, selectstringfrom the dropdown and press ‘Ok’
- 
Click ‘Add Member’ 
- 
Type f, selectfilefrom the dropdown and press ‘Ok’
- 
Double-click on the value field of godot_node_typeand type “instance”
- 
Double-click on the value of f, and select the right file from the file picker
This was becoming quite tedious as for each new object I added(step switches and rail doors, to name a few) I needed to go through all of the above steps. I re-wrote part of my LevelLoader script to create the needed objects themselves. Now, instead of pressing about 8 buttons, and typing various information, I can simply add “step_switch” to the list of IMPLEMENTED_OBJECTS:
## List of object types that are considered 'implemented'
## If a (tiled) object's type is found within this list, 
## the approporiate scene from res://objects/type/type.tscn
## is loaded in place of the rectangle left from YATI
const IMPLEMENTED_OBJECTS = [
    "loot",
    "box",
    "npc",
    "map_cursor",
    "map_point",
    "rail_door",
    "step_switch"
]
-        if !tiledObject.has_meta("godot_node_type") and !tiledObject.has_meta("f"):
+        if type in IMPLEMENTED_OBJECTS:
+            var path = "res://objects/%s/%s.tscn" % [type, type]
+            if FileAccess.file_exists(path):
+                var ob = load(path).instantiate()
+                layer.add_child(ob)
+                ob.global_position = tiledObject.global_position
+                transfer_metadata(tiledObject, ob)
+                tiledObject.queue_free()
+                continue
+            else:
+                push_warning("Object %s not found" % type)
+        
+        elif !tiledObject.has_meta("godot_node_type") and !tiledObject.has_meta("f"):
            var ob = unimplementedObjectScene.instantiate()
            layer.add_child(ob)
            ob.global_position = tiledObject.global_position
Much easier!
Debug save menu
For more convenient testing, I added a button to the debug menu to save the game in any location. This was quite helpful when creating the rail doors, as I didn’t have to load the save and then go through the fast-travel menus to get to the box room, and then push the metal crate to get up the stairs. For those interested, the menu I’m using is over on the tree menu addon I made myself, with details available at al2f/godot-tree-menu.
 
 
Switches and Rail doors
Previous door experience
The last time I tried to make sliding doors was in my other project - umbrella. And after the onslaught of bugs I received when making the doors, I was sure it would be the last. However, here I am, making sliding doors again.
 
 
Some screenshots of sliding doors from umbrella using KinematicBody2Ds for doors. Note how the door can be pushed out of the way.
In godot, there are 3 types of bodies: KinematicBody2D(called CharacterBody2D in Godot 4), RigidBody2D, and StaticBody2D.  StaticBodys are, well… static. They are used for large solid objects that do not move. KinematicBodys  are used for characters and enemies which are move based on code. Finally, RigidBodys are controlled by Godot’s physics engine, and are used for anything that needs to fall under gravity, but does not need any additional logic.
In umbrella, I tried both Kinematic and Static bodies when making doors:
- 
StaticBodys are meant to be static, and changing their position directly can have bad side effects, as they’re not meant to be moveable. Though a StaticBody worked as a door which slid open and closed, it pushed everything out of its way - including boxes and characters. As a result, anything that was under the door while it was closing was teleported into the nearby floor or wall, never to move again.
- 
KinematicBodys can be moved by themove_and_collidefunction, and collisions can be responded to. This means that I can make the door stop moving when it’s closing, and there’s an object under it. However, if you were persistent enough, you could push the door out of the way with a box. I guess that would be consistent with Phoenotopia though. Speedrunners regularly either clip through doors, or push them out of the way!
Step switches
I used Area2Ds and their body_entered and body_exited signals to make the switches. An tip when coding switches like these: Don’t just check for when a body has entered or exited the switch area. Keep track of objects that are pressing the switch. Let’s consider the below example:
- 
A box is pushed onto the switch. The body(box) has entered the switch’s area, and the rail door is told to open. So far, so good. 
- 
 
- 
Gail walks over to the switch, and pushes the box off it. Here, the box has exited the switch’s area, and the door was told to close. 
- 
 
- 
This is not the intended behaviour, however, as the switch should remain pressed. Though the box has moved off the switch, Gail has just entered its area, and Gail should currently be holding it down. 
In umbrella, I used a list to keep track of each body that was holding the switch down, and updated it as objects entered and exited the area. This time around, I used Area2D.get_overlapping_bodies() to see if the switch was colliding with any bodies, which shortened quite a bit of code.
Rail doors
I first tried a KinematicBody, with the same logic as above, but the door got stuck when trying to open if Gail was walking into it. I then tried a StaticBody, and on near auto-pilot, typed the movement code, assuming move_and_collide would work. I was quite surprised when it did, as StaticBody’s were, well… Static, and it didn’t make sense for them to have move_and_collide  function. It seems the behaviour was changed between Godot 3 and Godot 4.
Godot 3:
Static body for 2D physics. A StaticBody2D is a body that is not intended to move. It is ideal for implementing objects in the environment, such as walls or platforms.
Godot 4:
A static 2D physics body. It can’t be moved by external forces or contacts, but can be moved manually by other means such as code, AnimationMixers (with AnimationMixer.callback_mode_process set to AnimationMixer.ANIMATION_CALLBACK_MODE_PROCESS_PHYSICS), and RemoteTransform2D.
Now, with a moving StaticBody, the door could move, stop when something was in the way, and, what’s more important, not be moved by anything other than the code!

To link the switches and doors together, I used their id. In the tiled level, both doors and switches have an id property. In the box room, the switch has an id of 5, and triggers the corresponding rail door with an id of 5:

Another interesting property is the tdlr property. Don’t misread it as td;lr like I did. tdlr stands for top; down; left; right, and is a number from 1 to 4 representing the direction of an object. From what I’ve seen it appears quite a bit in the game. At first, I tried to treat it as rotation, and rotated the door according to its tdlr value:
- 
1->top->0 rotation 
- 
2->down->180 rotation 
- 
3->left->90 rotation 
- 
4->right->270 rotation 
However, this resulted in some pretty broken doors. I’m sure the Panselo Gate is meant to be fully closed here…

I found out when comparing the tdlr values of the Box House rail door, and the Panselo Gate rail door, it doesn’t matter if a door is facing down(tdlr=1) or top(tdlr=2), it will still be closed by default, and will open upwards when a switch is pressed.
The logic for the door was straightforward once it worked.
- 
When the level is loaded, update the rotation based off the tdlrproperty, and ignoretdlrif the door says its upside-down(tdlr=2)
- 
The door is either movingor notmoving.
- 
is_openis set totruewhen the door is currently opening, andfalseif it is currently closing
- 
When the door is moving, it tries to move to its closed(or open) position, stopping if anything is in the way
- 
Once the door reaches its open/closed position, it stops moving
Bat(s)
Equipping the bat
With doors working, Gail can no longer exit Panselo as she pleases. She needs a bat to hit the switch, so that the gate opens. She can already collect a bat, but she has no inventory to equip it from!
I created a nice inventory UI, with buttons for each item. As with most other UI elements, I re-designed them as themes instead of static images, so that the UI could be expanded on later if/when needed.

The inventory itself is a combination of HorizontalBoxContainers, VerticalBoxContainers and GridContainers.  the GridContainers organise the buttons in a grid, and theHBoxs and VBoxs order the 3 grids to make the layout.

I realised a bit later, that organising the inventory like this was a big problem. In the flash game, although the inventory is shown as three separate sections, it is actually stored as three lists, with each number corresponding to an item. This nicely stores all the items:
[
    [A, B, C, D, E, F, G, H, I],
    [J, K, L, M, N, O, P, Q, R],
    [S, T, U, V, W, X, Y, Z, !],
]

Most of the flash logic relies on being able to do things like set item #14. (letter O in the above examples, counting from 0). When stored as 3 lists, this is easy to do, but when tools, items, and key items are stored separately, this becomes a problem, as there is no way to tell which of the below items is ‘item 14’
Tools:     [A, B, C, D, J, K, L, M, S, T, U, V]
Items:     [E, F, G, H, I, N, O, P, Q, R]
Key Items: [W, X, Y, Z, !]
A lot of mathemagic later, and I had a function to convert this ‘item 14’ into ‘slot #6 of the items container’:
const LEFT_WIDTH=4
const RIGHT_WIDTH=5
const WIDTH = LEFT_WIDTH+RIGHT_WIDTH
## Black magic which happens to work
## As the inventory is stored as one list, the separation into 
## key items, items, and tools is purely visual
## Since I made the UI pretty, and used containers, I need a way to get an index
## like 14, and get the appropriate child element(child index 6 of the 'items' container)
func idx_to_item(idx: int):
    var row = idx/WIDTH
    var col = idx%WIDTH
    if col < LEFT_WIDTH:
        var left_child_idx = idx-RIGHT_WIDTH*row
        print(left_child_idx)
        return tools.get_child(left_child_idx)
    elif col >= LEFT_WIDTH:
        var    num = ((idx-LEFT_WIDTH*row)-LEFT_WIDTH)
        if row < 2:
            print(num)
            return items.get_child(num)
        else:
            print(num-RIGHT_WIDTH*2)
            return key_items.get_child(num-RIGHT_WIDTH*2)
With this, I could draw the inventory items (like Nana’s bracelet), however even after picking up the bat, it did not show in the inventory.
 
 
I then went down a rabbit hole of references. Using grep, and a lot of skimming through code to understand what was going on, and chasing variable names only to find I was going in circles.
The save file structure mentioned an equipped_club, which lead me to Gail’s weapon_equipped variable. In many cases, Gail’s weapon_equipped was being set to the value of save.equipped_club, but for a long time, I couldn’t see where either save.equipped_club or weapon_equipped were being set.
It turns out item management is spread over quite a few files:
- 
An Itemsscript contains the inventory descriptions of each item, and the actions that can be performed. It also contains functions for equipping various items:this.item_lookup = { 0:["",""], 1:["",""], 2:["Duri Herb\nA minty plant with slight curative properties. It is commonly used in cooking but can be eaten raw. Restores 7 HP.",null,this.use_button,this._Use7Healer,this.give_button,this._GiveItem,this.discard_button,this._DiscardItem], 3:["Honey Comb\nA block of wax cells carefully constructed by giant bull bees. It\'s dripping with honey! Boosts stamina and running speed for 1 minute.",null,this.use_button,this._Use1MinuteRecharger,this.give_button,this._GiveItem,this.discard_button,this._DiscardItem],private function _EquipPaddle() : void { FP.game_world.sound_g.swap.play(); this.player.tool_monitor.wpn_hud.SetFrame(1); this.player.weapon_equipped = 1; } private function _EquipHammer() : void { FP.game_world.sound_g.swap.play(); this.player.tool_monitor.wpn_hud.SetFrame(2); this.player.weapon_equipped = 2; } private function _EquipMace() : void { FP.game_world.sound_g.swap.play(); this.player.tool_monitor.wpn_hud.SetFrame(3); this.player.weapon_equipped = 3; }
- 
When a lootitem is collected, it calls Gail’sHandlePickupfunction. IfHandlePickupreturnstrue, then the loot is collected, and removed from the level. Otherwise, it stays. An example of whenHandlePickupwould returnfalse, is if the inventory is full, and Gail has no slots in the inventory to put the item into.
- 
HandlePickuphandles some animation logic when Gail picks up and item, and also calls theAddToInventoryfunction
- 
The AddToInventoryfunction finally adds the item to the inventory.
Interestingly, in the loot code, each item is assigned a row and column which specifies where it is in the inventory. Thus, row 4, column 2 (W) is shared by many items, including Nana’s Bracelet, the Golem Head and the Bandit Boss.
So in reality, it is not stored anywhere that you have Nana’s bracelet, the Golem Head, and Bandit Boss at the same time. As each newer(or older) item over-writes the previous. Also, many places in the code check if Gail has an item in a specific inventory slot, instead of checking the whole inventory, and would break if these items would move:
this.charge_max = this.items.ContainsAt(60,6,2) ? 20 : 50;
this.has_phoenix_ring = this.items.ContainsAt(61,5,2);
this.is_lucky = this.items.ContainsAt(59,7,2);
Swinging the bat
Then, finally time to make the bat swing, and, of course, hit. I already had the animations imported into an AnimatedSprite2D, however I still needed to make the bat’s hitbox appear at the right times. The flash just checked if the current frame number was one of the attacking frames:
private function _AttackingState() : void
{
    this._HandleGravity();
    this._MovementFriction();
    GameyMoveByP(this.vx,this.vy);
    if(this.turbo_attack > 0)
    {
        this.attack_charge = this.charge_max;
    }
    if(sprite.frame == 27 || sprite.frame == 33 || sprite.frame == 100 || sprite.frame == 106 || sprite.frame == 118 || sprite.frame == 124)
    {
 
  
 
I didn’t really like the idea of running code every time the sprite’s frame changes to see if we’re meant to show the hitbox. I converted the existing animations to use an AnimationPlayer instead of an animated sprite. This meant instead of the frames being stored as a list, they were stored as keys in the animation track. With each key having the value of the frame number, and the time during which it appears.

Gail’s frames stored in an AnimatedSprite2D.

Gail’s frames stored in an AnimationPlayer.
Importantly, using the AnimationPlayer meant I could add a `function call’ track, which calls a function whenever the animation reaches a certain point:

Inside Gail’s code, I then position the hitbox:
func bat_hitbox_show():
    weaponTrail.show()
    var charged = false;
    if charged:
        batbox.size = Vector2(35,8)
        batbox.damage_amount = 2*(5+equipped_weapon*5)
        if direction.x > 0:
            pass
        else:
            pass
    else:
        batbox.size = Vector2(27,8)/2
        batbox.damage_amount = (5+equipped_weapon*5)
        if direction.x > 0:
            batbox.position.x = (25.0)/2
            batbox.position.y = (15-11)/2
        else:
            batbox.position.x = (-25.0)/2
            batbox.position.y = (15 -11)/2
        batbox.start(0.1)
Here’s what it looks like in-game:

Hitting the boxes.
The interactions between the bat and boxes were done using two Area2Ds - ones called Hitboxes, and the other called Hurtboxes. In addition to each object having a collision shape, it has an area called a Hurtbox for taking damage, and an area called a Hitbox to give damage. The hitboxes have a set of general properties which describe the kind of damage it gives, and whenver a hitbox enters the hurtbox area, the thing that’s getting hit can decide how it wants to react to that damage.
The hitboxes have information like the damage_amount, damage_type, and frame_count. damage_type I have not used anywhere yet, as I do not know what types there are. It is just for compatibility with the flash game’s logic when I get around to it. frame_count is the number of frames the hitbox should stay active before deactivating. Relying on frame counters when the game could have a lower framerate, or lag spikes is a bad idea, so I multiply this number by 1/60 later, to get the number of seconds.
The box’s damage function is a lengthy set of checks on how the box should respond to damage depending on which type it is (small wooden crate, large wooden crate, large metal crate, etc.)
Also, this box hangs in midair after I destroy the bottom one. If anyone knows how I can update the top box to make it fall, please let me know.

State Machine
Lastly, I needed Gail to stand still when she was swinging the bat while running, and I realised didn’t know where in my code to add that.
Though I had different state like running, standing, and rising, the various logic was quite tightly mixed with everything else, and it was quite hard to tell at a glance how Gail would behave in any state.
if state == States.RISING:
        if velocity.y > 0:
            self.state = States.FALLING
    if state == States.FALLING:
        if is_on_floor():
            particles_on_land()
            self.state = States.STANDING
            velocity.y = 0
            apply_gravity_when_rising = false
    if state in [States.STANDING, States.RUNNING]:
        if was_on_floor and !is_on_floor():
            self.state = States.FALLING
        if state == States.STANDING:
            if abs(velocity.x) > 20:
                self.state = States.RUNNING
        elif state == States.RUNNING:
            if abs(velocity.x) < 20:
                self.state = States.STANDING
match state:
        States.RISING:
            if apply_gravity_when_rising:
                _HandleGravity()
            if is_on_ceiling():
                velocity.y = _GRAV
            _HandleLeftRightMovement()
        States.FALLING:
            _HandleGravity()
            _HandleLeftRightMovement()
        States.STANDING:
            _HandleGravity()
            _HandleLeftRightMovement()
        States.RUNNING:
            _HandleGravity()
            _HandleLeftRightMovement()
Splitting each state into its own file, and stealing writing a state machine to handle the transitions from one state to another, the code was altogether more readable, and easier to work with.
Now, the interactions between the running and standing states look like this:
Standing:
- When entering this state:
- If the batvariable is passed astrue, immediately start swinging. While swinging,Left/Rightkeys are ignored
- If the batvariable is not passed, play thestandingorholdStandanimation, depending on whether or not we’re holding a box
 
- If the 
- Physics
- If we’re moving more than 20px/s, move to the runningstate
 
- If we’re moving more than 20px/s, move to the 
- Input
- If Xis pressed, handle box pickup/throwing
- Swing the bat if Xis pressed
- Check for interactions with doors/NPCs when the Up/Xbuttons are pressed
- Jump if Zis pressed
- Look for ladders behind us when Upis pressed
 
- If 
Running:
- When entering this state:
- Play the runningorholdRunanimation depending on whether or not we’re holding a box
 
- Play the 
- Physics
- If we walk off the floor, and there is air below us, move to the fallingstate
- If we’re moving less than 20px/s, move to the standingstate
 
- If we walk off the floor, and there is air below us, move to the 
- Input
- If Xis pressed, handle box pickup/throwing
- If Xis pressed, move to thestandingstate, passingbatastrue
- Check for interactions with doors/NPCs when the Up/Xbuttons are pressed
- Jump if Zis pressed
 
- If 

 
  
 
  