Item value calculation

Does anyone know how NWN1 item value calculation works? I have the old BioWare Aurora Engine
Item Format pdf but the item value calculation described there isn’t 100% accurate (anymore). I’m looking for a way to calculate the value manually because RemoveItemProperty does not update the item value within the calling script and then the value returned by GetGoldPieceValue() is wrong.

So far I have a formula that calculates the correct item value for ‘normal’ items with ‘normal’ properties:

ItemValue = [ B + (1000.0 * (Sum of cost(iprp))^2) + Sum of (cost(sp)*f) ] * M * S + A

With B = BaseItem value, M = Item Multiplier, S = stack size, A = additional cost value and

cost(iprp) = cost(iprp type) * cost(iprp subtype) * cost(iprp costtablevalue) (for item properties that are not Cast Spell properties and with missing values = 1.0).

cost(sp) = (cost(iprp type) + cost(iprp subtype) * cost(iprp costtablevalue) (for item properties that are Cast Spell properties and with missing values = 0.0). The most expensive property get’s a factor 1.0, the 2nd expensive property get’s a factor 0.75 while the other properties get a factor 0.5.

This calculation is similar to the Bioware calculation, the calculation for the Cast Spell properties is the same (and actually taken from Bioware).

The formular returns wrong results for low Item Multipliers and high stack sizes. Also it seems that the value depends slightly on the order Cast Spell properties are applied to the item. For example adding Cast Spell: Aid to a potion of Cure Light Wounds results in a value of 135 while GetGoldPieceValue returns 130. But when I remove Cure Light Wounds and reapply it both my formula and GetGoldPieceValue return 130.

Any help is appreciated :slightly_smiling_face:

EDIT: Striked-through statements below are incorrect - please ignore

I believe that your formula should look like this (B not multiplied by M):

cost = [B + 1000.0*(...) * M] * S + A

Example: if you add Dodge (feat cost param = 1) to a Dagger (B=4, M=2), the result in the toolset is 2004, not 2008. I dunno if B should also be excluded from multiplication with S - you’d need to experiment with baseitems.2da (since vanilla stackable items all have B=0).

Can’t help right now with spell cost coefficient, but if everything else fails, you can try the approach below. It avoids any calculation, but is expensive.


Introduction

RemoveItemProperty appears to “mark” item props for deletion (invalidate them) rather than truly remove them - it happens after the script terminates. GetGoldPieceValue however incorrectly includes then in calculation. Probably RemoveEffect does the same.

The props (and effects) will return FALSE when passed to GetIsItemPropertyValid / GetIsEffectValid. You can use it to your advantage.

The algorithm

  1. Delete any item props you want from an item
  2. Create blank identical item (with same ResRef) somewhere
  3. Copy valid properties to the “copy” item
  4. Calculate cost of the copy
  5. Discard the copy
My test code is here - CLICK
void main()
{
    object oItem = CreateItemOnObject("nw_wswdg001"); // a dagger
    object oCopy;

    itemproperty ipProperty1 = ItemPropertyHaste();
    itemproperty ipProperty2 = ItemPropertyAttackBonus(5);
    itemproperty ipProperty;

    AddItemProperty(DURATION_TYPE_PERMANENT, ipProperty1, oItem);
    AddItemProperty(DURATION_TYPE_PERMANENT, ipProperty2, oItem);

    SpeakString("COST (2): " + IntToString(GetGoldPieceValue(oItem)));

    RemoveItemProperty(oItem, ipProperty1);

    SpeakString("COST (1): " + IntToString(GetGoldPieceValue(oItem)));

    // created on OBJECT_SELF for testing - should be
    // created in some temporary container or area
    // the unique tag is to ensure it won't be merged
    oCopy = CreateItemOnObject(GetResRef(oItem), OBJECT_SELF,
        GetItemStackSize(oItem), "COPY_" + ObjectToString(oItem));

    ipProperty = GetFirstItemProperty(oItem);
    while(GetIsItemPropertyValid(ipProperty))
    {
        AddItemProperty(GetItemPropertyDurationType(ipProperty),
            ipProperty, oCopy);
        ipProperty = GetNextItemProperty(oItem);
    }

    SpeakString("COST (real): " + IntToString(GetGoldPieceValue(oCopy)));

    DestroyObject(oCopy);
}

It appears to work correctly and can be turned into a proper function. But then it creates 1 item per 1 item so it’s going to ruin the performance when called in a loop often. It’s also not that helpful if you’re trying to remove properties sequentially.

1 Like

Thanks for your reply!

Yes of course, B does not get multiplied (the mistake is here in my post but not in my code).

Creating an item and copying the valid properties seems to be a very great idea. Not sure how it is with the additional costs and charges but that wouldn’t be too hard to include. Thanks for the example code.

I only remove properties one by one or maybe more than just one but only when I’m done I need the GoldPiece value. I’m working on some sort of dialog based item creator/changer that works on the 2da files only and whenever the user adds or removes a property the dialog should display the actual gold piece value.

This changes everything then (it’s good to include such info right away).

You add/remove IPs in dialogue’s action scripts, then calculate the cost in NPC’s reply’s StartingConditional (which is a different script, so the item should be updated by then) and put it in a custom token or a variable.

You could also check / adapt the various “forge” scripts on the Vault to see how they do it. Like this one (I haven’t tried it).

1 Like

I tried that already but it did not work.

At the moment everything is done in the conditional script but when the problem with GetGoldPieceValue appeared I moved the IP changing into the action script (actually into a script that is called via ExecuteScript within the action script). But still the value returned by GetGoldPieceValue() was the wrong one. And so I thought that the item’s value is updated (or the removed properties are actually removed) after both action and the next conditional script have terminated (and so there’s no need to include that info :grinning: ).

You’re right - it doesn’t get updated when you immediately go back to the “display cost” convo node. It’s like the dialogue moves too fast for the IPs to be purged in time. Pausing the conversation doesn’t help either.

Anyway, the good thing is that when I added an intermediate node (“NPC: property X is removed”, “PC: continue”), it worked well. It makes the convo longer (more clicking), though. You can however use that to play a VFX or summarize the current IPs, i.e.: “Haste is removed; following properties are present: …”, then later just show their number and cost.

1 Like

I thought about that one too but decided againt it. The problem also is, that once an item is selected the whole IP change process is done within a single dialog node and only within the conditional script (that always returns TRUE) of that node I know (depending on the previously selected answer) if I have to remove an IP, add an IP (and perhaps also have to remove an IP) or if I just have to present other IP options. For example if the user has selected IP Improved Evasion the script adds Improved Evasion but if the item already has IE than IE will be removed instead and when the user selects an IP that requires a subtype (or a costtable value or a param1 value) then the list of possible subtype values (costtable values or param1 values) will be presented.

So the IP changing dialog part has one node with ~30 answers and lots of custom tokens:

Summary
(IP modify script) NODE // (display item name, gold value and IP the user is working on)
        |- (conditionmal script 1) CustomToken1 (action script 1) -> NODE 
        |- (conditionmal script 2) CustomToken2 (action script 2) -> NODE 
        :
        |- (conditionmal script N-1) CustomTokenN-1 (action script N-1) -> NODE 
        |- (conditionmal script N) CustomTokenN (action script N) -> NODE      

The conditional script n returns TRUE if some sort of local ‘int array’ [n] is TRUE and the action script n sets a local int to n. So by checking that local int the IP modify script knows which answer the user has selected.[/details]

I’m using this system for the CCOH and it saves me lot’s of scripts as I can reuse the conditional/action scripts. And this item modifier will be part of an optional ‘cheat/debug/prepare for a adventure’-console.

Not sure how to fit an intermediate node in. Perhaps I could make the IP change script return FALSE and add the intermediate node on the same level:

Summary
(IP modify script) NODE1 // (display item name, gold value and IP the user is working on, returns FALSE if IPs are changed)
    |   |- (conditionmal script 1) CustomToken1 (action script 1) -> NODE1
    |   |- (conditionmal script 2) CustomToken2 (action script 2) -> NODE1
    :   :
    |   |- (conditionmal script N-1) CustomTokenN-1 (action script N-1) -> NODE1
    |   |- (conditionmal script N) CustomTokenN (action script N) -> NODE1     
    |
 NODE2 // displays 'You've changed the IPs, current IPs are...
        |-  -> NODE1

[edit]But unfortunately it seems that kind of link (linking to NODE1 and going to NODE2 instead if the conditional script returns FALSE for NODE1) is not possible. I always wondered if it were possible and now I know it’s not.[/edit]

I guess I have to think about this some more…

Anyway you have given me some good ideas, thank you for that.

1 Like

I think that is most likely because otherwise removing IPs/Effects in loop would not work.

Thanks.

That, but in hindsight I think mostly because GetFirst... and GetNext... functions skip over the marked IPs / effects, yielding an invalid one only when there is truly nothing else. Which is correct and expected.

What you run into is an obvious engine bug, but since it doesn’t affect the campaigns (IPs are rarely manually removed in general), BW had little incentive to fix it (the Lexicon states it is known since v1.61).

I was thinking of this:

// (NODE1 display item name, gold value and IP count)
(IP modify script) NODE1
    |   |- (conditionmal script 1) CustomToken1 (action script 1) -> NODE2 -> NODE1
    |   |- (conditionmal script 2) CustomToken2 (action script 2) -> NODE2 -> NODE1
    :   :
// NODE2 says "property X added / removed", plus anything sans the cost

Would that work? Since the convo is dynamic, you can use extra tokens to populate NODE2.

For IPs for example like AC/Damage/Enhancement Bonus IPSafeAddItemProperty() often also has to remove an IP. I’m not quite sure if you can upgrade items in HotU but if you can (and I think you can) it’s most likely done using IPSafeAddItemProperty(). So the problem most likely exists at least in one of the official campaigns. Perhaps BW worked around the problem (like I’m trying to do) but perhaps we all payed far too much for our upgrades but noone ever noticed :grinning:

About what you’ve suggested:
It would work but it would also make the whole thing very clumsy and cumbersome as the user would get NODE2 whenever he selects something: the user selects an item… ‘you’ve selected item XY, now I will present you the possible IPs’ instead of just presenting the IPs, the user selects an IP Type… ‘you’ve selected IP XY, now I will present you the possible subtypes’ instead of just presenting the subtypes, the user selects a SubType… ‘you’ve selected SubType XY, now I will present you the possible costtable values’ istead… after a while I guess the user may think that I’m consider him stupid.

But I think if I don’t get the item value calculation done I could try something similar of what you’ve suggested:

// (NODE1 display item name, gold value, current IP, ...)
(IP modify script) NODE1
  |- (conditionmal script 1) CustomToken1 (action script 1)  -> NODE1
  |- (conditionmal script n1) CustomToken1 (action script 1) -> NODE2 -> NODE1
  |- (conditionmal script 2) CustomToken2 (action script 2) -> NODE1
  |- (conditionmal script n2) CustomToken2 (action script 2) -> NODE2 -> NODE1
  |- ...

The conditional scrips nM already exist and return TRUE if the conditional scripts M would return FALSE and vice versa. But then another problem (or thing that is somewhat less then perfect) would arise as I would have to compute for every possible user selection M if that selection would result in a IP change or not. (condition M = FALSE or TRUE). Another thing is that my dialog is made for up to 20 possible selection options (if there are more, the options are divided into 2 or more pages and the user gets options for switching to first/previous/next/last page). But for pages where not all the 20 selection options are required both conditional script M and conditional script nM would have to return FALSE, something that isn’t possible at the moment as they are only checking the same local int. Well there are solutions for that problem too (instead of using the n scripts I could use a different script number or I could upgrade
the conditional scripts so they can return FALSE regardless of the local int they are supposed to check).

But I thnk all this would at best be only the 2nd best solution so I will spend some more time on my Calculate Item Value algorithm. Some progress is made here as I’m able to calculate the additional costs of an item - which provided to be much more complex than I originally thought - I thought that GetGoldPieceValue() would return baseitem cost + additional cost for an unidentified item but unfortunately it does not (only base item cost is returned). So I had to copy the item, remove all the properties, get the gold piece value, substract the base item cost and… realized that Im chasing my own tail. I tried to copy the modified copy but for that copy GetGoldPieceValue still returned the wrong value. In the end I managed to calculate the additional costs in two steps (step 1: copy item, strip IPs, step 2: calculate additional costs and destroy copy).

To give you an idea how that what I’m talking about looks like I’ve made some screen shots:

Summary


The user has selected an item and he can now choose an IP to add (or remove)


The user has choosen IP ‘Enhancement Bonus’

Now he has to choose the amount of enhancement bonus:

The user has choosen +2. Now he could choose another bonus or he could remove the +2 bonus by clicking +2 again (both would result in GetGoldPieceValue return the wrong value - the left value is my claculated value, the right one is what GetGoldPieceValue() returns). Selecting ‘Back’ would return him to the previous selction.


The user has selected ‘Back’ and now he could add another IP or go back to item selection.

1 Like

Probably. There is a forge in the Drow city (?) where you can exchange $ for IPs. Poor players who paid for some before getting that gnarly Scythe.

Nice pictures. Portrait pack? Check NCTS if you ever need more colors or more script-controlled color dynamics (i.e. higher bonus = stronger shade). This is product placement.

I now understand why an extra node would be annoying / slow everything.

So how about that calculate-cost-of-copy route? Updated algorithm:

  1. User selects target item; you make a blank CopyObject somewhere (additional cost gets transferred over).
  2. In NODE1’s conditional you copy all IPs from target item to the copy and calculate the cost of the copy; then remove all IPs from the copy.
  3. User selects something and IPs are added or removed from target.
  4. Return to step 1 (the copy stays as long as same item is worked on).

If you’re willing to keep creating and deleting the IPs, this saves you the trouble of manual cost calculation. I made a test and it worked without any intermediate nodes, keeping the interface fast and streamlined.

Here is my script
int StartingConditional()
{
    object oItem = GetObjectByTag("DAGGER"); // the target
    object oCopy = GetLocalObject(OBJECT_SELF, "copy");

    if(!GetIsObjectValid(oCopy))
    {
        // make a copy if it doesn't exist yet
        oCopy = CopyObject(oItem, GetLocation(OBJECT_SELF),
            OBJECT_SELF, "COPY_" + ObjectToString(oItem));
        SetLocalObject(OBJECT_SELF, "copy", oCopy);
    }

    // copy IPs from target to copy
    itemproperty ipProperty = GetFirstItemProperty(oItem);
    while(GetIsItemPropertyValid(ipProperty))
    {
        AddItemProperty(DURATION_TYPE_PERMANENT, ipProperty, oCopy);
        ipProperty = GetNextItemProperty(oItem);
    }

    // get the cost of the copy
    SetCustomToken(1000, IntToString(GetGoldPieceValue(oCopy)));

    // remove all IPs from the copy
    ipProperty = GetFirstItemProperty(oCopy);
    while(GetIsItemPropertyValid(ipProperty))
    {
        RemoveItemProperty(oCopy, ipProperty);
        ipProperty = GetNextItemProperty(oCopy);
    }

    return TRUE;
}

I guess it works because the game begins to wait for user input (reply selection), thus giving the engine time to “apply” the IP removal from copy. I haven’t checked if user can click fast enough to cause a race condition, though…

  • The portrait is made from a pic I’ve found on the internet, I think it’s somhow related to Skyrim (never played that so I don’t know).
  • In fact I’ve downloaded NCTS some time ago and thought about using it. It looks really good, congratulations! I have thought about using it for CCOH but decided against it for the moment. Besides the required work (large part of the CCOH code is just a big mess) I want’t sure if I could use it without a tlk file and without overwriting existing NWN files (‘stringtokens.2da’). But I’m thinking about rewriting the CCOH and then I plan to replace my color system with your NCTS.
  • item value calculation: the ‘copy IPs to blank copy’ approach is probably the best as it does not require to calculate the additional costs and it would work even if beamdog decided to change the item value calculation. But as I spend so much time on featuring out how item values are calculated I really want to complete that.

Btw (back to your first answer): B (= base item value) is to be multiplied by M (= item multiplier).

After tinkering with the algorithm somewhat more I think I have it now:

Summary
float MK_IPRP_CalculateItemPropertyCost(itemproperty iProp, int nCharges=-1)
{
    float fValue = 0.0f;
    if (GetIsItemPropertyValid(iProp))
    {
        int nType = GetItemPropertyType(iProp);
        int nSubType = GetItemPropertySubType(iProp);
        int nCostTableValue = GetItemPropertyCostTableValue(iProp);
//        int nParam1Value = GetItemPropertyParam1Value(iProp);

        string sTypeResRef = "itempropdef";
        string sSubTypeResRef = MK_IPRP_GetSubTypeResRef(iProp);
        string sCostTableResRef = MK_IPRP_GetCostTableResRef(iProp);
//        string sParam1ResRef = MK_IPRP_GetParam1ResRef(iProp);

        string sIPropCost = Get2DAString(sTypeResRef, "Cost", nType);
        string sSubTypeCost = Get2DAString(sSubTypeResRef, "Cost", nSubType);
        string sCostTableCost = Get2DAString(sCostTableResRef, "Cost", nCostTableValue);
//        string sParam1Cost = Get2DAString(sParam1ResRef, "Cost", nParam1Value);

        float fCharges = 1.0f;

        switch (nType)
        {
        case ITEM_PROPERTY_CAST_SPELL:
            switch (nCostTableValue)
            {
            case IP_CONST_CASTSPELL_NUMUSES_1_CHARGE_PER_USE:
            case IP_CONST_CASTSPELL_NUMUSES_2_CHARGES_PER_USE:
            case IP_CONST_CASTSPELL_NUMUSES_3_CHARGES_PER_USE:
            case IP_CONST_CASTSPELL_NUMUSES_4_CHARGES_PER_USE:
            case IP_CONST_CASTSPELL_NUMUSES_5_CHARGES_PER_USE:
                fCharges = IntToFloat(nCharges) / MK_IPRP_GetMaxCharges();
                break;
            default:
                fCharges = 1.0f;
            }
            fValue = (StringToFloat(sIPropCost) + StringToFloat(sCostTableCost)) * StringToFloat(sSubTypeCost) * fCharges;
            break;
        default:
            fValue = StringToFloat(sIPropCost!="" ? sIPropCost : sSubTypeCost) * (sCostTableCost!="" ? StringToFloat(sCostTableCost) : 1.0f);
            break;
        }
    }
    return fValue;
}

int MK_IPRP_CalculateGoldPieceValue(object oItem, int nAdditionalCost)
{
    int nValue=0;
    if (GetIsObjectValid(oItem))
    {
        int nBaseItemType = GetBaseItemType(oItem);

        // get the base cost from either baseitems.2da (everything but armor)
        // or armor.2da (armor)
        int nBaseCost = MK_IPRP_GetItemBaseCost(oItem);

        float fItemMultiplier = MK_Get2DAFloat("baseitems", "ItemMultiplier", nBaseItemType, 0.0);
        int nMaxStack = MK_Get2DAInt("baseitems", "Stacking", nBaseItemType, 1);

        int nCharges = GetItemCharges(oItem);

        float fIPCost = 0.0f;

        float fSpellCost1  = 0.0f;
        float fSpellCost2 = 0.0f;
        float fSpellCostR = 0.0f;

        itemproperty iProp = GetFirstItemProperty(oItem);
        while (GetIsItemPropertyValid(iProp))
        {
            if (GetItemPropertyDurationType(iProp) == DURATION_TYPE_PERMANENT)
            {
                switch (GetItemPropertyType(iProp))
                {
                case ITEM_PROPERTY_CAST_SPELL:
                {
                    float fSpellCost = MK_IPRP_CalculateItemPropertyCost(iProp, nCharges);

                    if (fSpellCost>fSpellCost1)
                    {
                        // *****************************************************
                        // The following line is somewhat queer as it makes
                        // the item value depend on the order in which 'Cast-
                        // Spell' properties are added to the item. If they are
                        // added in order from most expensive first and least
                        // expensive last the item will become more expensive as
                        // if they were added in reverse order. The original
                        // Bioware documentation (BioWare Aurora Engine Item
                        // Format) hints that following was supposed to happen:
                        //
                        //  - fSpellCost1: highest spell cost
                        //  - fSpellCost2: 2nd highest spell cost
                        //  - fSpellCostR: the rest
                        //
                        // The highest spell cost is multiplied by 1.0, the 2nd
                        // highest is multiplied by 0.75 and the rest is
                        // multiplied by 0.5.
                        //
                        // But if the CastSpell property with the highest cost
                        // is added after the 2nd highest then when no. 1 and
                        // no. 2 fight for first place no. 2 will not go to 2nd
                        // place but to the rest instead. So at the end 2nd
                        // place will be given to the wrong SpellCast property.
                        // It's even possible that 2nd place remains empty.
                        //
                        fSpellCostR += fSpellCost1;
                        //
                        // Using these two lines instead of the previous one
                        // would fix that.
                        //  fSpellCastR += fSpellCast2;
                        //  fSpellCast2 = fSpellCast1;

                        fSpellCost1  = fSpellCost;

                        // *****************************************************
                    }
                    else if (fSpellCost>fSpellCost2)
                    {
                        fSpellCostR += fSpellCost2;
                        fSpellCost2  = fSpellCost;
                    }
                    else
                    {
                        fSpellCostR += fSpellCost;
                    }

                    break;
                }
                default:
                    // Properties that aren't CastSpell properties
                    fIPCost += MK_IPRP_CalculateItemPropertyCost(iProp);
                    break;
                }
            }
            iProp = GetNextItemProperty(oItem);
        }

        int nSpellCost = FloatToInt(fSpellCost1 + 0.75f*fSpellCost2 + 0.5f*fSpellCostR);

        // For the next line the order of the multiplicands is important.
        //
        // The order 1000*fIPCost*fIPCost would give us the correct result but
        // unfortunately for some fIPCost that would be not the result the
        // official NWN item value calculation apparently gets sometimes (result-1).
        //
        // More precisely for every fIPCost where the IEEE 754 floating
        // point represenation is lower than fIPCost. That is for example 0.7,
        // 0.9, 1.4, 1.8, 2.1, 2.8, 3.3, 3.6, ...
        // In game this can be seen on any melee weapon+1 with no other IPs.
        // It's gold piece value is two gold to low (two because of the item
        // multiplier which is 2 for any weapon).
        //
        // For example for a simple dagger+1 we have:
        //   - basecost is 2,
        //   - item multiplier is 2,
        //   - fIPCost is 0.9F (IEEE 754 presentation is 0.899999976).
        // Calculating the better but wrong way would give us:
        //   - 1000 * 0.899999976 * 0.899999976 = 900.000 * 0.899999976 = 810.000 = 810
        //     Item value then would be 2*(2 + 810) = 2*812 = 1624
        // Doing the calculation as it's apparently done in NWN we get:
        //   - 0.899999976 * 0.899999976 * 1000 = 0.809999943 * 1000 = 809.999938 = 809
        //     That makes the item value 2*(2 + 809) = 2*811 = 1622 (two less than it should be)
        //
        int nIPCost =FloatToInt(fIPCost*fIPCost*1000);

        int nStackSize = GetItemStackSize(oItem);

        float fItemValue = (nBaseCost + nIPCost + nSpellCost) * fItemMultiplier + nAdditionalCost;

        nValue = FloatToInt(fItemValue) * nStackSize;
        if ((nValue==0) && (nStackSize==nMaxStack))
        {
            nValue = 1;
        }
    }
    return nValue;
}

This won’t compile because some function definitions are missing but it should be obvious what these function are doing.

1 Like

The best part about NCTS is that stringtokens & tlk is needed only for direct token embedding in the dialogue, i.e. <ncGold>this is gold.</nc>.

If you generate the color tags via script only, you only need the named_colors.hak. But then your project does not need any haks. In any case, it is a good learning resource.

Details

ncts

SetCustomToken(1002,
    NCTS_GetColorTokenFromHSV(NCTS_MakeHSV(0, 100, 75)) +
    "This response has 75% brightness.</c>");

I’ll probably need to rewrite it, though.

I made a mistake when reading the table. You are correct - I’ll amend that post.

Good job cracking that. Though looking at all those cases makes me appreciate the copy approach even more :wink: Vault entry soon?

I also have a hint. Everyone has their coding style of course, but to avoid deep code nesting (read: to keep things close to line beginning), I employ an “early quit” when possible and sensible:

if(!GetIsObjectValid(oItem)) { return 0; }

Small update to the algorithm: instead of

   int nIPCost =FloatToInt(1000*fIPCost*fIPCost);

it has to be

   int nIPCost =FloatToInt(fIPCost*fIPCost*1000);

Otherwise sometimes the calculated value differs by 1 * item multiplier from gold piece value. See code and comments for more information.

@NWShacker: Actually I had been a friend of early exits too but especially when writing NWScript I find it easier the other way. For example quite often I want to have one debug message displaying the function call, parameters and the result. Can’t do that with early exists and to save me the trouble of rewriting the code usually my function have only one exit.

Yes, BW chose the worst order, only one that loses accuracy in 32-bit floating point here. Rounding nIPCost instead of truncating would correct that, though. At least they didn’t go with -ffast-math (or it’s equivalent).

Strangely enough, when fIPCost is a literal 0.9 (your chosen example), the calculation by NWN engine yields correct result regardless of operand order:

void main()
{
    float fLiteral = 0.9;
    SendMessageToPC(GetFirstPC(), "1000xFxF = " + IntToString(
        2 * (2 + FloatToInt(1000.0 * fLiteral * fLiteral))));
    SendMessageToPC(GetFirstPC(), "Fx1000xF = " + IntToString(
        2 * (2 + FloatToInt(fLiteral * 1000.0 * fLiteral))));
    SendMessageToPC(GetFirstPC(), "FxFx1000 = " + IntToString(
        2 * (2 + FloatToInt(fLiteral * fLiteral * 1000.0))));
}

float

void test()
{
    float fLiteral = StringToFloat("0.9");
    SendMessageToPC(GetFirstPC(), "StringToFloat(\"0.9\")="+FloatToString(StringToFloat("0.9")));
    SendMessageToPC(GetFirstPC(), "0.9="+FloatToString(0.9));
    SendMessageToPC(GetFirstPC(), "1000xFxF = " + IntToString(
        2 * (2 + FloatToInt(1000.0 * fLiteral * fLiteral))));
    SendMessageToPC(GetFirstPC(), "Fx1000xF = " + IntToString(
        2 * (2 + FloatToInt(fLiteral * 1000.0 * fLiteral))));
    SendMessageToPC(GetFirstPC(), "FxFx1000 = " + IntToString(
        2 * (2 + FloatToInt(fLiteral * fLiteral * 1000.0))));
}

Crafting_0000a

Apparently StringToFloat is the evildoer :grinning:

Actually no! The 0.9 = 0.9000... appears to be a weird rounding (?) quirk of the NWN engine (or its compiler). @meaglyn @sherincall

StringToFloat seems to behave like you’d expect strtof / atof to behave. If you try the code with a different value, say 0.7, you will see this:

007

Looks like a match. And an expected outcome in the FxFx1000 order.

The script
const string STRING = "0.7";
const float LITERAL = 0.7;

void main()
{
    float F = LITERAL;

    SendMessageToPC(GetFirstPC(), "raw value = " + FloatToString(F, 0));
    SendMessageToPC(GetFirstPC(), "1000xFxF = " + IntToString(
        2 * (2 + FloatToInt(1000.0 * F * F))));
    SendMessageToPC(GetFirstPC(), "Fx1000xF = " + IntToString(
        2 * (2 + FloatToInt(F * 1000.0 * F))));
    SendMessageToPC(GetFirstPC(), "FxFx1000 = " + IntToString(
        2 * (2 + FloatToInt(F * F * 1000.0))));

    F = StringToFloat(STRING);

    SendMessageToPC(GetFirstPC(), "raw value = " + FloatToString(F, 0));
    SendMessageToPC(GetFirstPC(), "1000xFxF = " + IntToString(
        2 * (2 + FloatToInt(1000.0 * F * F))));
    SendMessageToPC(GetFirstPC(), "Fx1000xF = " + IntToString(
        2 * (2 + FloatToInt(F * 1000.0 * F))));
    SendMessageToPC(GetFirstPC(), "FxFx1000 = " + IntToString(
        2 * (2 + FloatToInt(F * F * 1000.0))));
}

Here’s what happens in an identical program compiled with gcc on x86_64:

For 0.9
literal (raw):		0.899999976
converted (raw):	0.899999976
literal (1000@1):	1624
literal (1000@2):	1624
literal (1000@3):	1622
converted (1000@1):	1624
converted (1000@2):	1624
converted (1000@3):	1622
For 0.7
literal (raw):		0.699999988
converted (raw):	0.699999988
literal (1000@1):	984
literal (1000@2):	984
literal (1000@3):	982
converted (1000@1):	984
converted (1000@2):	984
converted (1000@3):	982
The code
#include <stdio.h>
#include <stdlib.h>

#define STRING "0.9"
#define LITERAL 0.9

union float2hex
{
    float f;
    int32_t i;
};

int main(int argc, char** argv)
{
    float literal = LITERAL;
    float converted = strtof(STRING, NULL);
    union float2hex fd;

    printf("literal (dec):\t\t%0.9f\n", literal);
    printf("converted (dec):\t%0.9f\n", converted);

    fd.f = literal;
    printf("literal (hex):\t\t0x%x\n", fd.i);
    fd.f = converted;
    printf("converted (hex):\t0x%x\n", fd.i);

    printf("literal (1000@1):\t%d\n",
        2 * (2 + (int)(1000 * literal * literal)));
    printf("literal (1000@2):\t%d\n",
        2 * (2 + (int)(literal * 1000 * literal)));
    printf("literal (1000@3):\t%d\n",
        2 * (2 + (int)(literal * literal * 1000)));

    printf("converted (1000@1):\t%d\n",
        2 * (2 + (int)(1000 * converted * converted)));
    printf("converted (1000@2):\t%d\n",
        2 * (2 + (int)(converted * 1000 * converted)));
    printf("converted (1000@3):\t%d\n",
        2 * (2 + (int)(converted * converted * 1000)));
}

(the code assumes typecast to int truncates floats towards 0)

The good thing is that you’ll never run into this issue in your script. I’d only change the 0.9 in the comments to avoid puzzling the too-curious users :wink:

That is not a bug per se. That is down to the accuracy of floating point numbers. The engine stores them as close as it can in 2 to the power format (in other words it is base 2 scientific notation) given the limitation of the number of bits it has to work with. Check out what I wrote in TR’s Basics - Variables, Types and Functions (TBC).

TR