GearHead Caramel Scenario Creator

A picture of the GearHead Caramel scenario generator.

It’ll be easy, I thought. Just throw some widgets together to link boilerplate code and you’ll be finished in a week. I really need to stop listening to my stupid brain.

Anyhow, I’ve been working on a GearHead Caramel scenario editor! In theory, this should allow players to create scenarios for GearHead Caramel without knowing a lick of Python. It should also allow player-created scenarios to be distributed without the intrinsic security problems of distributing Python scripts. The next release of GearHead Caramel (hopefully soon…) will have an early alpha version of this editor. Everything seems to be working, and it is possible to create simple scenarios in it. The big obstacle for now is the interface, which just lists every adventure component in an unsorted tree view and is therefore not very user friendly.

As soon as the scenario I’m currently building via the editor is somewhat playable, I plan to make a release. If you want to try the editor before then you can download the source from GitHub and try from there.

GearHead Caramel Cinematic Trailer

GearHead Caramel now has a cinematic trailer, created by the mega-talented cartoonist and animator Matt Sandbrook.

GearHead Caramel v0.620: Content and Hotkeys

GearHead Caramel v0.620 has just been released. There are a lot of changes this time around- as you’d expect from the length of time it took me to release this version. Every town in DeadZone Drifter now has a special feature. The opening scenario for DeadZone Drifter may be different depending on your character’s life history. There’s a new pilot suit for all body types by LordErin. You can now set combat hotkeys for any weapon or action. Finally, some bugs were fixed and some improvements were made. Try it out and let me know what you think.

You can download the game from itch.io or GitHub.

GearHead: Caramel Coming to Steam, Possibly Relicensing GH1 + GH2

A picture of the GearHead Caramel character creation screen.

I have finished the paperwork and am preparing to release GearHead Caramel on Steam. I think I have everything set up correctly to compile the Linux binaries for SteamOS; we’ll see how it goes.

As I mentioned earlier, I would also like to bring GearHead1 and GearHead2 to Steam. To do this I’d need to change their license from LGPL to some other open source license, probably Apache 2.0 (which is what GearHead Caramel uses). This change would only affect future releases; existing versions will remain under LGPL.

Before changing the license I want to make sure that no contributors have any objections. Unfortunately, contacting contributors from 20 years ago and even figuring out who contributed what is proving to be difficult. So, if you have contributed to the GearHead code base and have any opinions about the license change, please let me know.

Modding GearHead Caramel: Create Your Own Adventure, Part Two

The Frigate Up repair shop in Namok.

In part one of the tutorial, we created an empty city scene and stuck an exit in it. In this part we will add a building, a shopkeeper, and a shop. You can download the expanded script file from Dropbox.

Now, we could stick all of this new content into the same Plot we used last time. However, I’ve found that it’s usually better to keep things in smaller, more manageable units. So we’re going to add a new Plot for the shop we’re going to add.

class FrigateUpRepairShop(Plot):
    LABEL = "POTES_REPAIR_SHOP"
    active = True
    scope = "LOCALE"

    def custom_init(self, nart):
        building = self.register_element("_EXTERIOR", game.content.ghterrain.ScrapIronBuilding(
            waypoints={"DOOR": ghwaypoints.GlassDoor(name="Frigate Up")},
            door_sign=(game.content.ghterrain.FixitShopSignEast, game.content.ghterrain.FixitShopSignSouth),
            tags=[pbge.randmaps.CITY_GRID_ROAD_OVERLAP]), dident="METROSCENE")

        team1 = teams.Team(name="Player Team")
        team2 = teams.Team(name="Civilian Team")
        intscene = gears.GearHeadScene(35, 35, "Wujung Tires", player_team=team1, civilian_team=team2,
                                       attributes=(gears.tags.SCENE_PUBLIC, gears.tags.SCENE_BUILDING, gears.tags.SCENE_SHOP),
                                       scale=gears.scale.HumanScale)

        intscenegen = pbge.randmaps.SceneGenerator(intscene, gharchitecture.ScrapIronWorkshop(),
                                                   decorate=gharchitecture.FactoryDecor())

        self.register_scene(nart, intscene, intscenegen, ident="LOCALE", dident="METROSCENE")

        foyer = self.register_element('_introom', pbge.randmaps.rooms.ClosedRoom(anchor=pbge.randmaps.anchors.south),
                                      dident="LOCALE")
        foyer.contents.append(ghwaypoints.MechEngTerminal())

        backroom = self.register_element('_backroom',
                                         pbge.randmaps.rooms.ClosedRoom(decorate=gharchitecture.StorageRoomDecor()),
                                         dident="LOCALE")

        game.content.plotutility.TownBuildingConnection(
            self, self.elements["METROSCENE"], intscene,
            room1=building, room2=foyer, door1=building.waypoints["DOOR"], move_door1=False
        )

        npc = self.register_element("SHOPKEEPER",
                                    gears.selector.random_character(50,
                                                                    local_tags=self.elements["METROSCENE"].attributes,
                                                                    job=gears.jobs.ALL_JOBS["Shopkeeper"]),
                                    dident="_introom")

        self.shop = services.Shop(services.MEXTRA_STORE, npc=npc, rank=50)

        return True

    def SHOPKEEPER_offers(self, camp):
        mylist = list()

        mylist.append(Offer("[OPENSHOP]",
                            context=ContextTag([context.OPEN_SHOP]), effect=self.shop,
                            data={"shop_name": "Frigate Up", "wares": "refurbished salvage"}
                            ))

        mylist.append(Offer("[HELLO] Frigate Up is the place you come when you've done just that.",
                            context=ContextTag([context.HELLO]),
                            ))

        return mylist

The attributes defined at the top of the new Plot class work the same as last time. The label needs to be unique for a given plot type, so for plots associated with “Pirates of the East Sea” I’m going to add a “POTES_” prefix. The scope of this Plot is set to “LOCALE”, which means that the scripts of this plot will only be called when the player is in the scene. I’ll talk about that a bit more when we get to the scripts.

The custom_init method is where we add things to the campaign world and set up for the adventure. The Plot we wrote last time created the city map and registered it under the element name “METROSCENE”. This new plot, which is going to be a child of the previous plot, will inherit all of these elements except the ones that begin with an underscore (_).

The first thing we add is a building to the city scene. The “dident” parameter of register_element is the Destination Identifier; we can use this parameter to place the element we’re registering inside of a previously defined element, in this case “METROSCENE”.

        building = self.register_element("_EXTERIOR", game.content.ghterrain.ScrapIronBuilding(
            waypoints={"DOOR": ghwaypoints.GlassDoor(name="Frigate Up")},
            door_sign=(game.content.ghterrain.FixitShopSignEast, game.content.ghterrain.FixitShopSignSouth),
            tags=[pbge.randmaps.CITY_GRID_ROAD_OVERLAP]), dident="METROSCENE")

That’s the building exterior taken care of. For the interior, we need to define a new scene. This works exactly the same as the city scene in the first tutorial.

        # Add the interior scene. Once again we have to define the teams, define the scene itself, and give it a
        # scene generator. Everything gets added to the campaign world using the register_scene method.
        team1 = teams.Team(name="Player Team")
        team2 = teams.Team(name="Civilian Team")
        intscene = gears.GearHeadScene(35, 35, "Frigate Up", player_team=team1, civilian_team=team2,
                                       attributes=(gears.tags.SCENE_PUBLIC, gears.tags.SCENE_BUILDING, gears.tags.SCENE_SHOP),
                                       scale=gears.scale.HumanScale)

        intscenegen = pbge.randmaps.SceneGenerator(intscene, gharchitecture.ScrapIronWorkshop(),
                                                   decorate=gharchitecture.FactoryDecor())

        # It's a convention for plots that add a scene to the adventure to label their primary scene "LOCALE".
        # This is not a rule, but it can be useful. Subplots which add things to a scene will usually use "LOCALE"
        # as the scene ID to add things to.
        self.register_scene(nart, intscene, intscenegen, ident="LOCALE", dident="METROSCENE")

Frigate Up is one of the repair shops in Namok in GearHead Arena. I’ve always liked that name, so that’s the repair shop we’re adding to our adventure. Our building is going to need at least one room, and while we’re at it we might as well add a second one.

        foyer = self.register_element('_introom', pbge.randmaps.rooms.ClosedRoom(anchor=pbge.randmaps.anchors.south),
                                      dident="LOCALE")

        # Let's add a back room for absolutely no reason.
        backroom = self.register_element('_backroom',
                                         pbge.randmaps.rooms.ClosedRoom(decorate=gharchitecture.StorageRoomDecor()),
                                         dident="LOCALE")

The two rooms will be automatically connected by the map generator. However, we’re going to have to connect the interior scene to the city exterior by ourselves. Fortunately the plotutility module provides some easy ways to do so.

        game.content.plotutility.TownBuildingConnection(
            self, self.elements["METROSCENE"], intscene,
            room1=building, room2=foyer, door1=building.waypoints["DOOR"], move_door1=False

The parameters here are the plot adding the connection (self), then the two scenes being connected. TownBuildingConnection could build a connection with just this information, but we want to specify a few more details. The exterior room is the ScrapIronBuilding and the interior room is the foyer. The exterior door is the door we added to the ScrapIronBuilding, and it should not be removed from where it is.

Mostly empty building

So now we have an exterior building, an interior scene, and we can move freely between the two. Time to add some content to the interior.

npc = self.register_element("SHOPKEEPER",
                                    gears.selector.random_character(50,
                                                                    local_tags=self.elements["METROSCENE"].attributes,
                                                                    job=gears.jobs.ALL_JOBS["Shopkeeper"]),
                                    dident="_introom")

        # Create the Shop service, and link it to this NPC.
        self.shop = services.Shop(services.MEXTRA_STORE, npc=npc, rank=50)

The shopkeeper is a random NPC with location tags taken from the city scene and job set to Shopkeeper. They will be placed in the foyer room.

The shop is an object of type services.Shop. This is a callable object, and so we can open the shop any time we like by calling self.shop(camp). In this case, we want the shop to be called from dialogue with the PC. So, we define a method that gives the shopkeeper some dialogue.

GearHead Caramel dialogue does not work like the dialogue in previous games. In fact, I’m not sure that any other game has ever used this method… but it’s a very useful method for a game that features procedural storytelling.

The terminology for dialogue in GHC is lifted directly from improv theatre, so if you’ve ever watched Whose Line Is It Anyway you are well on your way to understanding how it works. Instead of constructing a dialogue tree, you just have to supply a list of NPC Offers. An Offer is something that might be said by the NPC under certain circumstances. The player is then given a choice of several Replies to that Offer; each Reply leads to another of the NPC’s Offers, in GHC’s version of the Yes, And principle.

What this means is that an NPC’s dialogue can be constructed from multiple different sources, none of which need to know about each other, and you will still get a coherent conversation. The grammar that links Offers to Replies is found in the game.ghdialogue.ghreplies module.

The method that defines dialogue Offers for our shopkeeper is SHOPKEEPER_offers. Because the scope of this plot was set to LOCALE, these offers will only get added if the current scene is LOCALE. The first Offer in a friendly conversation will always have context HELLO; if the NPC doesn’t have a HELLO Offer defined, they can use a generic HELLO Offer. This Plot provides two Offers for our shopkeeper: one which opens the shop, and a HELLO Offer which gives the Frigate Up sales pitch.

    def SHOPKEEPER_offers(self, camp):
        mylist = list()

        mylist.append(Offer("[OPENSHOP]",
                            context=ContextTag([context.OPEN_SHOP]), effect=self.shop,
                            data={"shop_name": "Frigate Up", "wares": "refurbished salvage"}
                            ))

        # I am going to give the shopkeeper one more offer so they can give the Frigate Up sales pitch.
        mylist.append(Offer("[HELLO] Frigate Up is the place you come when you've done just that.",
                            context=ContextTag([context.HELLO]),
                            ))

        return mylist

Some things about the first Offer. The NPC’s dialogue is “[OPENSHOP]”; this is a grammar tag that will be expanded to a randomly generated line each time the shop is entered. This grammar tag needs some extra data, so the shop name and a brief description of its wares are included in the Offer. The effect is set to self.shop; this effect will be called when this Offer is given.

The second offer also uses a grammar tag, “[HELLO]”. You can see all of the standard grammar tags in the game.ghdialogue.ghgrammar module. It’s also possible for Plots to define custom grammar tags.

One last thing before our village has a fully functional shop; the FrigateUpRepairShop plot has to be added by the EastSeaPiratesScenario plot. This is accomplished by the following line in the EastSeaPiratesScenario custom_init method:

self.add_sub_plot(nart, "POTES_REPAIR_SHOP")

Download the script file from Dropbox, unzip it to the content folder, and play around with it. You can add different shops by copying and pasting the new Plot, then changing the parts you want. Let me know if you have any questions.

GearHead Discord

Once upon a time, in the distant past, there was a GearHead forum. Now, in the sparkling future, there is a GearHead Discord. You can use that link to join and talk about GearHead, mecha/games more generally, or just hang out. This is my first time starting a Discord and I have no idea what I’m doing.

Modding GearHead Caramel: Create Your Own Adventure, Part One

Contents of the ghcaramel user directory.

One of the great new features of GearHead Caramel is the ability to add your own content that will automatically be loaded and used by the game. If you check your ghcaramel user folder (which should be located in your home directory) you’ll see a bunch of folders for content, designs, and images. Today I’m going to show you how to create your own adventure module.

The content for GearHead Caramel is written in Python, as is the entire game, so it would be helpful to know a bit about the language. It would also be helpful to download the GearHead Caramel source code so you can reference all the tools available and see how the current adventure modules do things. Step one is to create a new Python file in the content folder; I’m naming my new adventure “pirates_of_east_sea.py”.

Source code for Pirates of the East Sea,

You can download this file from Dropbox. Just place it in your content folder and it will be loaded when you start GearHead Caramel. I’ll go through the source line by line and explain what each section does. First, we have the import section:

import gears
from pbge.plots import Plot, Adventure, NarrativeRequest, PlotState
import pbge
from pbge.dialogue import Offer,ContextTag
from game import teams,ghdialogue
from game.ghdialogue import context
import pygame
import random
from game.content.ghwaypoints import Exit
from game.content.plotutility import AdventureModuleData
import game
from game.content import plotutility

This part just loads the Python modules and objects that are going to be used in this adventure. You have full access to all the bits of GearHead Caramel, as well as external libraries like PyGame. You don’t need to know what any of this does right now, but in case you are curious: pbge is the “Polar Bear Game Engine” package; it contains a lot of utilities and base classes for making an isometric RPG. gears is the package that defines the GearHead construction and combat rules. game is the package that contains the user interface bits and adventure content. After it is loaded, the module we’re writing now will be placed under game.content.ghplots.pirates_of_east_sea. The module name is determined by the file name, so if you change the filename to something else the module name will also change.

class EastSeaPiratesScenario( Plot ):
    LABEL = "SCENARIO_PIRATES_OF_THE_EAST_SEA"
    active = True
    scope = True

Next, we define a class for our new adventure. All of the narrative content in GearHead Caramel is contained in Plot classes; EastSeaPiratesScenario is defined as a subclass of Plot, so it inherits all of the regular Plot methods + properties and is able to define its own methods and properties.

The LABEL property is used to locale Plots of a given type. Since there is only one Plot of this type, we want the LABEL to be unique.

The active property determines whether or not this plot this plot is currently active. Seems obvious now that I’ve typed it out. Anyhow, Plots can be turned on or off like a light switch.

The scope property tells the game where this Plot’s methods might get called. If True, this means that the Plot has global scope and can be called from anywhere in the campaign world.

For now, it’s okay to just think of LABEL, active, and scope as magic. That is to say, you can use the words but you don’t need to understand them. We’ll come back to these words later on when we do need to use them.

    ADVENTURE_MODULE_DATA = AdventureModuleData(
        "Pirates of the East Sea",
        "Go fight pirates in Namok.",
        (158, 5, 10), None,
    )

Next we define another property for our EastSeaPiratesScenario class: ADVENTURE_MODULE_DATA. This property marks this Plot as the base of an adventure module, and provides some data that gets passed to the scenario chooser when you start a new campaign.

The AdventureModuleData class takes four parameters: a name, a description, the date of this adventure (in year, month, day format), and the name of an image file to use in the scenario menu. In this case I don’t have an image for this adventure so I just pass None. If you want to add one, the scenario menu image should be a 280px x 500px png (the same aspect ratio as a VHS cassette). You can place it in the ghcaramel/image folder.

    def custom_init( self, nart ):
        self.ADVENTURE_MODULE_DATA.apply(nart.camp)

        # Create a city scene.
        team1 = teams.Team(name="Player Team")
        team2 = teams.Team(name="Civilian Team", allies=(team1,))
        myscene = gears.GearHeadScene(50, 50, "Namok City", player_team=team1, civilian_team=team2,
                                      scale=gears.scale.HumanScale, is_metro=True,
                                      faction=gears.factions.TerranFederation,
                                      attributes=(
                                      gears.personality.GreenZone, gears.tags.City, gears.tags.SCENE_PUBLIC),
                                      exploration_music='airtone_-_reCreation.ogg')

        # Create a scene generator
        myscenegen = pbge.randmaps.CityGridGenerator(myscene, game.content.gharchitecture.HumanScaleGreenzone(),
                                                     road_terrain=game.content.ghterrain.Flagstone)

        # Register the city scene and the metro data
        self.register_scene(nart, myscene, myscenegen, ident="METROSCENE")
        self.register_element("METRO", myscene.metrodat)

Next, we define a custom_init method (method is just a fancy name for a function attached to a class). This is the method that gets called when a Plot is initialized; we will use this to create all the scenes and other things we need this plot to create.

The first thing we do is apply the ADVENTURE_MODULE_DATA to the campaign. Just take my word on it for now.

Next, we create a city scene. We need two teams to begin with: one for the player character and another for the city’s civilians. This will act as a default team when characters are placed in the city.

The GearHeadScene class takes a whole lot of parameters. First it needs a width and height (50×50), then a name, the teams, the scale… is_metro=True marks this scene as the base for a metropolis. Then the city faction is defined, it’s given some attributes, and the exploration music is set.

The city also needs a map generator, and that’s defined next. You can look through pbge.randmaps and game.content.gharchitecture to see various types of map generators and options.

Finally, the scene object gets joined to the map generator object using the register_scene method. This method also assigns the scene an element name, “METROSCENE”. We will learn more about elements next time. The metrodat property of the scene gets registered under the element name “METRO”.

       # Create the entry/exit point.
        myroom = self.register_element("_ENTRY_ROOM", pbge.randmaps.rooms.Room(3, 3, anchor=pbge.randmaps.anchors.south),
                                        dident="METROSCENE")
        mygate = self.register_element("MISSION_GATE", Exit(
            name="To The Docks", desc="You stand before the Namok waterfront. The southern docks are used for terrestrial shipping.",
            anchor=pbge.randmaps.anchors.middle,plot_locked=True
        ), dident="_ENTRY_ROOM")


        # Record the scene/entrance in the campaign.
        nart.camp.home_base = (myscene, mygate)
        nart.camp.scene = myscene
        nart.camp.entrance = mygate

        return True

We need a place for the player character to enter this scene. So, next we add a room to “METROSCENE” and add an Exit waypoint to that room.

The campaign object needs to know about our scene and waypoint, so we manually set the scene and entrance of the campaign to the objects we just created. The home_base property of the campaign tells where to go if the party gets entirely wiped out. We set this to our newly created scene and entrance as well.

The custom_init method returns True to let the plot initializer know that everything went according to plan. We have now created an empty city scene with an entrance point, but there are a few more methods to define before it’s completely usable.

    def METROSCENE_ENTER(self, camp):
        # Deal with recovery of incapacitated/dead lancemates+mecha
        etlr = plotutility.EnterTownLanceRecovery(camp, self.elements["METROSCENE"], self.elements["METRO"])
        
    def MISSION_GATE_menu(self, camp, thingmenu):
        thingmenu.add_item("Exit this adventure.", self._end_adventure)

    def _end_adventure(self, camp):
        camp.eject()

The METROSCENE_ENTER method is called whenever the player enters the scene “METROSCENE”. Scenarios in GearHead Caramel have to deal with dead/incapacitated lancemates manually. Fortunately, there’s an EnterTownLanceRecovery class in plotutility that should be appropriate for most situations.

The MISSION_GATE_menu method adds a menu item to the Exit waypoint we created above. In this case, the added menu item calls the _end_adventure method if selected.

And that’s it! We now have a fully functional (if somewhat boring) adventure module. You can run GearHead Caramel and it will be waiting for you in the scenario menu.

GearHead Caramel scenario menu with "Pirates of the East Sea" scenario present.

There’s nothing but an empty city scene, and nothing to do but exit the scenario, but it works. You can use this py file as a starting point for creating your own adventures. In the next tutorial, we’ll add some actual content.

An empty city map with a lone bored cavalier wandering around.

GearHead Caramel v0.612

CNA-15 Century mecha from GearHead.

I’ve just uploaded GearHead Caramel v0.612. This is just a quick bugfix release- I accidentally broke GH1 character import in v0.611. Also, I added the CNA-15 Century mecha from GH1 and GH2.

You can download it from GitHub, Patreon, or buy it from itch.io. If you are using the itch.io app your install of GearHead Caramel should automatically update… I hope. This is my first time using the itch.io Butler system.

GearHead Caramel on itch.io

Title screen of GearHead Caramel showing a dielancer mecha.

I’ve just uploaded GearHead Caramel to itch.io. Its price there is $10, but you will still be able to download the game from GitHub for free. Think of it as a throwback to 1990s shareware. I hope that if you enjoy the game you will consider buying it to support development.

If all goes well, later on I would like to upload all three GearHead games to Steam. I plan to use the 3D models I’m currently making to fill in all the missing mecha sprites from GH1 and GH2.

GearHead Caramel v0.611

I’ve just uploaded GearHead Caramel v0.611 to GitHub. You can download the binaries from there or from Patreon.