Thursday 21 March 2013

The farmer quest

I haven't had much time to work on anything this week, but I did get the "farmer quest" from Zabin's game implemented (or at least, the script for it). I'm a bit blank on the imagination at the moment, so by just getting old functionality working the problem is just one of filling out the implementation and script api.

This was pretty nasty in Dusk script, but is relatively straightforward in javascript. I experimented with just using the java api's a bit here too, rather than just adding script-friendly functions; it's a bit harder to use, but is more powerful.

The 'quest' is operated by walking onto a location infront of the farmer, this is just attached to the action for the location. Rather than 'bounce' the player back a square I just let them stay there - actions only trigger when you enter a location, not for standing on one. Prevents some ugly flicker in the client.

if (trigger.isPlayer()) {
    var state = trigger.getInteger("farmerquest");
    var list = trigger.getAllItems("bugcorpse");

    if (state == 0) {
        // first meeting
        trigger.chat("Farmer says: It doesn't look like my crop is going to support me this year.");
        trigger.chat("Farmer says: Those $%$# bugs ate half my crop.");
        trigger.chat("Farmer says: I have a job for you, if you could kill those bugs and bring me back their corpse, i'll reward you for each one.");
        trigger.chat("Farmer says: If you bring me 20 corpse's at once and I'll give you something special.");
        trigger.setInteger("farmerquest", 1);

Since the state can be tracked explicitly, it's easier to test at what position the script is, without resorting to returning from the script. Actually as a side-effect of the way scripts are invoked, it's actually impossible to return anyway - every script must be fully structured just like the original PASCAL. i.e. there is no 'return' or 'exit' keyword allowed, since the scripts are run as top-level code.

    } else if (state < 3 && list.size() >= 20) {
        // Check for 20 at a go
        for (var i=0;i<20;i++) {
            trigger.removeItem(list.get(i));
        }
        trigger.chat("Farmer says: That's 20 corpses, here take my Kaizer Blade for helping me.");
        trigger.setInteger("farmerquest", 3);
        trigger.addItem(game.createItem("kaizer blade"));

This bit can also just access the Java List interfaces directly to do bulk operations, as well as the wider 'game' object which has general utility functions. I do however have to make sure I design these public interfaces properly as for example the scripts are executing on another thread (eventually in a sandbox), so getting the mix right will be an ongoing experiment.

    } else if (state <3 && !list.isEmpty()) {
        trigger.removeItem(list.get(0));
        trigger.chat("Farmer says: Great, you killed another Bug, here's 30 gold for your reward.");
        trigger.addGold(30);
        trigger.setInteger("farmerquest", 2);

Simple reward is trivial to implement, it also tracks if you've done it at least once.

    } else if (state == 1) {
        trigger.chat("Farmer says: Have you collected any corpses yet?");
    } else if (state == 2) {
        trigger.chat("Farmer says: What are you doing back again, haven't you got work to do?");
    } else {
        trigger.chat("Farmer says: Thank you for your work, I think my crops will be safe for another year.");
    }
}

And finally a bit more detail for the default case since we track the state explicitly. And this version doesn't let the player keep killing the newbie enemies for easy cash once they've got the grand prize.

Update: I decided to keep up with this approach and i'm working on porting Zabin's game to the new engine bit by bit. I've imported the map just as another world which you can enter via a door and started on the tutorial. This will let me work on it in little pieces and also expose any implementation issues in manageable chunks.

e.g. When I added all the mobs from the original I hit a bug with the entity index and decided to change it completely. Fortunately as one of the first things i "fixed" was to abstract the map and map-related operations it means I have complete flexibility on how the internals are implemented.

Previously it was implemented as a 2d index storing a pointer to the first entity at a given tile location. These were then linked using a next field in the object and managed using some single-linked-list logic. For big maps with sparse entities it takes a lot of memory just for the array - 2MB just for the original dusk map. I changed it to use a hash table keyed on an x+y pair and stored using a LinkedList. Lookups will be marginally slower but it's made up for by the reduced footprint and ease of use, and because I'd abstracted the map previously I can always change it.

Update: Another thing I noticed that doorways were a pita to write. For each one you need at least an entrance and leave action, as well as an entrance and leaving location. And each action requires a one-line script. So I added another function in the map file which allows jumps without requiring a script to be run.

   x.y.goto=alias-name

It still requires 4 settings, but it doesn't need the two single-line scripts. This also means that map location aliases must now be globally unique.

No comments: