Skip to content

Instantly share code, notes, and snippets.

@Glidias
Last active May 8, 2019 03:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Glidias/9cbd8bd8114649207b79c252873fd207 to your computer and use it in GitHub Desktop.
Save Glidias/9cbd8bd8114649207b79c252873fd207 to your computer and use it in GitHub Desktop.
SONG OF SWORDS: Character Creation Field formulas
SONG OF SWORDS: Character Creation UI Field formulas (for Beta 2.0)
/*
PSEUDO CODE LEGEND SUMMARY:
* instance field quantifier/process description
"the" - single/sole/final, etc.
"each" - for each, resolves to different results per instance
"all" - for all instances, resolves similarly across all instances
=> Direct instance implication/continuation (ie. holds internal field)
|=> per instance respective field value implication (1 to 1 relationships)
->instanceField
:min/:max value UI constraints per instance field
:misc per instance UI field property (eg. ":disabled"/":enabled"/":buyable", etc.)
.instanceField computed property
Valid() to perform both client/server-side validation beyond :min/:max client-side constraints
Warning() to perform client side validation that player still has unspent spendable points left
{
- 1st param: true/false. True to indicate warning.
- 2nd param: Remaining points left
}
*/
NOTE: No particular language here. Pseudo code for reference only.
Use the below formulas as a reference to bind to your own reactive UI bindings/validation/warning routines accordingly.
_____________
*the CampaignPowerLevel
=> PCP // (Player Creation Points pool)
=> MaxPCPPercategory // (Max Player Creation Points assignable per category)
;
_____________
*each CategoryPCP
|=> RaceTier
|=> AvailableAttributePoints
|=> BnBpoints
|=> SkillPoints
|=> Money
|=> ProfeciencyPoints
;
remainingAssignable = CampaignPowerLevel->PCP - SumOf(each CategoryPCP);
*each CategoryPCP
:min = (.notMagic ? 1 : 0);
:max = Min(CampaignPowerLevel->MaxPCPPercategory, {current} + remainingAssignable);
.Valid() = {current} <= CampaignPowerLevel->MaxPCPPercategory;
Valid(remainingAssignable) = remainingAssignable >= 0;
Warning(remainingAssignable) = {remainingAssignable > 0, remainingAssignable};
_____________
=> RaceTier
*each Race
:canSelect = CanChooseRace(race, RaceTier);
Valid(SelectedRace) = SelectedRace and CanChooseRace(SelectedRace, RaceTier);
_______________
|=> AvailableAttributePoints
totalAttributePointsSpent = SumOf(AttributePointsExpenditure(each Attribute));
remainingAttributePoints = AvailableAttributePoints - totalAttributePointsSpent;
*each Attribute
.racialModifier = racialModifierForAttribute(attribute); // +/- or 0 (if no modification)
.bareMinAttribute = (.notMagic?1:0);
.Valid(attribute) = attribute + racialModifier >= bareMinAttribute;
:min = Max(bareMinAttribute - racialModifier, bareMinAttribute);
:max = Min({current} + MaxAttributeLevelUpsFrom({current}, remainingAttributePoints), 8);
Valid(remainingAttributePoints) = remainingAttributePoints > =0;
Warning(remainingAttributePoints) = remainingAttributePoints > 0 and CanBuyMoreAttributeLevels(remainingAttributePoints) ? {TRUE, remainingAttributePoints} : FALSE;
_____________
=> BnBpoints
totalBaneExpenditure() => SumOf(all assigned Banes' costs);
totalBoonExpenditure() => SumOf(all assigned Boons' costs);
maxBanePointsEarnable = BnBpoints < 0 ? 15 - BnBpoints : 15;
totalBanePointsEarned => min( maxBanePointsEarnable, totalBaneExpenditure() );
totalBanePointsSpent = totalBaneExpenditure() - totalBanePointsEarned;
totalBnBScore = BnBpoints + totalBanePointsEarned - totalBanePointsSpent - totalBoonExpenditure();
maxBoonsSpendable = BnBpoints + maxBanePointsEarnable;
maxBoonsSpendableLeft = maxBoonsSpendable - totalBoonExpenditure();
*each BoonBaneAssign
(.rank >= 1) => active;
(.rank == 0) => not active;
*each BoonAssign->rank
:min = 0;
:max = Min(boonAssign.boon->maxTimesApplyable, HighestBoonRankPossibleWithRemaining(boonAssign, maxBoonsSpendableLeft) );
*each BaneAssign-rank
:min = 0;
:max = bane->highestRankPossible;
Valid(totalBnBScore) = totalBnBScore >= 0;
Warning(totalBnBScore) = totalBnBScore > 0 and CanBuyMoreBoon(totalBnBScore) ? {TRUE, totalBnBScore} : FALSE;
_______________
=> SkillPoints
totalSkillPointsProvided = SkillPoints + intelligence*2;
maxSkillPacketsAllowed = Floor( (totalSkillPointsProvided-individualSkillsSpent)/3 );
skillPacketsRemaining = maxSkillPacketsAllowed - skillPacketsBought;
maxIndividualSkillsSpendable = totalSkillPointsProvided - skillPacketsBought*3;
individualSkillsSpent = Sum(...*each Skill=>individuallyAssignedSkillLevel(skill) );
individualSkillsRemaining = maxIndividualSkillsSpendable - individualSkillsSpent;
totalSkillPointsLeft = totalSkillPointsProvided - individualSkillsSpent - skillPacketsBought*3;
*each SkillPacket
:min = 0; // min == 0 ==> not bought
:max = Min({current} + skillPacketsRemaining), MaxQtyUntilEntirelyUseless(skillPacket));
MaxQtyUntilEntirelyUseless(skillPacket) =>
// For static skillPackets (stupidly lenient)
Max(...*each skillPacket->skill: skillPacket->{current} + Ceil( (5 - currentPacketClampedSkillLevel(skill))/skillPacket->skill->qty ) )
// For static skillPackets (pennywise));
Same as above, but instead of using Max(), uses Min().
// For static skillPackets (pennywise and choosy));
Same as above, but instead of using Max(), uses Min(), and also replaces the Ceil(), with Floor() instead.
// For dynamic skillPackets (stupidly lenient)
AllTrue(...*each currentPacketAssignedSkillLevel(skill) < 5 ) ? skillPacket->{current} : skillPacket->{current} + 1;
// For dynamic skillPackets (pennywise)
IfGot1Case(...*each currentPacketAssignedSkillLevel(skill) + skillPacket->skill->qty - 5 > 1 ) ? skillPacket->{current} : skillPacket->{current} + 1;
// For dynamic skillPackets (pennywise and choosy)
IfGot1Case(...*each currentPacketAssignedSkillLevel(skill) + skillPacket->skill->qty > 5 ) ? skillPacket->{current} : skillPacket->{current} + 1;
Note: currentPacketClampedSkillLevel(skill) cannot go above 5.
It is a function that will clamp all skill levels assigned through skill packets to 5.
/* // is the below needed?
Alternatively, currentPacketClampedSkillLevel() should simply just be the regular skill level
combining (individuallyAssignedSkillLevel(skill) + curentPacketAssignedSkillLevel(skill)), but also clamped to 5.
If using the latter approach, then assigning individual skills earlier will also affect the buying of skill packets by limiting
how much a packet may affects it's relavant skills. Thus, this "hopefully" forces/encourages players to NOT buy skills individually
first but SHOULD buy it later after considering the packets. (Yes, it's a kinda sucky system as of now from UX standpoint...)
*/
*each SkillPacket->skill
:earnable = currentPacketClampedSkillLevel(skill) < 5;
*each Skill
= currentSkillLevelsEarnedThroughPackets(skill) + individuallyAssignedSkillLevel(skill);
:min = currentSkillLevelsEarnedThroughPackets(skill);
:max = {current} + individualSkillsRemaining;
Valid(totalSkillPointsLeft) = totalSkillPointsLeft >= 0;
Warning(totalSkillPointsLeft) = totalSkillPointsLeft > 0 : {TRUE : totalSkillPointsLeft } : FALSE;
______________
=> Social Class and Wealth
=> Money
=> WealthPoints
// For similar social class + wealth combo
// (current values for Social Class and Wealth is by index starting from zero)
SocialClass and Wealth
=> {categoryPCP - 1}
// For possible mismatched SocialClass and Wealth combo:
TotalPCPRequired = SocialClass != Wealth ? Ceil( ((SocialClass+1) + (Wealth+1))/2 ) + 2 : SocialClass+1;
Valid(TotalPCPRequired) => categoryPCP >= TotalPCPRequired
Warning(TotalPCPRequired) => categoryPCP > TotalPCPRequired
Solve for Maximum allowed SocialClass or Wealth indices, given categoryPCP..
let x = SocialClass+1; // (to solve given known `y` and `C`)
let y = Wealth+1; // (to solve given known `x` and `C`)
let C = categoryPCP;
Equation is:
Ceil((x+y)/2) + 2 <= C;
Workings and proof:
// Without Ceil(), if ((x + y)/2) is a whole number, assumably for any value of y which makes (x+y) cleanly divisible by 2.
(x+y)/2 <= C - 2;
x+y <= C*2 - 4;
x <= C*2 - 4 - y;
// Now, with Ceil() in effect, it's obvious (intuitively) based on common sense that
// the total PCP required will be increased by +1. But below is still a proof nevertheless.
// With Ceil(), if ((x+y)/2) is NOT a whole number, assumably for any value of y which makes (x+y) not cleanly divisible by 2.
// This will result in a "+0.5" remainder on LHS with Ceil(),
// to meet given cost on LHS.
(x+y)/2 + 0.5 + 2 <= C;
(x+y)/2 + 2.5 <= C;
(x+y)/2 <= C - 2.5;
x+y <= C*2 - 5;
x <= C*2 - 5 - y;
// (x+y) will have odd parity (with even+odd) combo, and even parity(with even+even or odd+odd) matching combo
// So, 2 ternary cases can be defined clearly as below with XOR operator to determine whether Ceil() occurs or not
x <= ((x&1)^(y&1)) != 0 ? C*2 - 5 - y : C*2 - 4 - y;
// as shown above, it's only a -1 maximum constant value adjustment, if Ceil() occurs.
So, another way to represent it is to conditionally subtract by 1 if necessary:
x <= C*2 - 4 - y - ((x&1)^(y&1));
- x and y is freely interchangable since they only exist within a (sum).
- to determine the ultimate solution for x, first determine test x initially as it is without the XOR operation.
- Substitute that x test result into the equation on RHS with the XOR operation to determine if the final result is lowered by 1.
----
moneyLeft = Money - TotalMoneyExpenditure();
remainingWealthPoints = WealthPoints - TotalWealthPointsExpenditure();
3 seperate makeshift WealthAssetList arrays for writing out:
Minor Assets (6gp liquidated value per asset)
Moderate Assets (12gp liquidated value per asset):
Major Assets(18gp liquidated value per asset):
WealthAssetList
:canBuyExtraSlot = moneyLeft - wealthAssetList->liquidateValue * 1.5;
:canUnbuyExtraSlot = wealthAssetList->boughtExtraSlots >= 1;
:min= wealthAssetList->boughtExtraSlots;
:max= Floor((remainingWealthPoints+wealthAssetList->slotWorth*{currentLength})/wealthAssetList->slotWorth) + wealthAssetList->boughtExtraSlots;
________________
=> Inventory and Shopping
// TotalMoneyExpenditure includes any selected school's entry cost besides cost of inventory
// If Shopping
*each Item
:canBuy = moneyLeft - item.cost >=0; // buying of 1 qty
:min = 0;
:max(when Not bought at all yet) = {current} + Floor(moneyLeft/item.cost);
OR
:max(when already have bought Qty) = {current} + Floor((moneyLeft+costOfItemXQty(item,qty))/item.cost);
// Validate money
Valid(moneyLeft) = moneyLeft >= 0;
Warning(moneyLeft) = FALSE; // ie. players are free to save as much money as they wish.
_________________
=> ProfeciencyPoints
#VERSION_1#
profArcCost = isHuman ? 1 : 3;
// or
#VERSION_2#
profArcCost = 3;
totalAvailProfSlots = SelectedSchool ? SelectedSchool->maxProfs : 0;
levelsExpenditure = GetSchoolLevelingCostFromZero(SelectedSchoolLevel); // function() returns 0 if no School selected yet
profsExpenditure = Max( profArcCost * TotalProfecienciesBoughtUnderSchool() - (#VERSION_2# and isHuman ? 2 : 0), 0); // function() returns 0 if no School selected yet
levelsProfsExpenditure = profsExpenditure + levelsExpenditure;
totalProfExpenditure = SelectedSchool ? SelectedSchool->arcCost + levelProfsExpenditure : 0;
profPointsLeft = ProfeciencyPoints - totalProfExpenditure;
*each School
:canSelect = ProfeciencyPoints - levelsProfsExpenditure >= school->arcCost
and school->CheckOtherRequirements(Character);
// typically Money.moneyLeft, or other aspects of Character
*each Profeciency
:canBuy = SelectedSchool and !AlreadySelected(profeciency) and
TotalProfecienciesBoughtUnderSchool() < totalAvailProfSlots
and profPointsLeft >= profArcCost;
:canUnbuy = TRUE; // unbuy() ==> min = 0
:min = 0; // min == 0 ==> not bought
:max = profeciency->maxLevels;
*the SelectedSchoolLevel
:min = 0;
:max = SelectedSchool ? GetSchoolMaxLevelsWith(ProfeciencyPoints - SelectedSchool->arcCost - profsExpenditure ) : 0;
// ie. players are free to save as much profeciency points if they wish at the start and defer buying of school/profiencies.
Valid(profPointsLeft) = profPointsLeft >= 0;
Warning(profPointsLeft) = FALSE;
@Glidias
Copy link
Author

Glidias commented May 8, 2019

Formulas based off last beta which might differ from official 1.0 release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment