Guides

Combat Systems with Damage formulas

Text-based combat systems require precise state synchronization between player input and automated combat rounds. This guide implements a pulse-based combat loop with damage normalization, skill interrupt handling, and overflow protection for DikuMUD-derived codebases or custom C implementations.

4-6 hours for initial implementation, plus 2 hours for balance testing10 steps
Combat Systems with Damage formulas illustration
Placeholder illustration shown while custom artwork is being produced.
1

Audit Combat Constants and Hooks

Locate damage calculation entry points in fight.c or equivalent combat module. Identify where weapon dice rolls, character level multipliers, and strength/dexterity stat bonuses converge. Document current integer types used for HP, mana, and damage calculations to assess overflow risk on 32-bit versus 64-bit architectures.

fight.c
/* Typical DikuMUD damage entry point signature */
int damage(CHAR_DATA *ch, CHAR_DATA *victim, int dam, int dt, int type);

/* Locate stat modifiers */
int str_app[26].todam;  /* Check array bounds */
int GET_DAMROLL(ch);    /* Verify macro expansion */

⚠ Common Pitfalls

  • Assuming 32-bit integers suffice for high-level damage calculations
  • Missing hidden modifiers in object affect fields that stack unexpectedly
2

Implement Normalized Damage Formula

Replace flat bonus damage with logarithmic scaling to prevent exponential growth at high levels. Structure the formula as: base_damage + (stat * level_coefficient) + dice_roll. Enforce hard caps at 32000 to prevent signed integer overflow on legacy 16-bit systems and intermediate calculation overflow.

fight.c
int calculate_damage(CHAR_DATA *ch, int base_dam) {
    int stat_bonus = GET_STR(ch) > 18 ? (GET_STR(ch) - 18) * 2 : 0;
    int level_mod = (GET_LEVEL(ch) * 3) / 2;
    long dam = base_dam + stat_bonus + level_mod;
    
    /* Overflow protection */
    if (dam > 32000) {
        log("Damage cap hit for %s", GET_NAME(ch));
        dam = 32000;
    }
    return (int)dam;
}

⚠ Common Pitfalls

  • Linear scaling causing end-game imbalance where high levels one-shot mobs
  • Integer division truncation errors when using small coefficients like 0.75
3

Build Combat Pulse Scheduler

Convert combat from event-driven to pulse-based timing. Assign each character a wait_state counter decrementing each game pulse (typically 4 times per second in comm.c). Combat rounds execute when wait_state reaches zero, then reset to PULSE_VIOLENCE (usually 12-24 pulses depending on desired attack speed).

comm.c
/* In update_handler() or pulse_update() */
for (ch = character_list; ch; ch = ch->next) {
    if (ch->wait_state > 0) {
        ch->wait_state--;
        continue;
    }
    
    if (FIGHTING(ch) && ch->wait_state == 0) {
        perform_violence(ch);
        ch->wait_state = PULSE_VIOLENCE;
    }
}

⚠ Common Pitfalls

  • Race conditions between flee commands and damage application causing desync
  • Desynchronization between player and mob wait states leading to double attacks
4

Add State Interrupt System

Implement CAN_FIGHT flag checks before damage application in perform_violence(). Stun, sleep, paralysis, and fear states must clear this flag. Flee attempts consume movement points proportional to encumbrance and set wait_state to prevent immediate re-engagement or chain fleeing.

fight.c
void perform_violence(CHAR_DATA *ch) {
    if (!CAN_FIGHT(ch)) {
        /* Handle stun/sleep states */
        if (IS_STUNNED(ch))
            send_to_char("You are too stunned to fight!\r\n", ch);
        return;
    }
    
    if (FIGHTING(ch))
        hit(ch, FIGHTING(ch), TYPE_UNDEFINED);
}

⚠ Common Pitfalls

  • Stun-lock loops from skill recalculation allowing permanent stun
  • Flee success rates not checking encumbrance or room exit flags
5

Create Combat Message Buffer

Queue combat messages in a ring buffer displayed at round end rather than per-hit. Prevents scroll spam during multi-opponent combat. Implement brief mode flag to suppress non-critical miss messages and show only significant hits or status changes.

fight.c
typedef struct combat_msg {
    char *msg;
    struct combat_msg *next;
} COMBAT_MSG;

void queue_combat_msg(CHAR_DATA *ch, char *msg) {
    COMBAT_MSG *new_msg = malloc(sizeof(COMBAT_MSG));
    new_msg->msg = strdup(msg);
    new_msg->next = ch->msg_queue;
    ch->msg_queue = new_msg;
}

⚠ Common Pitfalls

  • Memory leaks from unfreed message strings after combat ends
  • Out-of-order messages with area attacks hitting multiple targets simultaneously
6

Integrate Skill Command Hooks

Modify interpret() or command parser to check combat state before executing skills. Skills must verify WAIT_STATE is zero, check mana/move costs before deduction, and set custom wait delays. Interrupt auto-attack loops when skills are used to prevent skill-and-attack spam.

interpreter.c
/* In command interpreter */
if (IS_IN_COMBAT(ch) && cmd_info[cmd].combat_cmd) {
    if (ch->wait_state > 0) {
        send_to_char("You are still recovering from your last action.\r\n", ch);
        return;
    }
    
    if (GET_MANA(ch) < skill_cost) {
        send_to_char("You don't have enough mana.\r\n", ch);
        return;
    }
    
    ch->wait_state = skill_table[sn].beats;
}

⚠ Common Pitfalls

  • Skill spam bypassing combat rounds through alias macros
  • Mana cost checks after effect application allowing negative mana
7

Implement Overflow Protection

Add boundary checks before all arithmetic operations in damage calculations. If any intermediate value exceeds INT_MAX/2, cap and log the event. Check stat bonuses against defined MAX_STAT constants before applying multipliers to prevent underflow with negative resistances.

handler.c
#define SAFE_MULT(a, b, max) \
    (((a) > (max) / (b)) ? (max) : ((a) * (b)))

int apply_resistance(int dam, int resist) {
    /* Prevent underflow with negative resistances */
    if (resist < -100) resist = -100;
    if (resist > 100) resist = 100;
    
    return dam * (100 - resist) / 100;
}

⚠ Common Pitfalls

  • Signed integer overflow undefined behavior in C on multiplication
  • Underflow when negative resistances stack beyond -100% healing instead of damaging
8

Add Combat Analytics Logging

Write per-round damage data to external log file or SQL database. Track attacker level, defender level, damage dealt, skill used, and timestamp. Export to CSV for analysis in spreadsheet software to identify class imbalance or anomalous damage spikes from formula errors.

fight.c
void log_combat_data(CHAR_DATA *att, CHAR_DATA *def, int dam, int sn) {
    FILE *fp = fopen("combat.log", "a");
    if (!fp) return;
    
    fprintf(fp, "%ld|%s|%d|%s|%d|%d|%s\n",
        time(NULL), GET_NAME(att), GET_LEVEL(att),
        GET_NAME(def), GET_LEVEL(def), dam,
        sn > 0 ? skill_table[sn].name : "auto");
    fclose(fp);
}

⚠ Common Pitfalls

  • Disk I/O blocking game loop during heavy combat
  • Privacy concerns with logging player versus player combat data
9

Build OLC Combat Editor Integration

If using OLC (Online Creation), extend to edit combat constants in real-time. Export damage formula coefficients, pulse timings, and stat caps to editable tables. Implement hot-reload for constants without game reboot using a signal handler or in-game command.

olc.c
/* OLC menu for combat values */
OLC_MODE(d) = OEDIT_COMBAT_CONSTANTS;
send_to_char("1) Base damage multiplier: %f\r\n", d->olc_obj->dam_mult);
send_to_char("2) Pulse violence timing: %d\r\n", PULSE_VIOLENCE);
send_to_char("Enter option to edit or 0 to save: ", d->character);

⚠ Common Pitfalls

  • Buffer overflows from long formula strings in OLC input
  • Concurrent editing by multiple builders corrupting combat state
10

Stress Test Edge Cases

Create automated test scenarios: level 100 versus level 1 (should cap damage to prevent one-shots), max strength with +100 gear (check overflow handling), fifty mobs attacking one player (message buffer limits), continuous flee attempts (movement drain validation), and stun-lock chains.

⚠ Common Pitfalls

  • Ignoring compiler warnings on implicit int casts in damage formulas
  • Testing only solo combat scenarios and missing group aggro edge cases

What you built

Deploy changes to a test port before production. Monitor combat logs for 48 hours to detect anomalous damage spikes. Keep original combat functions in backup files for rapid rollback if balance breaks. Document custom formula coefficients in README.combat for future maintainers.