Feast 2.0!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

310 lines
9.7 KiB

  1. import { CombatTest, Stat, Vigor, Stats, StatToVigor } from '../combat'
  2. import { Creature } from "../creature"
  3. import { LogEntry, LogLines, PropElem, LogLine, nilLog } from '../interface'
  4. import { Verb } from '../language'
  5. function logistic (x0: number, L: number, k: number): (x: number) => number {
  6. return (x: number) => {
  7. return L / (1 + Math.exp(-k * (x - x0)))
  8. }
  9. }
  10. // TODO this will need to be able to return a LogEntry at some point
  11. abstract class RandomTest implements CombatTest {
  12. constructor (public fail: (user: Creature, target: Creature) => LogEntry) {
  13. }
  14. test (user: Creature, target: Creature): boolean {
  15. const userFail = user.effects.map(effect => effect.failTest(user, target))
  16. if (userFail.some(result => result.failed)) {
  17. return false
  18. }
  19. const targetFail = target.effects.map(effect => effect.failTest(target, user))
  20. if (targetFail.some(result => result.failed)) {
  21. return true
  22. }
  23. return Math.random() < this.odds(user, target)
  24. }
  25. abstract odds(user: Creature, target: Creature): number
  26. abstract explain(user: Creature, target: Creature): LogEntry
  27. }
  28. export enum TestCategory {
  29. Attack = "Attack",
  30. Vore = "Vore"
  31. }
  32. export class OpposedStatTest extends RandomTest {
  33. private f: (x: number) => number
  34. private k = 0.1
  35. // how much a stat can be reduced by its corresponding vigor being low
  36. private maxStatVigorPenalty = 0.5
  37. // how much the total score can be reduced by each vigor being low
  38. private maxTotalVigorPenalty = 0.1
  39. constructor (
  40. public readonly userStats: Partial<Stats>,
  41. public readonly targetStats: Partial<Stats>,
  42. fail: (user: Creature, target: Creature) => LogEntry,
  43. public category: TestCategory,
  44. private bias = 0
  45. ) {
  46. super(fail)
  47. this.f = logistic(0, 1, this.k)
  48. }
  49. odds (user: Creature, target: Creature): number {
  50. const userScore = this.getScore(user, this.userStats)
  51. const targetScore = this.getScore(target, this.targetStats)
  52. return this.f(userScore - targetScore + this.bias)
  53. }
  54. explain (user: Creature, target: Creature): LogEntry {
  55. return new LogLines(
  56. new LogLine(
  57. `Pits `,
  58. ...Object.entries(this.userStats).map(([stat, frac]) => {
  59. if (frac !== undefined) {
  60. return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
  61. } else {
  62. return nilLog
  63. }
  64. }),
  65. ` from ${user.name.possessive} stats against `,
  66. ...Object.entries(this.targetStats).map(([stat, frac]) => {
  67. if (frac !== undefined) {
  68. return new LogLine(`${(frac * 100).toFixed(0)}% `, new PropElem(stat as Stat))
  69. } else {
  70. return nilLog
  71. }
  72. }),
  73. ` from ${target.name.possessive} stats.`
  74. ),
  75. new LogLine(
  76. `${user.name.capital}: ${this.getScore(user, this.userStats)} // ${this.getScore(target, this.targetStats)} :${target.name.capital}`
  77. ),
  78. new LogLine(
  79. `${user.name.capital} ${user.name.conjugate(new Verb("have", "has"))} a ${(this.odds(user, target) * 100).toFixed(0)}% chance of winning this test.`
  80. )
  81. )
  82. }
  83. private getScore (actor: Creature, parts: Partial<Stats>): number {
  84. const total = Object.entries(parts).reduce((total: number, [stat, frac]) => {
  85. let value = actor.stats[stat as Stat] * (frac === undefined ? 0 : frac)
  86. const vigor = StatToVigor[stat as Stat]
  87. value = value * (1 - this.maxStatVigorPenalty) + value * this.maxStatVigorPenalty * actor.vigors[vigor] / actor.maxVigors[vigor]
  88. return total + value
  89. }, 0)
  90. const modifiedTotal = Object.keys(Vigor).reduce(
  91. (total, vigor) => {
  92. return total * (1 - this.maxStatVigorPenalty) + total * actor.vigors[vigor as Vigor] / actor.maxVigors[vigor as Vigor]
  93. },
  94. total
  95. )
  96. return modifiedTotal
  97. }
  98. }
  99. export class StatVigorSizeTest extends RandomTest {
  100. private f: (x: number) => number
  101. private k = 0.1
  102. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  103. super(fail)
  104. this.f = logistic(0, 1, this.k)
  105. }
  106. odds (user: Creature, target: Creature): number {
  107. let userPercent = 1
  108. let targetPercent = 1
  109. Object.keys(Vigor).forEach(key => {
  110. userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
  111. targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
  112. userPercent = Math.max(0, userPercent)
  113. targetPercent = Math.max(0, targetPercent)
  114. })
  115. if (userPercent === 0) {
  116. targetPercent *= 4
  117. }
  118. if (targetPercent === 0) {
  119. userPercent *= 4
  120. }
  121. const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
  122. return this.f(this.bias + sizeOffset * 5 + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  123. }
  124. explain (user: Creature, target: Creature): LogEntry {
  125. let result: LogEntry
  126. let userPercent = 1
  127. let targetPercent = 1
  128. Object.keys(Vigor).forEach(key => {
  129. userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
  130. targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
  131. userPercent = Math.max(0, userPercent)
  132. targetPercent = Math.max(0, targetPercent)
  133. })
  134. if (userPercent === 0) {
  135. targetPercent *= 4
  136. }
  137. if (targetPercent === 0) {
  138. userPercent *= 4
  139. }
  140. const sizeOffset = Math.log2(user.voreStats.Mass / target.voreStats.Mass)
  141. const userMod = user.stats[this.stat] * userPercent
  142. const targetMod = target.stats[this.stat] * targetPercent
  143. const delta = userMod - targetMod + sizeOffset * 5
  144. if (delta === 0) {
  145. result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
  146. } else if (delta < 0) {
  147. result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
  148. } else {
  149. result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
  150. }
  151. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  152. return result
  153. }
  154. }
  155. export class StatVigorTest extends RandomTest {
  156. private f: (x: number) => number
  157. private k = 0.1
  158. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  159. super(fail)
  160. this.f = logistic(0, 1, this.k)
  161. }
  162. odds (user: Creature, target: Creature): number {
  163. let userPercent = 1
  164. let targetPercent = 1
  165. Object.keys(Vigor).forEach(key => {
  166. userPercent *= user.vigors[key as Vigor] / Math.max(1, user.maxVigors[key as Vigor])
  167. targetPercent *= target.vigors[key as Vigor] / Math.max(1, target.maxVigors[key as Vigor])
  168. userPercent = Math.max(0, userPercent)
  169. targetPercent = Math.max(0, targetPercent)
  170. })
  171. if (userPercent === 0) {
  172. targetPercent *= 4
  173. }
  174. if (targetPercent === 0) {
  175. userPercent *= 4
  176. }
  177. return this.f(this.bias + user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  178. }
  179. explain (user: Creature, target: Creature): LogEntry {
  180. let result: LogEntry
  181. let userPercent = 1
  182. let targetPercent = 1
  183. Object.keys(Vigor).forEach(key => {
  184. userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
  185. targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
  186. userPercent = Math.max(0, userPercent)
  187. targetPercent = Math.max(0, targetPercent)
  188. })
  189. if (userPercent === 0) {
  190. targetPercent *= 4
  191. }
  192. if (targetPercent === 0) {
  193. userPercent *= 4
  194. }
  195. const userMod = user.stats[this.stat] * userPercent
  196. const targetMod = target.stats[this.stat] * targetPercent
  197. const delta = userMod - targetMod
  198. if (delta === 0) {
  199. result = new LogLine('You and the target have the same effective', new PropElem(this.stat), '.')
  200. } else if (delta < 0) {
  201. result = new LogLine('You effectively have ', new PropElem(this.stat, -delta), ' less than your foe.')
  202. } else {
  203. result = new LogLine('You effectively have ', new PropElem(this.stat, delta), ' more than you foe.')
  204. }
  205. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  206. return result
  207. }
  208. }
  209. export class StatTest extends RandomTest {
  210. private f: (x: number) => number
  211. private k = 0.1
  212. constructor (public readonly stat: Stat, private bias = 0, fail: (user: Creature, target: Creature) => LogEntry) {
  213. super(fail)
  214. this.f = logistic(0, 1, this.k)
  215. }
  216. odds (user: Creature, target: Creature): number {
  217. return this.f(this.bias + user.stats[this.stat] - target.stats[this.stat])
  218. }
  219. explain (user: Creature, target: Creature): LogEntry {
  220. const delta: number = user.stats[this.stat] - target.stats[this.stat]
  221. let result: LogEntry
  222. if (delta === 0) {
  223. result = new LogLine('You and the target have the same ', new PropElem(this.stat), '.')
  224. } else if (delta < 0) {
  225. result = new LogLine('You have ', new PropElem(this.stat, -delta), ' less than your foe.')
  226. } else {
  227. result = new LogLine('You have ', new PropElem(this.stat, delta), ' more than you foe.')
  228. }
  229. result = new LogLine(result, 'Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%')
  230. return result
  231. }
  232. }
  233. export class ChanceTest extends RandomTest {
  234. constructor (public readonly chance: number, fail: (user: Creature, target: Creature) => LogEntry) {
  235. super(fail)
  236. }
  237. odds (user: Creature, target: Creature): number {
  238. return this.chance
  239. }
  240. explain (user: Creature, target: Creature): LogEntry {
  241. return new LogLine('You have a flat ' + (100 * this.chance) + '% chance.')
  242. }
  243. }