The GAM model of creature level shows relative influence to a few attributes.
model.gam.everything <- gam(
level ~
s(hardiness) +
s(fortitude) +
s(dexterity) +
s(endurance) +
s(intellect) +
s(cleverness) +
s(dependability) +
s(courage) +
s(fierceness) +
s(power) +
s(kinetic) +
s(energy) +
s(blast) +
s(heat) +
s(cold) +
s(electricity) +
s(acid) +
s(stun),
data = normalized_df,
family = gaussian()
)
summary(model.gam.everything)
##
## Family: gaussian
## Link function: identity
##
## Formula:
## level ~ s(hardiness) + s(fortitude) + s(dexterity) + s(endurance) +
## s(intellect) + s(cleverness) + s(dependability) + s(courage) +
## s(fierceness) + s(power) + s(kinetic) + s(energy) + s(blast) +
## s(heat) + s(cold) + s(electricity) + s(acid) + s(stun)
##
## Parametric coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 23.05135 0.06939 332.2 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Approximate significance of smooth terms:
## edf Ref.df F p-value
## s(hardiness) 2.774 3.579 4.803 0.001739 **
## s(fortitude) 8.925 8.994 92.676 < 2e-16 ***
## s(dexterity) 7.381 8.354 7.183 < 2e-16 ***
## s(endurance) 1.000 1.000 9.498 0.002248 **
## s(intellect) 3.023 3.854 23.165 < 2e-16 ***
## s(cleverness) 4.658 5.777 14.774 < 2e-16 ***
## s(dependability) 1.090 1.171 6.456 0.011768 *
## s(courage) 4.612 5.632 4.316 0.000485 ***
## s(fierceness) 1.000 1.000 6.345 0.012290 *
## s(power) 8.165 8.785 18.242 < 2e-16 ***
## s(kinetic) 8.115 8.758 5.838 2.50e-07 ***
## s(energy) 8.453 8.892 50.694 < 2e-16 ***
## s(blast) 1.000 1.000 3.715 0.054860 .
## s(heat) 1.000 1.000 0.027 0.869184
## s(cold) 2.969 3.733 9.033 2.46e-06 ***
## s(electricity) 1.000 1.000 12.828 0.000399 ***
## s(acid) 3.651 4.474 4.538 0.001255 **
## s(stun) 1.000 1.000 11.723 0.000704 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## R-sq.(adj) = 0.991 Deviance explained = 99.3%
## GCV = 2.2033 Scale est. = 1.7816 n = 370
Specifically, the following attributes:
The appearance of kinetic, energy, cold and no other resists is especially strange.
Also interesting is that the following attributes have 1.00 degrees of freedom (so their influence is essentially flat):
We are going to make some assumptions about the model based on our domain knowledge of the game, and to avoid over-fitting.
It’s possible that the additive models associate higher weights to hardiness due to its correlation with fortitude. However, for now, we’ll assume that each attribute is more or less equal.
We’re combining these two because they are uniquely capped at 60%.
This is due to domain knowledge – “vuln stacking” creatures caused a measurable drop in creature level. It’s possible the special emphasis given to cold is due to over-fitting or something unique about furrycat’s data.
To mediate these concerns, we can create the following synthetic attributes
average_hdi – is the average of hardiness, dexterity, and intellect. Taking the mean of these attributes and training the GAM on this synthetic feature will force it not to over-fit on any of hardiness, dexterity, and intellect.
kinen – mean of kinetic and energy, for the same reasons.
nonkinen – mean of cold, heat, electricity, acid, and stun.
With that, this is the final model:
model.gam <- gam(
level ~
s(average_hdi) +
s(fortitude) +
s(cleverness) +
s(power) +
s(kinen) +
s(nonkinen),
data = normalized_df
)
summary(model.gam)
##
## Family: gaussian
## Link function: identity
##
## Formula:
## level ~ s(average_hdi) + s(fortitude) + s(cleverness) + s(power) +
## s(kinen) + s(nonkinen)
##
## Parametric coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 23.05135 0.09015 255.7 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Approximate significance of smooth terms:
## edf Ref.df F p-value
## s(average_hdi) 5.600 6.785 36.79 <2e-16 ***
## s(fortitude) 8.919 8.994 79.20 <2e-16 ***
## s(cleverness) 7.694 8.556 15.32 <2e-16 ***
## s(power) 2.371 3.040 56.96 <2e-16 ***
## s(kinen) 4.282 5.267 57.10 <2e-16 ***
## s(nonkinen) 2.356 2.978 44.41 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## R-sq.(adj) = 0.985 Deviance explained = 98.6%
## GCV = 3.2935 Scale est. = 3.0067 n = 370
Analysis of the data shows extreme an extreme segmentation point \(fortitude = 500\).
In fact, the \(fortitude >= 500\) data set is particularly well-behaved.
model.gam.armor <- gam(
level ~
s(average_hdi) +
s(fortitude) +
s(cleverness) +
s(power) +
s(kinen) +
s(nonkinen),
data = armor_df
)
summary(model.gam.armor)
##
## Family: gaussian
## Link function: identity
##
## Formula:
## level ~ s(average_hdi) + s(fortitude) + s(cleverness) + s(power) +
## s(kinen) + s(nonkinen)
##
## Parametric coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 42.7722 0.1541 277.6 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Approximate significance of smooth terms:
## edf Ref.df F p-value
## s(average_hdi) 4.641 5.649 20.446 < 2e-16 ***
## s(fortitude) 7.098 8.062 7.065 2.03e-06 ***
## s(cleverness) 1.000 1.000 64.836 < 2e-16 ***
## s(power) 1.445 1.759 43.904 < 2e-16 ***
## s(kinen) 1.000 1.000 44.441 < 2e-16 ***
## s(nonkinen) 4.239 5.227 11.536 1.89e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## R-sq.(adj) = 0.99 Deviance explained = 99.3%
## GCV = 2.5287 Scale est. = 1.875 n = 79
In fact, the relative degrees of freedom show that several of these parameters are already close to linear.
linear.fit.level.armor <- lm(
level ~
average_hdi +
fortitude +
cleverness +
power +
kinen +
nonkinen,
data = armor_df
)
summary(linear.fit.level.armor)
##
## Call:
## lm(formula = level ~ average_hdi + fortitude + cleverness + power +
## kinen + nonkinen, data = armor_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -4.1643 -1.0873 0.1926 1.0277 4.3139
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -21.331842 3.491081 -6.110 4.60e-08 ***
## average_hdi 0.027648 0.004965 5.568 4.18e-07 ***
## fortitude 0.056252 0.006059 9.285 6.18e-14 ***
## cleverness 0.024034 0.003182 7.552 1.05e-10 ***
## power 0.015740 0.002460 6.398 1.39e-08 ***
## kinen 0.096920 0.018767 5.164 2.06e-06 ***
## nonkinen 0.085904 0.015458 5.557 4.36e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.93 on 72 degrees of freedom
## Multiple R-squared: 0.9818, Adjusted R-squared: 0.9802
## F-statistic: 645.6 on 6 and 72 DF, p-value: < 2.2e-16
This simple linear model behaves remarkably well.
And analysis of the residuals are highly promising.
shapiro.test(rs.model.level.armor)
##
## Shapiro-Wilk normality test
##
## data: rs.model.level.armor
## W = 0.98336, p-value = 0.3941
bptest(linear.fit.level.armor)
##
## studentized Breusch-Pagan test
##
## data: linear.fit.level.armor
## BP = 6.1042, df = 6, p-value = 0.4116
To summarize, this simple linear model for the “armored” data set (i.e. creatures that have armor):
Taken together, this may imply that the residuals are due to randomness within the crafting system itself, and may not be due to missing variables or unknown non-linear relationships.
The same analysis for unarmored creatures does not look as promising.
linear.fit.level.noarmor <- lm(
level ~
average_hdi +
fortitude +
cleverness +
power +
kinen +
nonkinen,
data = no_armor_df
)
summary(linear.fit.level.noarmor)
##
## Call:
## lm(formula = level ~ average_hdi + fortitude + cleverness + power +
## kinen + nonkinen, data = no_armor_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.0819 -1.2764 -0.0060 0.9886 7.5673
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 8.481758 0.415248 20.43 <2e-16 ***
## average_hdi 0.027228 0.002052 13.27 <2e-16 ***
## fortitude -0.018734 0.001335 -14.04 <2e-16 ***
## cleverness 0.027003 0.002131 12.67 <2e-16 ***
## power 0.013156 0.001308 10.06 <2e-16 ***
## kinen 0.114275 0.008455 13.52 <2e-16 ***
## nonkinen 0.060657 0.006034 10.05 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.14 on 284 degrees of freedom
## Multiple R-squared: 0.9375, Adjusted R-squared: 0.9362
## F-statistic: 709.9 on 6 and 284 DF, p-value: < 2.2e-16
Which shows a statistically decent but clearly heteroscedastic fit.
The residuals are clearly not normal.
shapiro.test(rs.model.level.noarmor)
##
## Shapiro-Wilk normality test
##
## data: rs.model.level.noarmor
## W = 0.98048, p-value = 0.0005241
bptest(linear.fit.level.noarmor)
##
## studentized Breusch-Pagan test
##
## data: linear.fit.level.noarmor
## BP = 46.786, df = 6, p-value = 2.064e-08
For unarmored creature level, a few ideas:
The influence of fortitude shows a slight negative influence below \(fortitude = 500\), and a positive influence above.
See the influence plot,
This is reflected in the above linear model, where fortitude’s slope in the unarmored data is actually negative!
Recall the result of the linear, unarmored model:
##
## Call:
## lm(formula = level ~ average_hdi + fortitude + cleverness + power +
## kinen + nonkinen, data = no_armor_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.0819 -1.2764 -0.0060 0.9886 7.5673
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 8.481758 0.415248 20.43 <2e-16 ***
## average_hdi 0.027228 0.002052 13.27 <2e-16 ***
## fortitude -0.018734 0.001335 -14.04 <2e-16 ***
## cleverness 0.027003 0.002131 12.67 <2e-16 ***
## power 0.013156 0.001308 10.06 <2e-16 ***
## kinen 0.114275 0.008455 13.52 <2e-16 ***
## nonkinen 0.060657 0.006034 10.05 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.14 on 284 degrees of freedom
## Multiple R-squared: 0.9375, Adjusted R-squared: 0.9362
## F-statistic: 709.9 on 6 and 284 DF, p-value: < 2.2e-16
Can this be true? Other influence models, such as GBMs, also show the same thing. Still, it is not an intuitive finding. However, note that:
I’m unsure if this could be accurate, but it is compelling nonetheless.
Further investigation reveals that a logarithmic transformation of level provides better fit, suggesting the creature level formula may be multiplicative rather than additive.
Testing log(level) as the outcome with raw attributes:
model.log.unarmored <- lm(
log(level) ~ hardiness + fortitude + dexterity + intellect +
cleverness + power + courage +
kinen + nonkinen,
data = no_armor_df
)
model.log.armored <- lm(
log(level) ~ hardiness + fortitude + dexterity + intellect +
cleverness + power + courage +
kinen + nonkinen,
data = armor_df
)
## Log(level) model R<U+00B2> (original scale):
## Unarmored: 0.9435
## Armored: 0.9781
The armored model achieves R² ≈ 0.98, while unarmored achieves R² ≈ 0.94.
The most striking finding is that armored and unarmored creatures use fundamentally different formulas. Several coefficients flip sign between the two regimes:
| Attribute | Unarmored (per 100) | Armored (per 100) | Sign Flip? |
|---|---|---|---|
| hardiness | +6.8% | -0.4% | YES |
| fortitude | -11.7% | +5.5% | YES |
| dexterity | +3.9% | +4.2% | |
| intellect | +5.7% | +4.4% | |
| cleverness | +9.9% | +4.8% | |
| power | +9.1% | +5.0% | |
| courage | -2.1% | +0.6% | YES |
| kinen | +93.6% | +28.2% | |
| nonkinen | +24.3% | +23.2% |
Sign Flips:
Fortitude: The most dramatic flip. For unarmored creatures, fortitude has a negative effect on level (-11.7% per 100), while for armored creatures it has a positive effect (+5.5% per 100). This is statistically significant in both models.
Hardiness and Courage: Also flip sign, but the armored coefficients are not statistically significant (p > 0.4), so these may be noise.
Coefficient Magnitude Differences:
The kinen (kinetic/energy resist) coefficient is 3.4x larger for unarmored creatures (+94.9%) compared to armored (+28.2%). This makes intuitive sense: unarmored creatures rely on resists for survivability, so high resists dramatically increase their effective power level.
The evidence strongly supports the hypothesis that the game uses two completely different formulas for calculating creature level:
Unarmored Formula (more complex):
Armored Formula (simpler, tighter fit):
This explains several long-standing mysteries:
Unarmored Model:
##
## Call:
## lm(formula = log(level) ~ hardiness + fortitude + dexterity +
## intellect + cleverness + power + courage + kinen + nonkinen,
## data = no_armor_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.50286 -0.05851 0.00413 0.05289 0.37859
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.367e+00 2.966e-02 79.821 < 2e-16 ***
## hardiness 6.606e-04 1.098e-04 6.017 5.53e-09 ***
## fortitude -1.241e-03 8.905e-05 -13.935 < 2e-16 ***
## dexterity 3.872e-04 1.125e-04 3.443 0.000664 ***
## intellect 5.512e-04 1.359e-04 4.055 6.49e-05 ***
## cleverness 9.441e-04 1.766e-04 5.347 1.86e-07 ***
## power 8.709e-04 7.229e-05 12.048 < 2e-16 ***
## courage -2.079e-04 7.426e-05 -2.799 0.005476 **
## kinen 6.608e-03 4.750e-04 13.912 < 2e-16 ***
## nonkinen 2.176e-03 3.487e-04 6.242 1.59e-09 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1142 on 281 degrees of freedom
## Multiple R-squared: 0.9267, Adjusted R-squared: 0.9243
## F-statistic: 394.7 on 9 and 281 DF, p-value: < 2.2e-16
Armored Model:
##
## Call:
## lm(formula = log(level) ~ hardiness + fortitude + dexterity +
## intellect + cleverness + power + courage + kinen + nonkinen,
## data = armor_df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.14813 -0.02288 0.00325 0.03289 0.11306
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.552e+00 1.190e-01 21.442 < 2e-16 ***
## hardiness -4.490e-05 2.216e-04 -0.203 0.84004
## fortitude 5.310e-04 1.833e-04 2.898 0.00503 **
## dexterity 4.128e-04 9.930e-05 4.157 9.10e-05 ***
## intellect 4.290e-04 8.774e-05 4.889 6.34e-06 ***
## cleverness 4.689e-04 8.738e-05 5.366 1.02e-06 ***
## power 4.859e-04 6.688e-05 7.265 4.39e-10 ***
## courage 5.791e-05 7.499e-05 0.772 0.44258
## kinen 2.481e-03 5.068e-04 4.895 6.21e-06 ***
## nonkinen 2.090e-03 4.385e-04 4.766 1.01e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.05144 on 69 degrees of freedom
## Multiple R-squared: 0.9799, Adjusted R-squared: 0.9773
## F-statistic: 373.7 on 9 and 69 DF, p-value: < 2.2e-16
The log-level models above are useful for understanding the relative importance of attributes, but for re-implementing the game’s creature level calculation, we need linear formulas that can be directly coded.
We fit linear models predicting level (not log(level)) from raw attributes:
# Linear models for formula derivation
model.armor.linear <- lm(
level ~ hardiness + fortitude + dexterity + intellect +
cleverness + power + kinen + nonkinen,
data = armor_df
)
model.noarmor.linear <- lm(
level ~ hardiness + fortitude + dexterity + intellect +
cleverness + power + kinen + nonkinen,
data = no_armor_df
)
These are the “raw” coefficients from linear regression, rounded to 3 decimal places:
Armored Formula (fortitude >= 500):
## level = -22
## + 0.011 * hardiness
## + 0.057 * fortitude
## + 0.006 * dexterity
## + 0.012 * intellect
## + 0.024 * cleverness
## + 0.016 * power
## + 0.1 * kinen
## + 0.08 * nonkinen
Unarmored Formula (fortitude < 500):
## level = 8
## + 0.01 * hardiness
## + -0.018 * fortitude // NEGATIVE!
## + 0.006 * dexterity
## + 0.012 * intellect
## + 0.026 * cleverness
## + 0.013 * power
## + 0.11 * kinen
## + 0.06 * nonkinen
For actual implementation, we want coefficients that represent simple fractions (like 1/100, 1/50, 3/200) that a game developer might reasonably use. These “clean” coefficients are derived by rounding to values that correspond to nice fractions while minimizing loss of fit.
Armored Formula (Clean):
level = -23
+ 0.01 * hardiness // 1/100
+ 0.06 * fortitude // 3/50
+ 0.005 * dexterity // 1/200
+ 0.01 * intellect // 1/100
+ 0.025 * cleverness // 1/40
+ 0.015 * power // 3/200
+ 0.1 * kinen // 1/10
+ 0.08 * nonkinen // 2/25
Unarmored Formula (Clean):
level = 9
+ 0.01 * hardiness // 1/100
- 0.02 * fortitude // -1/50 (NEGATIVE!)
+ 0.01 * dexterity // 1/100
+ 0.01 * intellect // 1/100
+ 0.025 * cleverness // 1/40
+ 0.015 * power // 3/200
+ 0.12 * kinen // 3/25
+ 0.06 * nonkinen // 3/50
| Formula | R<U+00B2> | SD | Mean Residual | Max |Residual| |
|---|---|---|---|---|
| Armored (regression) | 0.9797 | 1.84 | -0.65 | 4.82 |
| Armored (clean) | 0.9777 | 1.87 | 0.84 | 4.93 |
| Unarmored (regression) | 0.9353 | 2.11 | 0.43 | 7.69 |
| Unarmored (clean) | 0.9147 | 2.15 | -1.22 | 7.67 |
The clean formulas maintain excellent fit:
| Term | Armored (reg) | Armored (clean) | Unarmored (reg) | Unarmored (clean) | |
|---|---|---|---|---|---|
| (Intercept) | intercept | -22.000 | -23.000 | 8.000 | 9.000 |
| hardiness | hardiness | 0.011 | 0.010 | 0.010 | 0.010 |
| fortitude | fortitude | 0.057 | 0.060 | -0.018 | -0.020 |
| dexterity | dexterity | 0.006 | 0.005 | 0.006 | 0.010 |
| intellect | intellect | 0.012 | 0.010 | 0.012 | 0.010 |
| cleverness | cleverness | 0.024 | 0.025 | 0.026 | 0.025 |
| power | power | 0.016 | 0.015 | 0.013 | 0.015 |
| kinen | kinen | 0.100 | 0.100 | 0.110 | 0.120 |
| nonkinen | nonkinen | 0.080 | 0.080 | 0.060 | 0.060 |
The following pseudocode can be used to implement creature level calculation:
int calculateCreatureLevel(Creature* c) {
// Compute resist averages
float kinen = (c->kinetic + c->energy) / 2.0f;
float nonkinen = (c->blast + c->heat + c->cold +
c->electricity + c->acid + c->stun) / 6.0f;
float level;
if (c->fortitude >= 500) {
// ARMORED FORMULA
level = -23.0f
+ 0.01f * c->hardiness
+ 0.06f * c->fortitude
+ 0.005f * c->dexterity
+ 0.01f * c->intellect
+ 0.025f * c->cleverness
+ 0.015f * c->power
+ 0.1f * kinen
+ 0.08f * nonkinen;
} else {
// UNARMORED FORMULA (note: fortitude is SUBTRACTED)
level = 9.0f
+ 0.01f * c->hardiness
- 0.02f * c->fortitude // NEGATIVE!
+ 0.01f * c->dexterity
+ 0.01f * c->intellect
+ 0.025f * c->cleverness
+ 0.015f * c->power
+ 0.12f * kinen
+ 0.06f * nonkinen;
}
// Clamp and round
if (level < 1.0f) level = 1.0f;
if (level > 75.0f) level = 75.0f;
return (int)(level + 0.5f); // round to nearest
}
fortitude >= 500, which corresponds to the point where creatures gain armor