CataTweaks [ 2014-12-19 ]

I decided to compile my own version of game with things I’d like to play with, that didn’t get into the mainline. In this thread, I am going to share what features are different from main version of the game. Feel free to rip any part you like and think that could get into the mainline.

Branch (source code on Git) I plan to keep up to date with current experimental version (hopefully daily): CataTweaks

Clothing Layers:

New layers
back layer - for things worn on ones back, like back pack, large scabbard, quivers
over shoulder layer - for thing worn over shoulder like messenger bag or rifle with shoulder strap, large waterskin
order of layers: under->regular->waist->belted->over->back | over shoulder
example: under shirt->hoodie->tool belt->shoulder holster->trench coat->backpack | SMG with strap
order is not enforced in any way, it is just how I envision it
Enforcing max 1 backpack
controversial, but such rule already exists for helmets and boots
while I was ensured that you can wear backpack on your chest, and I believe it, it has following problems
1) would be very encumbering, more than standard penalty and not not just for your torso
2) would prohibit quick access to items under it (chest rigs)
3) game allows for much more backpacks than just 2 backpacks now, which is IMO worse than enforcing 1 backpack
Moved to regular layer from belted layer (Kevin’s suggestion)
MBR vests
floating vest
utility vest
chest rig
Moved to back layer from belted layer
all backpacks
quivers
Moved to over shoulder layer from belted layer
purse (enc 1->2)
messenger bag (enc 0->1)
fanny pack (storage 6->3, I am aware that original fanny packs are not worn over shoulder)
Scabbard
covers (and potentially encumber) legs / belted
can contain swords up to volume 8
Large scabbard
covers torso / back
can contain swords up to volume 20
sheathing / unsheathing costs time

Container Inventory Management:

Container functionality
containers can contain everything, not just liquids
containers can contain mix of different items ( or one liquid )
containers can contain other containers ( nesting )
some containers have defined maximum size of item you can put in (liquids and
powders excluded)
maximum size is defined in field max_size and is in ml ( 1 volume = 250 ml )
containers with one type of content are shown with amount
canvas bag with scrap metals (7)
containers with mixed content are shown with volume
canvas sack (8 / 60)
Advanced Inventory Management ( AIM )
you can select container in both panes ( press c while container is highlighted )
you can select container inside container same way
you can return to parent container ( up to area itself ) by pressing backspace
Inventory
you can eat from your containers, if multiple comestibles (food, drugs) are found
inside, selection dialog is provided
you can reload from ammo in your containers
examine window shows list of content for containers
Item changes:
bag_canvas_small
canvas bag -> small canvas bag
volume 1 -> 0
New items
bag_canvas_medium
name: canvas bag
volume: 1
contains: 15
Labels
you can label items by carving with knife or writing with marker
you can choose if it is label or note after you select item
label is showed instead of normal name, you can both label and note same item
you can label canvas bag to food bag, if you plan to have food in it
small canvas bag to med kit, and your baseball bat to smasher, if you want
if you examine labeled item, original item name is shown as well
you can sew labels and notes on items from cotton, leather and fur
by using sewing kit (needles) on reinforced item

Hybrid Inventory System

Container functionality
wearables ( items defined in armor.json ) can be turned into wearable containers
to do so, you have to add “contains” element into its definition, with number
representing container space volume
you can also define “max_size” of inserted item in ml ( 1 volume = 250 ml )
Advanced Inventory Management ( AIM )
new Wielded and Worn area ( shortcut w ), you can’t move stuff from / to there,
but you can easily access wielded and worn containers
new Backpack area ( shortcut b ) quick access to your worn backpack
Item changes:
all backpacks have their “storage” converted to “contains”, they still
have some storage area that counts towards inventory space
example:
backpack
"storage": 10
"contains": 40
same with purse and messenger bag
pouches have now container space only
Other items:
if you have suggestion, which other worn items should also work as container,
feel free to post it
you don’t have to wait though, just edit all items you want!

User manual

In original game, containers can always hold only 1 type of item at a time. It can be anything, if it is generated by game, but players can only fill them with liquid. There are different ways how to do it, one of them is using Advanced Inventory Management aka AIM (default shortcut ‘/’) . Container Inventory Management, aka CIM, widens player options regarding container filling. In AIM, you can now fill your containers with everything you want… well there are some logic limitations I will describe later.

Using AIM for container interaction:

  1. in one pane, you can highlight container item by using arrow keys up and down.
  2. once the container item is highlighted, you can press ‘c’ to switch that pane to show content of highlighted container
  3. you can repeat these steps to show content of nested container as well
  4. switch to other pane
  5. highlight items you want to load into the container and press ‘Enter’ or ‘m’ to move highlighted item
  6. to unselect container, you just press key for other area, while the pane is active
    or you can use ‘Backspace’ to select parent (item in which the container is) of currently selected item

It works pretty much like traditional file management programs line norton commander, salamander, free commander, etc.

Limitations:

  1. you cant mix different liquids in one container
  2. if your container holds liquid, it cant hold anything else
  3. if your container holds non-liquids, you cant fill it with liquid
  4. some containers can contain only small size, to simulate bottleneck
  5. you can fill container up to its storage space if it is rigid container (fixed shape)
  6. non-rigid containers, like sacks can have their maximum storage space limited by
    parent container, if it has less space

Once you have stuffed items, food for example, into container, you have two options if you want to consume it. Either pull it out of container first via AIM ( annoying way ), or use Eat command, select food container and pick what you want to eat from list of content (much better way) . Same goes for drugs and ammo.

Compiled game for Windows tiles for people who don’t want to compile game themselves - CDDA_CataTweaks

Great changes and additions!

I like those changes!

Cool, a scabbard I can fit my Heavy Sword of the Broken City in!

Thanks guys.

Right now, I am working on the possibility to put things (not just liquids) into containers. Hopefully, I will survive the endeavor.

Sounds awesome. Why aren’t you pull requesting this?

I tried, wasn’t awesome enough for people who can merge it into mainline. shrug

What we need for a container-based inventory is an inventory UI that makes it usable, otherwise it’s too clunky.

Right now, I am knee deep in AIM, I think that I can make it useable for container interaction itself.

For main inventory screen, I don’t think I can make it good enough and I don’t plan to. My goal is hybrid inventory system. Items that has pockets will give you general inventory storage for quick access (as it is now, shortcuts and all), but you will be able to put items into containers, without quick access though, you would need to pull that item out first.

Second step is adding container storage area to some wearable items, like backpacks, making them wearable containers. Such item could have some quick storage too. For example, backpack could have storage 5 (general quick access storage area, to simulate pockets on it) and container storage area 40.

Well, that is the plan, anyway.

Right now, I am knee deep in AIM, I think that I can make it useable for container interaction itself.

For main inventory screen, I don’t think I can make it good enough and I don’t plan to. My goal is hybrid inventory system. Items that has pockets will give you general inventory storage for quick access (as it is now, shortcuts and all), but you will be able to put items into containers, without quick access though, you would need to pull that item out first.[/quote]
That’s precisely why we don’t have it yet, overhauling the whole inventory ui to work properly with containers is a monumental task, but nothing short of that is going to be worthwhile. Manually putting things in containers and manually taking them back out again is simply a non-starter.

Be aware that the item class already has a contents member that can hold arbitrary items, and it’s already wired up to save/load.

That’s what general inventory storage is for, if you need something readily available, keep it in your pockets and not in your backpack (or other container). On the other hand, having container area that don’t mess with your inventory can be quite beneficial.

Use case - Loot run
You are going to loot nearest town. All equipment you need is readily available in your chest rig, army pants and trench coat pockets (and holsters, sheaths, scabbards…) . But you also have your trusted rucksack you plan to put the loot in. If you find something truly useful, you will put it in your pockets for quick access, other things you plan to sort out after return from your trip.

In other words, your loot will not mix up with your gear, making inventory screen a mess in the process.

I am aware of that, it is just missing “contains” field from container definition. And I plan to add new field for containers, “max_size” to limit maximum size of items loaded into container (liquids and powders excluded), so you can’t fill your jug with two by fours.

Could give head encumbrance reduced hearing penalties if you wanna touch that up too.

Honestly, I feel like the system that’s in now would work better than having to deal with the tedium of containers.

There are use cases where adding manual insertion/extraction of items from containers would be a net benefit. The problem is if that mechanism is generic, players can and will use it in situations where it’s not a win, where it is in fact a huge loss, which means bad play experience.

Guys, thanks for encouragement… wink

I could argue with you, but I don’t think I can convince you with words anyway, I just hope that you will give it a try when it is done.

I’m not in awe of your solution altough you seem to follow a concept, which is cool.
Let me just remind you about those hiking/mountaineering backpacks that are in fact modular; you may tackle the issue with this in mind.

On containers - you won’t have a full grasp of the issue until you’ve understood the scope of this game feature; I’m addressing people who are reading this topic but don’t dev the game. I believe the greatest deal is with functionality of items stored inside containers, that are held within (bigger) containers.
In other words, even object programming has its own shortcomings; think of this for a second:
“Once I’ve taken out some items from my backpack which is on my back, and maybe some more from the shopping bag in my right hand, where exactly are these items at this point?”

[quote=“vultures, post:17, topic:7777”]I’m not in awe of your solution altough you seem to follow a concept, which is cool.
Let me just remind you about those hiking/mountaineering backpacks that are in fact modular; you may tackle the issue with this in mind.[/quote]

Like multiple storage areas? I am not going to add that, but you can simulate it by putting containers into the bag.

Yes.

I just finished handling of non rigid containers, that change their volume with content. Things like putting sack with volume up to 60 into box with fixed volume of 15, then start filling the sack… !FUN!

And then I found that content of nested containers is not persisted in save. Edit: no matryoshka dolls → yes matryoshka dolls

confused

no matryoshka dolls -> yes matryoshka dolls

Comment of the year IMHO.

Just refactored player::consume so I can now add food consumption from nested / mixed containers without going insane. I wonder how can someone find anything in these walls of code.

Original

bool player::consume(int target_position)
{
    item *to_eat = NULL;
    it_comest *comest = NULL;
    int which = -3; // Helps us know how to delete the item which got eaten

    if(target_position == INT_MIN) {
        add_msg_if_player( m_info, _("You do not have that item."));
        return false;
    } if (is_underwater()) {
        add_msg_if_player( m_info, _("You can't do that while underwater."));
        return false;
    } else if( target_position < -1 ) {
        add_msg_if_player( m_info, _( "You can't eat worn items, you have to take them off." ) );
        return false;
    } else if (target_position == -1) {
        // Consume your current weapon
        if (weapon.is_food_container(this)) {
            to_eat = &weapon.contents[0];
            which = -2;
            if (weapon.contents[0].is_food()) {
                comest = dynamic_cast<it_comest*>(weapon.contents[0].type);
            }
        } else if (weapon.is_food(this)) {
            to_eat = &weapon;
            which = -1;
            comest = dynamic_cast<it_comest*>(weapon.type);
        } else {
            add_msg_if_player(m_info, _("You can't eat your %s."), weapon.tname().c_str());
            if(is_npc()) {
                debugmsg("%s tried to eat a %s", name.c_str(), weapon.tname().c_str());
            }
            return false;
        }
    } else {
        // Consume item from inventory
        item& it = inv.find_item(target_position);
        if (it.is_food_container(this)) {
            to_eat = &(it.contents[0]);
            which = 1;
            if (it.contents[0].is_food()) {
                comest = dynamic_cast<it_comest*>(it.contents[0].type);
            }
        } else if (it.is_food(this)) {
            to_eat = &it;
            which = 0;
            comest = dynamic_cast<it_comest*>(it.type);
        } else {
            add_msg_if_player(m_info, _("You can't eat your %s."), it.tname().c_str());
            if(is_npc()) {
                debugmsg("%s tried to eat a %s", name.c_str(), it.tname().c_str());
            }
            return false;
        }
    }

    if(to_eat == NULL) {
        debugmsg("Consumed item is lost!");
        return false;
    }

    int amount_used = 1;
    bool was_consumed = false;
    if (comest != NULL) {
        if (comest->comesttype == "FOOD" || comest->comesttype == "DRINK") {
            was_consumed = eat(to_eat, comest);
            if (!was_consumed) {
                return was_consumed;
            }
        } else if (comest->comesttype == "MED") {
            if (comest->tool != "null") {
                // Check tools
                bool has = has_amount(comest->tool, 1);
                // Tools with charges need to have charges, not just be present.
                if (item_controller->count_by_charges( comest->tool )) {
                    has = has_charges(comest->tool, 1);
                }
                if (!has) {
                    add_msg_if_player(m_info, _("You need a %s to consume that!"),
                                         item_controller->nname( comest->tool ).c_str());
                    return false;
                }
                use_charges(comest->tool, 1); // Tools like lighters get used
            }
            if (comest->has_use()) {
                //Check special use
                amount_used = comest->invoke(this, to_eat, false, pos());
                if( amount_used <= 0 ) {
                    return false;
                }
            }
            consume_effects(to_eat, comest);
            moves -= 250;
            was_consumed = true;
        } else {
            debugmsg("Unknown comestible type of item: %s\n", to_eat->tname().c_str());
        }
    } else {
 // Consume other type of items.
        // For when bionics let you eat fuel
        if (to_eat->is_ammo() && has_active_bionic("bio_batteries") &&
            dynamic_cast<it_ammo*>(to_eat->type)->type == "battery") {
            const int factor = 1;
            int max_change = max_power_level - power_level;
            if (max_change == 0) {
                add_msg_if_player(m_info, _("Your internal power storage is fully powered."));
            }
            charge_power(to_eat->charges / factor);
            to_eat->charges -= max_change * factor; //negative charges seem to be okay
            to_eat->charges++; //there's a flat subtraction later
        } else if (!to_eat->type->is_food() && !to_eat->is_food_container(this)) {
            if (to_eat->type->is_book()) {
                it_book* book = dynamic_cast<it_book*>(to_eat->type);
                if (book->type != NULL && !query_yn(_("Really eat %s?"), to_eat->tname().c_str())) {
                    return false;
                }
            }
            int charge = (to_eat->volume() + to_eat->weight()) / 9;
            if (to_eat->made_of("leather")) {
                charge /= 4;
            }
            if (to_eat->made_of("wood")) {
                charge /= 2;
            }
            charge_power(charge);
            to_eat->charges = 0;
            add_msg_player_or_npc( _("You eat your %s."), _("<npcname> eats a %s."),
                                     to_eat->tname().c_str());
        }
        moves -= 250;
        was_consumed = true;
    }

    if (!was_consumed) {
        return false;
    }

    // Actions after consume
    to_eat->charges -= amount_used;
    if (to_eat->charges <= 0) {
        if (which == -1) {
            weapon = ret_null;
        } else if (which == -2) {
            weapon.contents.erase(weapon.contents.begin());
            add_msg_if_player(_("You are now wielding an empty %s."), weapon.tname().c_str());
        } else if (which == 0) {
            inv.remove_item(target_position);
        } else if (which >= 0) {
            item& it = inv.find_item(target_position);
            it.contents.erase(it.contents.begin());
            const bool do_restack = inv.const_stack(target_position).size() > 1;
            if (!is_npc()) {
                bool drop_it = false;
                if (OPTIONS["DROP_EMPTY"] == "no") {
                    drop_it = false;
                } else if (OPTIONS["DROP_EMPTY"] == "watertight") {
                    drop_it = it.is_container() && !(it.has_flag("WATERTIGHT") && it.has_flag("SEALS"));
                } else if (OPTIONS["DROP_EMPTY"] == "all") {
                    drop_it = true;
                }
                if (drop_it) {
                    add_msg(_("You drop the empty %s."), it.tname().c_str());
                    g->m.add_item_or_charges(posx, posy, inv.remove_item(target_position));
                } else {
                    add_msg(m_info, _("%c - an empty %s"), it.invlet, it.tname().c_str());
                }
            }
            if (do_restack) {
                inv.restack(this);
            }
            inv.unsort();
        }
    }
    return true;
}

New

bool player::consume( int target_position )
{
    item *it = nullptr;
    it_comest *comest = nullptr;
    consume_loc loc = CONSUME_NONE; // Helps us know how to delete the item which got eaten

    it = consume_choose_item( target_position, loc );
    if( it == nullptr ) {
        return false;
    }

    int used_amount = 0;
    bool consumed = false;
    comest = dynamic_cast<it_comest*>( it->type );
    if( comest != nullptr ) {
        if( comest->comesttype == "FOOD" || comest->comesttype == "DRINK" ) {
            consumed = consume_food( it, comest, used_amount );
        } else if( comest->comesttype == "MED" ) {
            consumed = consume_med( it, comest, used_amount );
        } else {
            debugmsg( "Unknown comestible type of item: %s\n", it->tname().c_str() );
        }
    } else {
        consumed = consume_bio( it, used_amount );
    }

    if( !consumed ) {
        return false;
    }

    consume_cleanup( loc, target_position, it, used_amount );

    return true;
}

item* player::consume_choose_item( const int target_position, consume_loc &loc )
{
    item *to_eat = nullptr;

    if( target_position == INT_MIN ) {
        add_msg_if_player( m_info, _( "You do not have that item." ) );
        return nullptr;
    }
    if( is_underwater() ) {
        add_msg_if_player( m_info, _( "You can't do that while underwater." ) );
        return nullptr;
    }
    if( target_position < -1 ) {
        add_msg_if_player( m_info, _( "You can't eat worn items, you have to take them off." ) );
        return nullptr;
    }

    if( target_position == -1 ) {
        // Consume your current weapon
        if( weapon.is_food_container( this ) ) {
            to_eat = &weapon.contents[0];
            loc = CONSUME_WEAPON_CONTAINER;
        } else if( weapon.is_food( this ) ) {
            to_eat = &weapon;
            loc = CONSUME_WEAPON;
        } else {
            add_msg_if_player( m_info, _( "You can't eat your %s." ), weapon.tname().c_str() );
            if( is_npc() ) {
                debugmsg( "%s tried to eat a %s", name.c_str(), weapon.tname().c_str() );
            }
        }
    } else {
        // Consume item from inventory
        item& it = inv.find_item( target_position );
        if( it.is_food_container( this ) ) {
            to_eat = &( it.contents[0] );
            loc = CONSUME_INVENTORY_CONTAINER;
        } else if( it.is_food( this ) ) {
            to_eat = &it;
            loc = CONSUME_INVENTORY;
        } else {
            add_msg_if_player( m_info, _( "You can't eat your %s." ), it.tname().c_str() );
            if( is_npc() ) {
                debugmsg( "%s tried to eat a %s", name.c_str(), it.tname().c_str() );
            }
        }
    }

    return to_eat;
}

bool player::consume_food( item *it, it_comest *comest, int &used_amount )
{
    used_amount = 1;
    return eat( it, comest );
}

bool player::consume_med( item *it, it_comest *comest, int &used_amount )
{
    // Check tools
    if( comest->tool != "null" ) {
        bool has = has_amount( comest->tool, 1 );
        // Tools with charges need to have charges, not just be present.
        if( item_controller->count_by_charges( comest->tool ) ) {
            has = has_charges( comest->tool, 1 );
        }
        if( !has ) {
            add_msg_if_player( m_info, _( "You need a %s to consume that!" ),
                               item_controller->nname( comest->tool ).c_str() );
            return false;
        }
        use_charges( comest->tool, 1 ); // Tools like lighters get used
    }

    used_amount = 1;
    //Check special use
    if( comest->has_use() ) {
        used_amount = comest->invoke( this, it, false, pos() );
        if( used_amount <= 0 ) {
            return false;
        }
    }

    consume_effects( it, comest );
    moves -= 250;

    return true;
}

bool player::consume_bio( item *it, int &used_amount )
{
    if( it->type->is_food() ) {
        debugmsg("player::consume_bio( %s ); is food!", it->tname().c_str());
        return false;
    }
    if( it->is_food_container( this ) ) {
        debugmsg("player::consume_bio( %s ); is food container!", it->tname().c_str());
        return false;
    }

    if( it->is_ammo() && has_active_bionic( "bio_batteries" ) &&
        dynamic_cast<it_ammo*>( it->type )->type == "battery" ) {

        int missing_power = max_power_level - power_level;
        used_amount = std::min( (long)missing_power, it->charges );
        charge_power( used_amount );

        if( power_level == max_power_level ) {
            add_msg_if_player( m_info, _( "Your internal power storage is fully powered." ) );
        }
    } else {
        if( it->type->is_book() ) {
            it_book* book = dynamic_cast<it_book*>( it->type );
            if( book->type != NULL && !query_yn( _( "Really eat %s?" ), it->tname().c_str() ) ) {
                return false;
            }
        }

        int charge = ( it->volume() + it->weight() ) / 9;
        if( it->made_of( "leather" ) ) {
            charge /= 4;
        }
        if( it->made_of( "wood" ) ) {
            charge /= 2;
        }
        charge_power( charge );
        used_amount = it->charges;

        add_msg_player_or_npc( _( "You eat your %s." ), _( "<npcname> eats a %s." ),
                               it->tname().c_str() );
    }

    moves -= 250;

    return true;
}

void player::consume_cleanup( consume_loc loc, int target_position, item *it, int used_amount )
{
    it->charges -= used_amount;
    if( it->charges <= 0 ) {
        switch ( loc ) {
            case CONSUME_WEAPON:
                weapon = ret_null;
                break;
            case CONSUME_WEAPON_CONTAINER:
                weapon.contents.erase( weapon.contents.begin() );
                add_msg_if_player( _( "You are now wielding an empty %s." ), weapon.tname().c_str() );
                break;
            case CONSUME_INVENTORY:
                inv.remove_item( target_position );
                break;
            case CONSUME_INVENTORY_CONTAINER:
                item &cont = inv.find_item( target_position );
                cont.contents.erase( cont.contents.begin() );
                const bool do_restack = inv.const_stack( target_position ).size() > 1;
                if( !is_npc() ) {
                    if( options_drop_container( cont ) ) {
                        add_msg( _( "You drop the empty %s." ), cont.tname().c_str() );
                        g->m.add_item_or_charges( posx, posy, inv.remove_item( target_position ) );
                    } else {
                        add_msg( m_info, _( "%c - an empty %s" ), cont.invlet, cont.tname().c_str() );
                    }
                }
                if( do_restack ) {
                    inv.restack( this );
                }
                inv.unsort();
                break;
        }
    }
}

bool player::options_drop_container( const item &it ) const
{
    if ( it.is_container_empty() ) {
        if( OPTIONS["DROP_EMPTY"] == "all" ) {
            return true;
        }
        if( OPTIONS["DROP_EMPTY"] == "watertight" ) {
            return it.is_watertight_container();
        }
    }
    return false;
}