diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-13 04:58:34 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-13 04:58:34 +0800 |
| commit | e30ee839142c2ed1466dd0991470c5528e717fcc (patch) | |
| tree | c52c7e91cf6ecb1c5aa899fc55c2c58cc3e41705 | |
| parent | c3e0f2d163d5dc0b2a9e19b72d85c1611ef95270 (diff) | |
add battle module: stats, states, skills, formulas, turn order
| -rw-r--r-- | kon/konbattle.c | 1089 | ||||
| -rw-r--r-- | kon/konbattle.h | 200 |
2 files changed, 1289 insertions, 0 deletions
diff --git a/kon/konbattle.c b/kon/konbattle.c new file mode 100644 index 0000000..e0ed33d --- /dev/null +++ b/kon/konbattle.c @@ -0,0 +1,1089 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konbattle.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "konbattle.h" + +#include <math.h> +#include <string.h> + +/*** random numbers ***/ + +static uint32_t next(kon_battle_t *bt) +{ + uint32_t x = bt->seed; + + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + bt->seed = x; + return x; +} + +/* 0 up to but not including n */ +static int below(kon_battle_t *bt, int n) +{ + return n > 1 ? (int)(next(bt) % (uint32_t)n) : 0; +} + +static int percent(kon_battle_t *bt, int chance) +{ + return below(bt, 100) < chance; +} + +/*** formulas ***/ + +enum { + F_NUM, F_A, F_B, F_VAR, F_NEG, F_NOT, + F_ADD, F_SUB, F_MUL, F_DIV, F_MOD, F_POW, F_LT, F_GT, F_LE, F_GE, F_EQ, F_NE, F_AND, F_OR, F_SELECT, + F_FLOOR, F_CEIL, F_ROUND, F_ABS, F_SQRT, F_MAX, F_MIN +}; + +#define FORMULA_STACK 32 +#define FORMULA_DEPTH 24 + +typedef struct parser { + kon_formula_t *f; + const char *s; + int at, depth, stack, ok; +} parser_t; + +static void fail(parser_t *p) +{ + if (p->ok) p->f->errorAt = p->at; + p->ok = 0; +} + +static void emit(parser_t *p, int op, double n, int pushes) +{ + if (!p->ok) return; + if (p->f->count >= (int)(sizeof p->f->ops / sizeof p->f->ops[0])) { + fail(p); + return; + } + p->f->ops[p->f->count].op = op; + p->f->ops[p->f->count].n = n; + p->f->count++; + p->stack += pushes; + if (p->stack > FORMULA_STACK || p->stack < 1) fail(p); +} + +static void space(parser_t *p) +{ + while (p->s[p->at] == ' ' || p->s[p->at] == '\t' || p->s[p->at] == '\n' || p->s[p->at] == '\r') p->at++; +} + +static int accept(parser_t *p, const char *token) +{ + size_t n = strlen(token); + + space(p); + if (strncmp(p->s + p->at, token, n) != 0) return 0; + p->at += (int)n; + return 1; +} + +static int isWord(int c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +} + +static int word(parser_t *p, char *out, int size) +{ + int n = 0; + + space(p); + while (isWord((unsigned char)p->s[p->at])) { + if (n + 1 < size) out[n++] = p->s[p->at]; + p->at++; + } + out[n] = '\0'; + return n; +} + +static void expression(parser_t *p); + +static int statNumber(const char *name) +{ + static const char *names[KON_STATS] = {"mhp", "mmp", "atk", "def", "mat", "mdf", "agi", "luk", "hp", "mp", "tp", "level"}; + int i; + + for (i = 0; i < KON_STATS; i++) { + if (strcmp(name, names[i]) == 0) return i; + } + return -1; +} + +static void primary(parser_t *p) +{ + char w[32]; + + space(p); + if (!p->ok) return; + if (p->depth++ > FORMULA_DEPTH) { + fail(p); + return; + } + if (accept(p, "(")) { + expression(p); + if (!accept(p, ")")) fail(p); + } else if ((p->s[p->at] >= '0' && p->s[p->at] <= '9') || p->s[p->at] == '.') { + double v = 0.0, scale = 1.0; + int digits = 0; + + while (p->s[p->at] >= '0' && p->s[p->at] <= '9') { + v = v * 10.0 + (double)(p->s[p->at++] - '0'); + digits++; + } + if (p->s[p->at] == '.') { + p->at++; + while (p->s[p->at] >= '0' && p->s[p->at] <= '9') { + scale *= 0.1; + v += (double)(p->s[p->at++] - '0') * scale; + digits++; + } + } + if (!digits) fail(p); + emit(p, F_NUM, v, 1); + } else if (word(p, w, sizeof w)) { + if ((strcmp(w, "a") == 0 || strcmp(w, "b") == 0) && accept(p, ".")) { + char stat[32]; + int n; + + if (!word(p, stat, sizeof stat) || (n = statNumber(stat)) < 0) fail(p); + else emit(p, w[0] == 'a' ? F_A : F_B, (double)n, 1); + } else if (strcmp(w, "v") == 0 && accept(p, "[")) { + expression(p); + if (!accept(p, "]")) fail(p); + emit(p, F_VAR, 0.0, 0); + } else if (strcmp(w, "Math") == 0 && accept(p, ".")) { + static const struct { const char *name; int op, args; } fns[] = { + {"floor", F_FLOOR, 1}, {"ceil", F_CEIL, 1}, {"round", F_ROUND, 1}, {"abs", F_ABS, 1}, {"sqrt", F_SQRT, 1}, + {"max", F_MAX, 2}, {"min", F_MIN, 2}, {"pow", F_POW, 2} + }; + size_t i; + int found = -1; + + if (!word(p, w, sizeof w)) fail(p); + for (i = 0; i < sizeof fns / sizeof fns[0]; i++) { + if (strcmp(w, fns[i].name) == 0) found = (int)i; + } + if (found < 0 || !accept(p, "(")) { + fail(p); + } else { + expression(p); + if (fns[found].args == 2) { + if (!accept(p, ",")) fail(p); + expression(p); + } + if (!accept(p, ")")) fail(p); + emit(p, fns[found].op, 0.0, fns[found].args == 2 ? -1 : 0); + } + } else { + fail(p); + } + } else { + fail(p); + } + p->depth--; +} + +static void unary(parser_t *p) +{ + space(p); + if (!p->ok) return; + if (p->depth++ > FORMULA_DEPTH) { + fail(p); + return; + } + if (accept(p, "-")) { + unary(p); + emit(p, F_NEG, 0.0, 0); + } else if (accept(p, "+")) { + unary(p); + } else if (p->s[p->at] == '!' && p->s[p->at + 1] != '=') { + p->at++; + unary(p); + emit(p, F_NOT, 0.0, 0); + } else { + primary(p); + } + p->depth--; +} + +/* ** goes right to left and binds tighter than the minus in front of it */ +static void power(parser_t *p) +{ + unary(p); + space(p); + if (p->s[p->at] == '*' && p->s[p->at + 1] == '*') { + p->at += 2; + if (p->depth++ > FORMULA_DEPTH) { + fail(p); + return; + } + power(p); + p->depth--; + emit(p, F_POW, 0.0, -1); + } +} + +static void product(parser_t *p) +{ + power(p); + for (;;) { + int op; + + space(p); + if (p->s[p->at] == '*' && p->s[p->at + 1] != '*') op = F_MUL; + else if (p->s[p->at] == '/') op = F_DIV; + else if (p->s[p->at] == '%') op = F_MOD; + else return; + p->at++; + power(p); + emit(p, op, 0.0, -1); + } +} + +static void sum(parser_t *p) +{ + product(p); + for (;;) { + int op; + + space(p); + if (p->s[p->at] == '+') op = F_ADD; + else if (p->s[p->at] == '-') op = F_SUB; + else return; + p->at++; + product(p); + emit(p, op, 0.0, -1); + } +} + +static void relation(parser_t *p) +{ + sum(p); + for (;;) { + int op; + + if (accept(p, "<=")) op = F_LE; + else if (accept(p, ">=")) op = F_GE; + else if (accept(p, "<")) op = F_LT; + else if (accept(p, ">")) op = F_GT; + else return; + sum(p); + emit(p, op, 0.0, -1); + } +} + +static void equality(parser_t *p) +{ + relation(p); + for (;;) { + int op; + + if (accept(p, "===") || accept(p, "==")) op = F_EQ; + else if (accept(p, "!==") || accept(p, "!=")) op = F_NE; + else return; + relation(p); + emit(p, op, 0.0, -1); + } +} + +static void conjunction(parser_t *p) +{ + equality(p); + while (accept(p, "&&")) { + equality(p); + emit(p, F_AND, 0.0, -1); + } +} + +static void disjunction(parser_t *p) +{ + conjunction(p); + while (accept(p, "||")) { + conjunction(p); + emit(p, F_OR, 0.0, -1); + } +} + +static void expression(parser_t *p) +{ + disjunction(p); + if (accept(p, "?")) { + if (p->depth++ > FORMULA_DEPTH) { + fail(p); + return; + } + expression(p); + if (!accept(p, ":")) fail(p); + expression(p); + p->depth--; + emit(p, F_SELECT, 0.0, -2); + } +} + +int kon_formulaCompile(kon_formula_t *f, const char *text) +{ + parser_t p; + + memset(f, 0, sizeof *f); + f->errorAt = -1; + if (!text) { + f->errorAt = 0; + return 0; + } + p.f = f; + p.s = text; + p.at = p.depth = p.stack = 0; + p.ok = 1; + expression(&p); + space(&p); + if (p.ok && text[p.at] != '\0') fail(&p); + if (p.ok && p.stack != 1) fail(&p); + if (!p.ok) { + f->count = 0; + return 0; + } + return 1; +} + +static double sane(double v) +{ + if (!(v == v)) return 0.0; + if (v > 1e12) return 1e12; + if (v < -1e12) return -1e12; + return v; +} + +double kon_formulaEval(const kon_formula_t *f, const kon_battle_t *battle, const kon_battler_t *a, const kon_battler_t *b) +{ + double st[FORMULA_STACK + 2]; + int sp = 0, i; + + if (f->count <= 0) return 0.0; + for (i = 0; i < f->count; i++) { + int op = f->ops[i].op; + double x, y; + + switch (op) { + case F_NUM: + st[sp++] = f->ops[i].n; + break; + case F_A: + case F_B: { + const kon_battler_t *who = op == F_A ? a : b; + + st[sp++] = who ? (double)kon_battlerStat(battle, who, (int)f->ops[i].n) : 0.0; + break; + } + case F_VAR: { + double n = st[sp - 1]; + int id = n >= 0.0 && n < 1e9 ? (int)n : -1; + + st[sp - 1] = battle->vars && id >= 0 && id < battle->varCount ? (double)battle->vars[id] : 0.0; + break; + } + case F_NEG: st[sp - 1] = -st[sp - 1]; break; + case F_NOT: st[sp - 1] = st[sp - 1] == 0.0 ? 1.0 : 0.0; break; + case F_FLOOR: st[sp - 1] = floor(st[sp - 1]); break; + case F_CEIL: st[sp - 1] = ceil(st[sp - 1]); break; + case F_ROUND: st[sp - 1] = floor(st[sp - 1] + 0.5); break; + case F_ABS: st[sp - 1] = fabs(st[sp - 1]); break; + case F_SQRT: st[sp - 1] = st[sp - 1] > 0.0 ? sqrt(st[sp - 1]) : 0.0; break; + case F_SELECT: + x = st[sp - 3] != 0.0 ? st[sp - 2] : st[sp - 1]; + sp -= 2; + st[sp - 1] = x; + break; + default: + y = st[--sp]; + x = st[sp - 1]; + switch (op) { + case F_ADD: x = x + y; break; + case F_SUB: x = x - y; break; + case F_MUL: x = x * y; break; + case F_DIV: x = y != 0.0 ? x / y : 0.0; break; + case F_MOD: x = y != 0.0 ? fmod(x, y) : 0.0; break; + case F_POW: x = pow(x, y); break; + case F_LT: x = x < y; break; + case F_GT: x = x > y; break; + case F_LE: x = x <= y; break; + case F_GE: x = x >= y; break; + case F_EQ: x = x == y; break; + case F_NE: x = x != y; break; + case F_AND: x = x != 0.0 && y != 0.0; break; + case F_OR: x = x != 0.0 || y != 0.0; break; + case F_MAX: x = x > y ? x : y; break; + case F_MIN: x = x < y ? x : y; break; + } + st[sp - 1] = x; + break; + } + st[sp - 1] = sane(st[sp - 1]); + } + return sp == 1 ? st[0] : 0.0; +} + +/*** battlers ***/ + +void kon_battlerInit(kon_battler_t *b, const char *name, int team, int level) +{ + kon_battler_t zero = {0}; + int i; + + *b = zero; + b->name = name; + b->team = team == KON_ENEMIES ? KON_ENEMIES : KON_PARTY; + b->level = level; + b->hit = 95; + b->pdr = b->mdr = 100; + for (i = 0; i < KON_ELEMENTS; i++) b->elementRate[i] = 100; +} + +int kon_battlerHasState(const kon_battler_t *b, int id) +{ + int i; + + for (i = 0; i < b->stateCount; i++) { + if (b->states[i].id == id) return 1; + } + return 0; +} + +static const kon_state_t *stateOf(const kon_battle_t *battle, int id) +{ + return id >= 0 && id < battle->stateCount && battle->states ? &battle->states[id] : NULL; +} + +int kon_battlerStat(const kon_battle_t *battle, const kon_battler_t *b, int stat) +{ + long value, change = 0; + int i; + + switch (stat) { + case KON_STAT_HP: return b->hp; + case KON_STAT_MP: return b->mp; + case KON_STAT_TP: return b->tp; + case KON_STAT_LEVEL: return b->level; + } + if (stat < 0 || stat >= KON_PARAMS) return 0; + for (i = 0; i < b->stateCount; i++) { + const kon_state_t *s = stateOf(battle, b->states[i].id); + + if (s) change += s->percent[stat]; + } + if (change < -100) change = -100; + if (change > 10000) change = 10000; + value = ((long)b->base[stat] + b->plus[stat]) * (100 + change) / 100; + if (value < (stat == KON_STAT_MHP ? 1 : 0)) value = stat == KON_STAT_MHP ? 1 : 0; + if (value > 99999999L) value = 99999999L; + return (int)value; +} + +int kon_battlerAlive(const kon_battle_t *battle, const kon_battler_t *b) +{ + int i; + + if (b->hp <= 0) return 0; + for (i = 0; i < b->stateCount; i++) { + const kon_state_t *s = stateOf(battle, b->states[i].id); + + if (s && s->death) return 0; + } + return 1; +} + +static void clampBattler(const kon_battle_t *battle, kon_battler_t *b) +{ + int mhp = kon_battlerStat(battle, b, KON_STAT_MHP), mmp = kon_battlerStat(battle, b, KON_STAT_MMP); + + if (b->hp > mhp) b->hp = mhp; + if (b->hp < 0) b->hp = 0; + if (b->mp > mmp) b->mp = mmp; + if (b->mp < 0) b->mp = 0; + if (b->tp > 100) b->tp = 100; + if (b->tp < 0) b->tp = 0; +} + +static int deathState(const kon_battle_t *battle) +{ + int i; + + for (i = 0; i < battle->stateCount; i++) { + if (battle->states[i].death) return i; + } + return -1; +} + +static int removeState(kon_battler_t *b, int id) +{ + int i; + + for (i = 0; i < b->stateCount; i++) { + if (b->states[i].id == id) { + b->states[i] = b->states[--b->stateCount]; + return 1; + } + } + return 0; +} + +static int addState(kon_battle_t *bt, kon_battler_t *b, int id) +{ + const kon_state_t *s = stateOf(bt, id); + int i, turns = 0; + + if (!s) return 0; + if (s->death) { + b->hp = 0; + b->stateCount = 0; + } else if (!kon_battlerAlive(bt, b)) { + return 0; + } + if (s->removeAt && s->turnsMax > 0) { + int lo = s->turnsMin > 0 ? s->turnsMin : 1; + + turns = s->turnsMax > lo ? lo + below(bt, s->turnsMax - lo + 1) : lo; + } + for (i = 0; i < b->stateCount; i++) { + if (b->states[i].id == id) { + b->states[i].turns = turns; + return 1; + } + } + if (b->stateCount >= KON_BATTLER_STATES) return 0; + b->states[b->stateCount].id = id; + b->states[b->stateCount].turns = turns; + b->stateCount++; + return 1; +} + +/*** skills ***/ + +int kon_skillCompile(kon_skill_t *skill) +{ + if (!skill->formulaText || !skill->formulaText[0]) { + memset(&skill->formula, 0, sizeof skill->formula); + skill->formula.errorAt = -1; + return 1; + } + return kon_formulaCompile(&skill->formula, skill->formulaText); +} + +/*** battle ***/ + +void kon_battleInit(kon_battle_t *bt, kon_battler_t *battlers, int count, const kon_state_t *states, int stateCount, uint32_t seed) +{ + kon_battle_t zero = {0}; + + *bt = zero; + bt->battlers = battlers; + bt->count = battlers && count > 0 ? (count > KON_BATTLE_MAX ? KON_BATTLE_MAX : count) : 0; + bt->states = states; + bt->stateCount = states && stateCount > 0 ? stateCount : 0; + bt->seed = seed ? seed : 0x9E3779B9u; + bt->escapeRatio = -1.0f; +} + +static int aliveNumber(const kon_battle_t *bt, int i) +{ + return i >= 0 && i < bt->count && kon_battlerAlive(bt, &bt->battlers[i]); +} + +/* the battlers that can be hit by a scope for a user. returns how many, written to out */ +static int friends(const kon_battle_t *bt, int user, int wantAlive, int *out) +{ + int i, n = 0; + + for (i = 0; i < bt->count; i++) { + if (bt->battlers[i].team == bt->battlers[user].team && aliveNumber(bt, i) == wantAlive) out[n++] = i; + } + return n; +} + +static int foes(const kon_battle_t *bt, int user, int *out) +{ + int i, n = 0; + + for (i = 0; i < bt->count; i++) { + if (bt->battlers[i].team != bt->battlers[user].team && aliveNumber(bt, i)) out[n++] = i; + } + return n; +} + +static int targets(kon_battle_t *bt, int user, const kon_skill_t *skill, int target, int *out) +{ + int list[KON_BATTLE_MAX], n, i, count = 0; + + if (bt->forced > 0 && (skill->scope == KON_SCOPE_ONE_ENEMY || skill->scope == KON_SCOPE_ONE_ALLY || skill->scope == KON_SCOPE_RANDOM_ENEMY)) { + out[0] = bt->forced - 1; + return 1; + } + switch (skill->scope) { + case KON_SCOPE_USER: + out[count++] = user; + break; + case KON_SCOPE_ALL_ENEMIES: + count = foes(bt, user, out); + break; + case KON_SCOPE_ALL_ALLIES: + count = friends(bt, user, 1, out); + break; + case KON_SCOPE_ALL_ALLIES_DEAD: + count = friends(bt, user, 0, out); + break; + case KON_SCOPE_ONE_ENEMY: + n = foes(bt, user, list); + if (n == 0) break; + if (target >= 0 && target < bt->count && bt->battlers[target].team != bt->battlers[user].team && aliveNumber(bt, target)) out[count++] = target; + else out[count++] = list[below(bt, n)]; + break; + case KON_SCOPE_RANDOM_ENEMY: + n = foes(bt, user, list); + for (i = 0; n > 0 && i < (skill->randomCount > 0 ? skill->randomCount : 1) && i < KON_BATTLE_MAX; i++) out[count++] = list[below(bt, n)]; + break; + case KON_SCOPE_ONE_ALLY: + n = friends(bt, user, 1, list); + if (n == 0) break; + if (target >= 0 && target < bt->count && bt->battlers[target].team == bt->battlers[user].team && aliveNumber(bt, target)) out[count++] = target; + else out[count++] = list[below(bt, n)]; + break; + case KON_SCOPE_ONE_ALLY_DEAD: + n = friends(bt, user, 0, list); + if (n == 0) break; + if (target >= 0 && target < bt->count && bt->battlers[target].team == bt->battlers[user].team && !aliveNumber(bt, target)) out[count++] = target; + else out[count++] = list[below(bt, n)]; + break; + default: + break; + } + return count; +} + +static void resultInit(kon_result_t *r, int user, int target) +{ + kon_result_t zero = {0}; + + *r = zero; + r->user = user; + r->target = target; +} + +static void noteAdded(kon_result_t *r, int id) +{ + if (r->addedCount < KON_SKILL_STATES) r->added[r->addedCount++] = id; +} + +static void noteRemoved(kon_result_t *r, int id) +{ + if (r->removedCount < KON_SKILL_STATES) r->removed[r->removedCount++] = id; +} + +static int stateNoted(kon_battle_t *bt, kon_battler_t *t, int id, kon_result_t *r) +{ + int had = kon_battlerHasState(t, id); + + if (addState(bt, t, id)) { + if (!had) noteAdded(r, id); + return 1; + } + return 0; +} + +/* death follows hp: dying clears the other states, coming back removes death */ +static void settle(kon_battle_t *bt, kon_battler_t *t, kon_result_t *r) +{ + int death = deathState(bt); + + clampBattler(bt, t); + if (death < 0) return; + if (t->hp <= 0 && !kon_battlerHasState(t, death)) { + stateNoted(bt, t, death, r); + r->died = 1; + } else if (t->hp > 0 && kon_battlerHasState(t, death)) { + removeState(t, death); + noteRemoved(r, death); + r->revived = 1; + } +} + +static void hit(kon_battle_t *bt, int user, const kon_skill_t *skill, int target, kon_result_t *r) +{ + kon_battler_t *u = &bt->battlers[user], *t = &bt->battlers[target]; + int success = skill->successRate > 0 ? skill->successRate : 100; + double value; + int i, dmg; + + resultInit(r, user, target); + + if (skill->hitType == KON_HIT_PHYSICAL) { + success = success * u->hit / 100; + if (!percent(bt, success)) { + r->missed = 1; + return; + } + if (percent(bt, t->eva)) { + r->evaded = 1; + return; + } + } else if (skill->hitType == KON_HIT_MAGICAL) { + if (!percent(bt, success)) { + r->missed = 1; + return; + } + if (percent(bt, t->mev)) { + r->evaded = 1; + return; + } + } else if (!percent(bt, success)) { + r->missed = 1; + return; + } + + if (skill->damageType != KON_DAMAGE_NONE) { + int recover = skill->damageType == KON_DAMAGE_HP_RECOVER || skill->damageType == KON_DAMAGE_MP_RECOVER; + int amp; + + value = kon_formulaEval(&skill->formula, bt, u, t); + if (skill->element > 0 && skill->element < KON_ELEMENTS) value = value * (double)t->elementRate[skill->element] / 100.0; + if (value > 0.0) { + if (skill->hitType == KON_HIT_PHYSICAL) value = value * (double)t->pdr / 100.0; + else if (skill->hitType == KON_HIT_MAGICAL) value = value * (double)t->mdr / 100.0; + } + if (recover) value = -value; + if (skill->critical && !recover && percent(bt, u->cri * (100 - t->cev) / 100)) { + value *= 3.0; + r->critical = 1; + } + if (value > 0.0 && t->guarding) value /= 2.0; + amp = (int)floor(fabs(value) * (double)skill->variance / 100.0); + if (amp > 0) value += (double)(below(bt, amp + 1) + below(bt, amp + 1) - amp); + dmg = (int)floor(value + 0.5); + if (dmg > 99999999) dmg = 99999999; + if (dmg < -99999999) dmg = -99999999; + + switch (skill->damageType) { + case KON_DAMAGE_HP: + case KON_DAMAGE_HP_RECOVER: + case KON_DAMAGE_HP_DRAIN: { + int before = t->hp; + + t->hp -= dmg; + clampBattler(bt, t); + r->hp = t->hp - before; + if (skill->damageType == KON_DAMAGE_HP_DRAIN) { + int ub = u->hp; + + u->hp += -r->hp; + clampBattler(bt, u); + r->userHp = u->hp - ub; + } + break; + } + default: { + int before = t->mp; + + t->mp -= dmg; + clampBattler(bt, t); + r->mp = t->mp - before; + if (skill->damageType == KON_DAMAGE_MP_DRAIN) { + int ub = u->mp; + + u->mp += -r->mp; + clampBattler(bt, u); + r->userMp = u->mp - ub; + } + break; + } + } + if (r->hp < 0 && t->hp > 0) { + for (i = t->stateCount - 1; i >= 0; i--) { + const kon_state_t *s = stateOf(bt, t->states[i].id); + + if (s && s->removeByDamage > 0 && percent(bt, s->removeByDamage)) { + noteRemoved(r, t->states[i].id); + removeState(t, t->states[i].id); + } + } + } + } + settle(bt, t, r); + + for (i = 0; i < skill->addStateCount && i < KON_SKILL_STATES; i++) { + if (percent(bt, skill->addState[i].chance)) { + if (stateNoted(bt, t, skill->addState[i].id, r) && t->hp <= 0) r->died = 1; + } + } + for (i = 0; i < skill->removeStateCount && i < KON_SKILL_STATES; i++) { + if (removeState(t, skill->removeState[i])) noteRemoved(r, skill->removeState[i]); + } + settle(bt, t, r); + u->tp += skill->tpGain; + clampBattler(bt, u); +} + +int kon_battleUse(kon_battle_t *bt, int user, const kon_skill_t *skill, int target, kon_result_t *out, int cap) +{ + int list[KON_BATTLE_MAX], n, i, rep, made = 0, repeats; + kon_battler_t *u; + + if (!bt || !skill || user < 0 || user >= bt->count || !aliveNumber(bt, user)) return -1; + u = &bt->battlers[user]; + if (u->mp < skill->mpCost || u->tp < skill->tpCost) return -1; + u->mp -= skill->mpCost; + u->tp -= skill->tpCost; + + n = targets(bt, user, skill, target, list); + repeats = skill->repeats > 0 ? skill->repeats : 1; + for (i = 0; i < n; i++) { + for (rep = 0; rep < repeats; rep++) { + kon_result_t scratch; + + hit(bt, user, skill, list[i], made < cap && out ? &out[made] : &scratch); + made++; + } + } + return made < cap ? made : (cap > 0 ? cap : 0); +} + +/*** turns ***/ + +int kon_battleQueue(kon_battle_t *bt, int kind, int battler, const kon_skill_t *skill, int target) +{ + kon_action_t *a; + + if (!bt || bt->queued >= KON_BATTLE_QUEUE || battler < 0 || battler >= bt->count) return 0; + if (kind != KON_ACT_SKILL && kind != KON_ACT_GUARD && kind != KON_ACT_ESCAPE) return 0; + if (kind == KON_ACT_SKILL && !skill) return 0; + a = &bt->queue[bt->queued++]; + a->kind = kind; + a->battler = battler; + a->skill = skill; + a->target = target; + a->speed = 0; + bt->sorted = 0; + return 1; +} + +static void sortQueue(kon_battle_t *bt) +{ + int i, j; + + for (i = 0; i < bt->queued; i++) { + kon_action_t *a = &bt->queue[i]; + int agi = kon_battlerStat(bt, &bt->battlers[a->battler], KON_STAT_AGI); + + a->speed = agi + below(bt, 5 + agi / 4); + if (a->kind == KON_ACT_GUARD) a->speed += 2000; + else if (a->kind == KON_ACT_SKILL) a->speed += a->skill->speed; + } + for (i = 1; i < bt->queued; i++) { + kon_action_t held = bt->queue[i]; + + for (j = i - 1; j >= 0 && bt->queue[j].speed < held.speed; j--) bt->queue[j + 1] = bt->queue[j]; + bt->queue[j + 1] = held; + } + bt->sorted = 1; + bt->next = 0; +} + +static int restriction(const kon_battle_t *bt, const kon_battler_t *b) +{ + int i; + + for (i = 0; i < b->stateCount; i++) { + const kon_state_t *s = stateOf(bt, b->states[i].id); + + if (s && s->restriction) return s->restriction; + } + return 0; +} + +static float agility(const kon_battle_t *bt, int team) +{ + long total = 0; + int i, n = 0; + + for (i = 0; i < bt->count; i++) { + if (bt->battlers[i].team == team && kon_battlerAlive(bt, &bt->battlers[i])) { + total += kon_battlerStat(bt, &bt->battlers[i], KON_STAT_AGI); + n++; + } + } + return n ? (float)total / (float)n : 0.0f; +} + +static int escape(kon_battle_t *bt, int user, kon_result_t *out, int cap) +{ + kon_result_t r; + float party = agility(bt, KON_PARTY), enemies = agility(bt, KON_ENEMIES); + + if (bt->escapeRatio < 0.0f) bt->escapeRatio = enemies > 0.0f ? 0.5f * party / enemies : 1.0f; + resultInit(&r, user, -1); + if ((float)below(bt, 10000) < bt->escapeRatio * 10000.0f) { + r.escape = 2; + bt->escaped = 1; + } else { + r.escape = 1; + bt->escapeRatio += 0.1f; + } + if (cap > 0 && out) out[0] = r; + return cap > 0 ? 1 : 0; +} + +static void afterAction(kon_battle_t *bt, int user) +{ + kon_battler_t *b = &bt->battlers[user]; + int i; + + for (i = b->stateCount - 1; i >= 0; i--) { + const kon_state_t *s = stateOf(bt, b->states[i].id); + + if (s && s->removeAt == 1 && b->states[i].turns > 0 && --b->states[i].turns == 0) removeState(b, b->states[i].id); + } +} + +int kon_battleNext(kon_battle_t *bt, kon_result_t *out, int cap) +{ + if (!bt) return -1; + if (!bt->sorted) sortQueue(bt); + while (bt->next < bt->queued) { + kon_action_t a = bt->queue[bt->next++]; + kon_battler_t *u = &bt->battlers[a.battler]; + int n = 0, r; + + if (!kon_battlerAlive(bt, u)) continue; + r = restriction(bt, u); + if (r == 4) continue; + if (r >= 1 && r <= 3 && a.kind != KON_ACT_GUARD) { + int pool[KON_BATTLE_MAX], count = 0, i; + + for (i = 0; i < bt->count; i++) { + int same = bt->battlers[i].team == u->team; + + if (!aliveNumber(bt, i)) continue; + if (r == 2 || (r == 1 && !same) || (r == 3 && same)) pool[count++] = i; + } + if (!bt->attack || count == 0) continue; + a.kind = KON_ACT_SKILL; + a.skill = bt->attack; + a.target = pool[below(bt, count)]; + } + bt->last = a; + if (a.kind == KON_ACT_GUARD) { + u->guarding = 1; + } else if (a.kind == KON_ACT_ESCAPE) { + n = escape(bt, a.battler, out, cap); + } else { + bt->forced = r >= 1 && r <= 3 ? a.target + 1 : 0; + n = kon_battleUse(bt, a.battler, a.skill, a.target, out, cap); + bt->forced = 0; + if (n < 0) n = 0; + } + afterAction(bt, a.battler); + return n; + } + return -1; +} + +int kon_battleEndTurn(kon_battle_t *bt, kon_result_t *out, int cap) +{ + int i, made = 0; + + if (!bt) return 0; + for (i = 0; i < bt->count; i++) { + kon_battler_t *b = &bt->battlers[i]; + kon_result_t r; + int mhp, mmp, j; + + b->guarding = 0; + if (!kon_battlerAlive(bt, b)) continue; + resultInit(&r, i, i); + mhp = kon_battlerStat(bt, b, KON_STAT_MHP); + mmp = kon_battlerStat(bt, b, KON_STAT_MMP); + { + int hp = b->hp, mp = b->mp; + + b->hp += (int)((long)mhp * b->hrg / 100); + b->mp += (int)((long)mmp * b->mrg / 100); + b->tp += b->trg; + clampBattler(bt, b); + r.hp = b->hp - hp; + r.mp = b->mp - mp; + } + for (j = b->stateCount - 1; j >= 0; j--) { + const kon_state_t *s = stateOf(bt, b->states[j].id); + + if (s && s->removeAt == 2 && b->states[j].turns > 0 && --b->states[j].turns == 0) { + noteRemoved(&r, b->states[j].id); + removeState(b, b->states[j].id); + } + } + settle(bt, b, &r); + if (r.hp || r.mp || r.removedCount || r.died) { + if (made < cap && out) out[made] = r; + made++; + } + } + bt->turn++; + bt->queued = bt->next = bt->sorted = 0; + return made < cap ? made : (cap > 0 ? cap : 0); +} + +void kon_battleEnd(kon_battle_t *bt) +{ + int i, j; + + if (!bt) return; + for (i = 0; i < bt->count; i++) { + kon_battler_t *b = &bt->battlers[i]; + + b->guarding = 0; + for (j = b->stateCount - 1; j >= 0; j--) { + const kon_state_t *s = stateOf(bt, b->states[j].id); + + if (s && s->removeAtBattleEnd && !s->death) removeState(b, b->states[j].id); + } + clampBattler(bt, b); + } + bt->queued = bt->next = bt->sorted = 0; +} + +kon_outcome_t kon_battleOutcome(const kon_battle_t *bt) +{ + int i, party = 0, enemies = 0; + + if (bt->escaped) return KON_BATTLE_ESCAPED; + for (i = 0; i < bt->count; i++) { + if (!kon_battlerAlive(bt, &bt->battlers[i])) continue; + if (bt->battlers[i].team == KON_PARTY) party++; + else enemies++; + } + if (party == 0) return KON_BATTLE_DEFEAT; + if (enemies == 0) return KON_BATTLE_VICTORY; + return KON_BATTLE_ONGOING; +} diff --git a/kon/konbattle.h b/kon/konbattle.h new file mode 100644 index 0000000..740aefc --- /dev/null +++ b/kon/konbattle.h @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konbattle.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KONBATTLE_H +#define KONBATTLE_H + +#include <stddef.h> +#include <stdint.h> + +/*** definition ***/ + +/* turn based fights with the rules of the common RPG makers: stats, states, skills with a damage formula, hit and +critical rolls, element rates, turn order by agility and escaping. nothing here draws or reads input, the program +asks for actions and shows the results. everything lives in tables you own */ + +enum { + KON_STAT_MHP, KON_STAT_MMP, KON_STAT_ATK, KON_STAT_DEF, KON_STAT_MAT, KON_STAT_MDF, KON_STAT_AGI, KON_STAT_LUK, /* the first eight can be changed by states */ + KON_STAT_HP, KON_STAT_MP, KON_STAT_TP, KON_STAT_LEVEL, + KON_STATS +}; + +#define KON_PARAMS 8 +#define KON_BATTLER_STATES 8 +#define KON_ELEMENTS 16 +#define KON_SKILL_STATES 4 +#define KON_BATTLE_QUEUE 32 +#define KON_BATTLE_MAX 32 + +enum { KON_PARTY, KON_ENEMIES }; + +typedef struct kon_battler { + const char *name; + int team; + int level; + int base[KON_PARAMS], plus[KON_PARAMS]; /* what it is, and what equipment adds */ + int hp, mp, tp; + int hit, eva, cri, cev, mev, mrf; /* percents */ + int pdr, mdr; /* damage taken from physical and magical hits, percent */ + int hrg, mrg, trg; /* per turn, percent of the most, tp is points */ + int elementRate[KON_ELEMENTS]; /* percent, the element numbers are yours, 0 is no element */ + struct { int id; int turns; } states[KON_BATTLER_STATES]; /* turns is 0 for one that does not wear off */ + int stateCount; + int guarding; +} kon_battler_t; + +/* death is the state that is on while hp is 0. restriction: 0 nothing, 1 attacks an enemy, 2 attacks anyone, +3 attacks a friend, 4 cannot act. percent changes the first eight stats, -50 is half and 100 is double */ +typedef struct kon_state { + const char *name; + int death; + int restriction; + int turnsMin, turnsMax; /* both 0 for until it is removed */ + int removeAt; /* 0 never, 1 after its owner's action, 2 at the end of the turn */ + int removeAtBattleEnd; + int removeByDamage; /* percent chance when hit */ + int percent[KON_PARAMS]; +} kon_state_t; + +/* the numbers here are what the stat is worth (a.atk) and are never negative */ +typedef struct kon_formula { + struct { int op; double n; } ops[96]; + int count; + int errorAt; /* where it went wrong, or -1 */ +} kon_formula_t; + +typedef enum { + KON_SCOPE_NONE, KON_SCOPE_ONE_ENEMY, KON_SCOPE_ALL_ENEMIES, KON_SCOPE_RANDOM_ENEMY, KON_SCOPE_ONE_ALLY, KON_SCOPE_ALL_ALLIES, + KON_SCOPE_ONE_ALLY_DEAD, KON_SCOPE_ALL_ALLIES_DEAD, KON_SCOPE_USER +} kon_scope_t; + +typedef enum { + KON_DAMAGE_NONE, KON_DAMAGE_HP, KON_DAMAGE_MP, KON_DAMAGE_HP_RECOVER, KON_DAMAGE_MP_RECOVER, KON_DAMAGE_HP_DRAIN, KON_DAMAGE_MP_DRAIN +} kon_damageType_t; + +typedef enum { KON_HIT_CERTAIN, KON_HIT_PHYSICAL, KON_HIT_MAGICAL } kon_hitType_t; + +/* skills and items are the same thing here. a zero in successRate, repeats or randomCount means 100, 1 and 1, +so a table can leave them out */ +typedef struct kon_skill { + const char *name; + int mpCost, tpCost, tpGain; + kon_scope_t scope; + int randomCount; + int speed; + int successRate; + int repeats; + kon_hitType_t hitType; + kon_damageType_t damageType; + int element; + const char *formulaText; /* like "a.atk * 4 - b.def * 2", a is the user and b the target. see kon_skillCompile */ + kon_formula_t formula; + int variance; /* percent */ + int critical; /* can it be a critical hit */ + struct { int id; int chance; } addState[KON_SKILL_STATES]; + int addStateCount; + int removeState[KON_SKILL_STATES]; + int removeStateCount; +} kon_skill_t; + +enum { KON_ACT_NONE, KON_ACT_SKILL, KON_ACT_GUARD, KON_ACT_ESCAPE }; + +typedef struct kon_action { + int kind; + int battler; + const kon_skill_t *skill; + int target; /* a battler number, ignored by the scopes that pick themselves */ + int speed; +} kon_action_t; + +/* what happened to one target, for the program to show */ +typedef struct kon_result { + int user, target; + int escape; /* 0 not an escape, 1 failed, 2 got away */ + int missed, evaded, critical; + int hp, mp; /* change to the target, a hit is negative */ + int userHp, userMp; /* change to the user, from draining */ + int died, revived; + int added[KON_SKILL_STATES], addedCount; + int removed[KON_SKILL_STATES], removedCount; +} kon_result_t; + +typedef enum { KON_BATTLE_ONGOING, KON_BATTLE_VICTORY, KON_BATTLE_DEFEAT, KON_BATTLE_ESCAPED } kon_outcome_t; + +typedef struct kon_battle { + kon_battler_t *battlers; + int count; + const kon_state_t *states; + int stateCount; + const kon_skill_t *attack; /* what a confused battler does */ + int32_t *vars; /* for v[n] in formulas, can be a script's variables */ + int varCount; + + kon_action_t queue[KON_BATTLE_QUEUE]; + kon_action_t last; /* what kon_battleNext just did, with the skill it really used */ + int queued, next, sorted; + int turn; + float escapeRatio; + int escaped; + uint32_t seed; + int forced; /* private: the battler a confused action must hit, plus one */ +} kon_battle_t; + +/*** formulas ***/ + +/* a stat of the user is a.atk, of the target b.def, v[3] is a variable. + - * / % ** < > <= >= == != && || ! ?: parentheses and +Math.max min pow floor ceil round abs sqrt. 0 and errorAt set if it is something else */ +int kon_formulaCompile(kon_formula_t *f, const char *text); +double kon_formulaEval(const kon_formula_t *f, const kon_battle_t *battle, const kon_battler_t *a, const kon_battler_t *b); + +/*** battlers ***/ + +/* the usual start: 95 hit, 100 element rates, 100 damage rates */ +void kon_battlerInit(kon_battler_t *b, const char *name, int team, int level); +int kon_battlerStat(const kon_battle_t *battle, const kon_battler_t *b, int stat); +int kon_battlerAlive(const kon_battle_t *battle, const kon_battler_t *b); +int kon_battlerHasState(const kon_battler_t *b, int id); + +/*** skills ***/ + +int kon_skillCompile(kon_skill_t *skill); /* 0 if the formula is bad, it then does nothing */ + +/*** battle ***/ + +void kon_battleInit(kon_battle_t *bt, kon_battler_t *battlers, int count, const kon_state_t *states, int stateCount, uint32_t seed); + +/* a whole turn is: queue what everyone does, call kon_battleNext until it is -1 showing each result, then kon_battleEndTurn */ +int kon_battleQueue(kon_battle_t *bt, int kind, int battler, const kon_skill_t *skill, int target); +/* does the next action, in order of speed. gives how many results it wrote (0 for a guard or an action that could not happen) or -1 when the turn's actions are used up */ +int kon_battleNext(kon_battle_t *bt, kon_result_t *out, int cap); +/* regeneration and states wearing off, one result for each battler it touched */ +int kon_battleEndTurn(kon_battle_t *bt, kon_result_t *out, int cap); +void kon_battleEnd(kon_battle_t *bt); +kon_outcome_t kon_battleOutcome(const kon_battle_t *bt); + +/* one skill right now, the other pieces use this. -1 if it cannot be paid for */ +int kon_battleUse(kon_battle_t *bt, int user, const kon_skill_t *skill, int target, kon_result_t *out, int cap); + +#endif /* KONBATTLE_H */ |
