Data

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):

Feature Engineering

We are going to make some assumptions about the model based on our domain knowledge of the game, and to avoid over-fitting.

  1. Hardiness, Dexterity, and Intellect have the same weights against level

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.

  1. Kinetic and Energy resists have the same weights against level

We’re combining these two because they are uniquely capped at 60%.

  1. The other resists have the same weight against level

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

  1. 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.

  2. kinen – mean of kinetic and energy, for the same reasons.

  3. 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

Armored Creature Levels

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):

  1. The residuals are +/- 4 levels
  2. The residuals have median 0.19 levels.
  3. The residuals are normally distributed
  4. The residuals are homoscedastic

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.

Unarmored Creature Levels

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

Summary and areas for future investigation

  1. Using segment analysis, there is statistical evidence that armor is a change point in the model for creature level
  2. An ordinary linear regression fits armored creature levels remarkably well
  3. Unarmored creature levels are not so simple…

For unarmored creature level, a few ideas:

  1. It could be that lower level are more sensitive to rounding, giving the heteroscedastic results
  2. Creature level may be calculated from “final” attributes (such as health, action, mind), which themselves are rounded, which further causes sensitivity at lower creature levels and attributes, where rounding is proportionally more significant
  3. There may be more change points in the unarmored data set

The Strange Case of Fortitude

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:

  1. It would explain the existence of high-fortitude, unarmored CL 10s with lots of vulnerabilities – maximizing fortitude to be as close to 499 as possible would minimize its contribution to creature level!
  2. It would explain the question of how many levels armor adds – some players said 10, others said 6. In this view, obtaining armor does not strictly add a constant creature level; it rolls you into a different segment of the linear model, where the influence of fortitude becomes sharply positive
  3. It would also explain why change point detection was so adamant about a change point existing at \(fortitude = 500\) – it’s not that armor adds a constant level, it’s that parameters of the level calculation change entirely!

I’m unsure if this could be accurate, but it is compelling nonetheless.

Logarithmic Model Analysis

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
)

Model Fit Comparison

## 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.

Coefficient Comparison: The Two Formulas

The most striking finding is that armored and unarmored creatures use fundamentally different formulas. Several coefficients flip sign between the two regimes:

Percentage effect on level per 100 attribute points
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%

Key Findings

Sign Flips:

  1. 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.

  2. 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.

Visualization

Interpretation

The evidence strongly supports the hypothesis that the game uses two completely different formulas for calculating creature level:

Unarmored Formula (more complex):

  • Base level ≈ 10.6
  • Fortitude lowers level (bio-engineers could maximize fortitude to ~499 while keeping level low)
  • Kinetic/Energy resists have huge impact on level
  • Offensive attributes (cleverness, power) have larger coefficients

Armored Formula (simpler, tighter fit):

  • Base level ≈ 12.8
  • Fortitude raises level (as intuitively expected)
  • Resists have moderate impact
  • More balanced coefficients across attributes

This explains several long-standing mysteries:

  1. High-fortitude, low-level unarmored creatures existed because maximizing fortitude actually minimized its contribution to level
  2. Players disagreed on “how many levels armor adds” because armor doesn’t add a constant—it changes the entire formula
  3. The segmentation analysis found a breakpoint at fortitude=500 because crossing that threshold doesn’t just add armor, it switches which formula the game uses

Model Summaries

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

Deriving Implementable Formulas

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.

Linear Model Derivation

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
)

Regression-Derived Coefficients

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

Clean Game-Dev Friendly Coefficients

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 Validation

Formula Validation Metrics
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:

  • Armored: R² ≈ 0.98, residuals typically within ±2 levels
  • Unarmored: R² ≈ 0.91, residuals typically within ±3 levels

Coefficient Comparison Table

Coefficient Comparison: Regression vs Clean Values
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

Visualization of Clean Formulas

Final Implementation

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
}

Key Implementation Notes

  1. Armor threshold: The switch point is fortitude >= 500, which corresponds to the point where creatures gain armor
  2. Fortitude sign flip: The most dramatic finding—fortitude has opposite effects in the two formulas
  3. kinen vs nonkinen: Kinetic/Energy resists (capped at 60%) are weighted differently than other resists
  4. Residual sources: Remaining error is likely due to rounding in the original game’s attribute calculations