Create Your Own Adventure: Scenes and NPCs


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, "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())
        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

Get GearHead Caramel

Buy Now$10.00 USD or more

Leave a comment

Log in with itch.io to leave a comment.