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.

Modding GearHead Caramel: How to Make Portrait Bits

Picture shows a head and arms in the Gimp image editor.

Now that GearHead Caramel is in a properly playable state, I thought I should start explaining how it works. Unlike previous GearHead games, Caramel uses a portrait generator that assembles characters from component parts. Each portrait bit can be anchored to a particular spot on the portrait, can contain multiple images, and each of those images can be placed at different depths. Also, while the portrait is being constructed, a matching 64×64 pixel avatar is also built.

That’s a lot of information to dump at once, so let’s look at a specific example: por_hair_halftail.png.

A hairstyle sprite from GearHead Caramel.

This hairstyle comes in three parts. The first goes on top of the head, in front of any facial features. The second goes on top of the head but behind the ear. The third goes behind the head and the body. You will also notice that the hair is colored pure green (#000100 to #00FF00) and placed on a pure blue (#0000FF) background. Assembled, the hair looks like this:

Let’s start with the color. Most GearHead sprites come with five color channels: pure red, pure yellow, pure green, pure cyan, and pure magenta. For characters, these channels correspond to clothing, skin, hair, metal, and accent color, respectively. The pure colors are used by the color switcher to decide what color each pixel should be in the final portrait.

The easiest way to get the color channels right is to first draw your sprite in black and white, then color it using a multiply layer on top. This can be done in Gimp, Photoshop, and probably whatever graphics program you use unless it’s MS Paint. Use #FF0000, #FFFF00, #00FF00, #00FFFF, and #FF00FF as your colors on the multiply layer.

The Chameleon mecha portrait from GearHead Caramel showing how coloring can be done using the multiply layer.

Next question: How do we tell GearHead Caramel about the new portrait bit? For that, we turn to JSON. Inside the image folder there are a bunch of portrait definition files. Here’s the portrait definition for the hairstyle shown above:

Each portrait bit needs a unique name. The bit type tells the portrait generator when to use this bit; in this case, the type is “hair”, so this bit may be used whenever the portrait requests hair. Next you see definitions for each of the layers giving the image filename, anchor, offsets, depth, and frame information. After that, there’s layer information for the 64x64px avatar. The avatar layers have the same format as the portrait layers. Finally, there is a style tag. This particular hairstyle will normally only be chosen for a character with the “Feminine” tag, but you can disable style tags in the character creator and give this hairstyle to anyone you want.

I have uploaded my portrait work files and JSON definition files to Google Drive, so you can play around with them without needing to download the source code. You can place new portrait bits and JSON files in the image subfolder of your user folder (which should be “ghcaramel/” in your home directory). Please let me know if you have any questions.

GearHead Caramel v0.600: Completely Completeable

GearHead Caramel v0.600 has just been released. The big news this time around is that the DeadZone Drifter campaign now has a proper conclusion. In addition there are new skill trainers, you can buy medicine + other consumables, and you can ask potential lancemates about their skills before you hire them. The binaries for Windows, Linux, and MacOS can be downloaded from GitHub or Patreon.

Try it out and let me know what you think. Happy holidays!

Merry Christmas!

I was hoping to get the first complete release of DeadZone Drifter finished for today, but then last night while I was playing through the conclusion I found a major bug that has somehow gone unnoticed for the past year or so. Anyhow, I fixed the bug, and may or may not post a new release later today. It’s coming up soon at least!