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.
 
 
 
 
 

500 line
15 KiB

  1. import { Creature, POV, Entity } from './entity'
  2. import { POVPair, POVPairArgs } from './language'
  3. import { Container } from './vore'
  4. import { LogEntry, LogLines, CompositeLog, FAElem, LogLine, FormatEntry, FormatOpt } from './interface'
  5. export enum DamageType {
  6. Pierce = "Pierce",
  7. Slash = "Slash",
  8. Crush = "Crush",
  9. Acid = "Acid",
  10. Seduction = "Seduction",
  11. Dominance = "Dominance"
  12. }
  13. export interface DamageInstance {
  14. type: DamageType;
  15. amount: number;
  16. target: Vigor;
  17. }
  18. export enum Vigor {
  19. Health = "Health",
  20. Stamina = "Stamina",
  21. Willpower = "Willpower"
  22. }
  23. export const VigorIcons: {[key in Vigor]: string} = {
  24. [Vigor.Health]: "fas fa-heart",
  25. [Vigor.Stamina]: "fas fa-bolt",
  26. [Vigor.Willpower]: "fas fa-brain"
  27. }
  28. export type Vigors = {[key in Vigor]: number}
  29. export enum Stat {
  30. STR = 'Strength',
  31. DEX = 'Dexterity',
  32. CON = 'Constitution'
  33. }
  34. export type Stats = {[key in Stat]: number}
  35. export const StatIcons: {[key in Stat]: string} = {
  36. [Stat.STR]: 'fas fa-fist-raised',
  37. [Stat.DEX]: 'fas fa-feather',
  38. [Stat.CON]: 'fas fa-heartbeat'
  39. }
  40. export interface CombatTest {
  41. test: (user: Creature, target: Creature) => boolean;
  42. odds: (user: Creature, target: Creature) => number;
  43. explain: (user: Creature, target: Creature) => LogEntry;
  44. }
  45. function logistic (x0: number, L: number, k: number): (x: number) => number {
  46. return (x: number) => {
  47. return L / (1 + Math.exp(-k * (x - x0)))
  48. }
  49. }
  50. abstract class RandomTest implements CombatTest {
  51. test (user: Creature, target: Creature): boolean {
  52. return Math.random() < this.odds(user, target)
  53. }
  54. abstract odds(user: Creature, target: Creature): number
  55. abstract explain(user: Creature, target: Creature): LogEntry
  56. }
  57. export class StatVigorTest extends RandomTest {
  58. private f: (x: number) => number
  59. constructor (public readonly stat: Stat, k = 0.1) {
  60. super()
  61. this.f = logistic(0, 1, k)
  62. }
  63. odds (user: Creature, target: Creature): number {
  64. let userPercent = 1
  65. let targetPercent = 1
  66. Object.keys(Vigor).forEach(key => {
  67. userPercent *= user.vigors[key as Vigor] / user.maxVigors[key as Vigor]
  68. targetPercent *= target.vigors[key as Vigor] / target.maxVigors[key as Vigor]
  69. userPercent = Math.max(0, userPercent)
  70. targetPercent = Math.max(0, targetPercent)
  71. })
  72. if (userPercent === 0) {
  73. targetPercent *= 4
  74. }
  75. if (targetPercent === 0) {
  76. userPercent *= 4
  77. }
  78. console.log(userPercent, targetPercent, this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent))
  79. return this.f(user.stats[this.stat] * userPercent - target.stats[this.stat] * targetPercent)
  80. }
  81. explain (user: Creature, target: Creature): LogEntry {
  82. const delta: number = user.stats[this.stat] - target.stats[this.stat]
  83. let result: string
  84. if (delta === 0) {
  85. result = 'You and the target have the same ' + this.stat + '.'
  86. } else if (delta < 0) {
  87. result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.'
  88. } else {
  89. result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.'
  90. }
  91. result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%'
  92. return new LogLines(result)
  93. }
  94. }
  95. export class StatTest extends RandomTest {
  96. private f: (x: number) => number
  97. constructor (public readonly stat: Stat, k = 0.1) {
  98. super()
  99. this.f = logistic(0, 1, k)
  100. }
  101. odds (user: Creature, target: Creature): number {
  102. return this.f(user.stats[this.stat] - target.stats[this.stat])
  103. }
  104. explain (user: Creature, target: Creature): LogEntry {
  105. const delta: number = user.stats[this.stat] - target.stats[this.stat]
  106. let result: string
  107. if (delta === 0) {
  108. result = 'You and the target have the same ' + this.stat + '.'
  109. } else if (delta < 0) {
  110. result = 'You have ' + delta + ' less ' + this.stat + ' than your foe.'
  111. } else {
  112. result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.'
  113. }
  114. result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%'
  115. return new LogLines(result)
  116. }
  117. }
  118. export class ChanceTest extends RandomTest {
  119. constructor (public readonly chance: number) {
  120. super()
  121. }
  122. odds (user: Creature, target: Creature): number {
  123. return this.chance
  124. }
  125. explain (user: Creature, target: Creature): LogEntry {
  126. return new LogLines('You have a flat ' + (100 * this.chance) + '% chance.')
  127. }
  128. }
  129. export class Damage {
  130. readonly damages: DamageInstance[]
  131. constructor (...damages: DamageInstance[]) {
  132. this.damages = damages
  133. }
  134. scale (factor: number): Damage {
  135. const results: Array<DamageInstance> = []
  136. this.damages.forEach(damage => {
  137. results.push({
  138. type: damage.type,
  139. amount: damage.amount * factor,
  140. target: damage.target
  141. })
  142. })
  143. return new Damage(...results)
  144. }
  145. toString (): string {
  146. return this.damages.map(damage => damage.amount + " " + damage.type).join("/")
  147. }
  148. render (): LogEntry {
  149. return new LogLine(...this.damages.flatMap(instance => {
  150. return [instance.amount.toString(), new FAElem(VigorIcons[instance.target]), " " + instance.type]
  151. }))
  152. }
  153. renderShort (): LogEntry {
  154. const totals: Vigors = Object.keys(Vigor).reduce((total: any, key) => { total[key] = 0; return total }, {})
  155. this.damages.forEach(instance => {
  156. totals[instance.target] += instance.amount
  157. })
  158. return new FormatEntry(new LogLine(...Object.keys(Vigor).flatMap(key => totals[key as Vigor] === 0 ? [] : [totals[key as Vigor].toFixed(0).toString(), new FAElem(VigorIcons[key as Vigor])])), FormatOpt.DamageInst)
  159. }
  160. }
  161. export interface Combatant {
  162. actions: Array<Action>;
  163. }
  164. export abstract class Action {
  165. allowed (user: Creature, target: Creature): boolean {
  166. return this.conditions.every(cond => cond.allowed(user, target))
  167. }
  168. abstract execute(user: Creature, target: Creature): LogEntry
  169. constructor (public name: string, public desc: string, private conditions: Array<Condition> = []) {
  170. }
  171. toString (): string {
  172. return this.name
  173. }
  174. }
  175. export interface Condition {
  176. allowed: (user: Creature, target: Creature) => boolean;
  177. }
  178. class InverseCondition implements Condition {
  179. allowed (user: Creature, target: Creature): boolean {
  180. return !this.condition.allowed(user, target)
  181. }
  182. constructor (private condition: Condition) {
  183. }
  184. }
  185. class CapableCondition implements Condition {
  186. allowed (user: Creature, target: Creature): boolean {
  187. return !user.disabled
  188. }
  189. }
  190. class DrainedVigorCondition implements Condition {
  191. allowed (user: Creature, target: Creature): boolean {
  192. return user.vigors[this.vigor] <= 0
  193. }
  194. constructor (private vigor: Vigor) {
  195. }
  196. }
  197. export interface Actionable {
  198. actions: Array<Action>;
  199. }
  200. abstract class SelfAction extends Action {
  201. allowed (user: Creature, target: Creature) {
  202. if (user === target) {
  203. return super.allowed(user, target)
  204. } else {
  205. return false
  206. }
  207. }
  208. }
  209. abstract class PairAction extends Action {
  210. allowed (user: Creature, target: Creature) {
  211. if (user !== target) {
  212. return super.allowed(user, target)
  213. } else {
  214. return false
  215. }
  216. }
  217. }
  218. abstract class TogetherAction extends PairAction {
  219. allowed (user: Creature, target: Creature) {
  220. if (user.containedIn === target.containedIn) {
  221. return super.allowed(user, target)
  222. } else {
  223. return false
  224. }
  225. }
  226. }
  227. export class AttackAction extends TogetherAction {
  228. protected test: StatTest
  229. protected successLines: POVPairArgs<Entity, Entity, { damage: Damage }> = new POVPairArgs([
  230. [[POV.First, POV.Third], (user, target, args) => new LogLine(
  231. `You smack ${target.name} for `,
  232. args.damage.renderShort()
  233. )],
  234. [[POV.Third, POV.First], (user, target, args) => new LogLine(
  235. `${user.name.capital} smacks you for `,
  236. args.damage.renderShort()
  237. )],
  238. [[POV.Third, POV.Third], (user, target, args) => new LogLine(
  239. `${user.name.capital} smacks ${target.name} for `,
  240. args.damage.renderShort()
  241. )]
  242. ])
  243. protected failLines: POVPair<Entity, Entity> = new POVPair([
  244. [[POV.First, POV.Third], (user, target) => new LogLines(`You try to smack ${target.name}, but you miss`)],
  245. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} misses you`)],
  246. [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} misses ${target.name}`)]
  247. ])
  248. constructor (protected damage: Damage) {
  249. super('Attack', 'Attack the enemy', [new CapableCondition()])
  250. this.test = new StatTest(Stat.STR)
  251. }
  252. execute (user: Creature, target: Creature): LogEntry {
  253. if (this.test.test(user, target)) {
  254. target.takeDamage(this.damage)
  255. return this.successLines.run(user, target, { damage: this.damage })
  256. } else {
  257. return this.failLines.run(user, target)
  258. }
  259. }
  260. }
  261. export class DevourAction extends TogetherAction {
  262. private test: StatVigorTest
  263. protected failLines: POVPair<Entity, Entity> = new POVPair([
  264. [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to make a meal out of ${target.name}`)],
  265. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to devour you, but fails`)],
  266. [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} unsuccessfully tries to swallow ${target.name}`)]
  267. ])
  268. allowed (user: Creature, target: Creature): boolean {
  269. const owner = this.container.owner === user
  270. const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref))
  271. const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref))
  272. if (owner && predOk && preyOk) {
  273. return super.allowed(user, target)
  274. } else {
  275. return false
  276. }
  277. }
  278. constructor (protected container: Container) {
  279. super('Devour', 'Try to consume your foe', [new CapableCondition()])
  280. this.name += ` (${container.name})`
  281. this.test = new StatVigorTest(Stat.STR)
  282. }
  283. execute (user: Creature, target: Creature): LogEntry {
  284. if (this.test.test(user, target)) {
  285. return this.container.consume(target)
  286. } else {
  287. return this.failLines.run(user, target)
  288. }
  289. }
  290. }
  291. export class FeedAction extends TogetherAction {
  292. private test: StatTest
  293. protected failLines: POVPair<Entity, Entity> = new POVPair([
  294. [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to feed yourself to ${target.name}`)],
  295. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to feed ${user.pronouns.possessive} to you, but fails`)],
  296. [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} unsuccessfully tries to feed ${user.pronouns.possessive} to ${target.name}`)]
  297. ])
  298. allowed (user: Creature, target: Creature): boolean {
  299. const owner = this.container.owner === target
  300. const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref))
  301. const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref))
  302. if (owner && predOk && preyOk) {
  303. return super.allowed(user, target)
  304. } else {
  305. return false
  306. }
  307. }
  308. constructor (protected container: Container) {
  309. super('Feed', 'Feed yourself to your opponent', [new DrainedVigorCondition(Vigor.Willpower)])
  310. this.name += ` (${container.name})`
  311. this.test = new StatTest(Stat.STR)
  312. }
  313. execute (user: Creature, target: Creature): LogEntry {
  314. if (this.test.test(user, target)) {
  315. return this.container.consume(user)
  316. } else {
  317. return this.failLines.run(user, target)
  318. }
  319. }
  320. }
  321. export class StruggleAction extends PairAction {
  322. private test: StatVigorTest
  323. protected failLines: POVPair<Entity, Entity> = new POVPair([
  324. [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to escape from ${target.name}`)],
  325. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to escape from you, but fails`)],
  326. [[POV.Third, POV.Third], (user, target) => new LogLines(`${target.name} unsuccessfully struggles within ${target.name}`)]
  327. ])
  328. allowed (user: Creature, target: Creature) {
  329. if (user.containedIn === this.container) {
  330. return super.allowed(user, target)
  331. } else {
  332. return false
  333. }
  334. }
  335. constructor (public container: Container) {
  336. super('Struggle', 'Try to escape your predator', [new CapableCondition()])
  337. this.test = new StatVigorTest(Stat.STR)
  338. }
  339. execute (user: Creature, target: Creature): LogEntry {
  340. if (user.containedIn !== null) {
  341. if (this.test.test(user, target)) {
  342. return user.containedIn.release(user)
  343. } else {
  344. return this.failLines.run(user, target)
  345. }
  346. } else {
  347. return new LogLines("Vore's bugged!")
  348. }
  349. }
  350. }
  351. export class DigestAction extends SelfAction {
  352. protected lines: POVPair<Entity, Entity> = new POVPair([])
  353. allowed (user: Creature, target: Creature) {
  354. if (this.container.owner === user && this.container.contents.length > 0) {
  355. return super.allowed(user, target)
  356. } else {
  357. return false
  358. }
  359. }
  360. constructor (protected container: Container) {
  361. super('Digest', 'Digest all of your current prey', [new CapableCondition()])
  362. this.name += ` (${container.name})`
  363. }
  364. execute (user: Creature, target: Creature): LogEntry {
  365. const results = this.container.tick(60)
  366. return new CompositeLog(results)
  367. }
  368. }
  369. export class ReleaseAction extends PairAction {
  370. allowed (user: Creature, target: Creature) {
  371. if (target.containedIn === this.container && this.container.contents.indexOf(target) >= 0) {
  372. return super.allowed(user, target)
  373. } else {
  374. return false
  375. }
  376. }
  377. constructor (protected container: Container) {
  378. super('Release', 'Release one of your prey')
  379. this.name += ` (${container.name})`
  380. }
  381. execute (user: Creature, target: Creature): LogEntry {
  382. return this.container.release(target)
  383. }
  384. }
  385. export class TransferAction extends PairAction {
  386. protected lines: POVPairArgs<Entity, Entity, { from: Container; to: Container }> = new POVPairArgs([
  387. [[POV.First, POV.Third], (user, target, args) => new LogLine(`You squeeze ${target.name} from your ${args.from.name} to your ${args.to.name}`)],
  388. [[POV.Third, POV.First], (user, target, args) => new LogLine(`You're squeezed from ${user.name}'s ${args.from.name} to ${target.pronouns.possessive} ${args.to.name}`)],
  389. [[POV.Third, POV.Third], (user, target, args) => new LogLine(`${user.name} squeezes ${target.name} from ${user.pronouns.possessive} ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`)]
  390. ])
  391. allowed (user: Creature, target: Creature) {
  392. if (target.containedIn === this.from) {
  393. return super.allowed(user, target)
  394. } else {
  395. return false
  396. }
  397. }
  398. constructor (protected from: Container, protected to: Container) {
  399. super('Transfer', `Shove your prey from your ${from.name} to your ${to.name}`, [new CapableCondition()])
  400. }
  401. execute (user: Creature, target: Creature): LogEntry {
  402. this.from.release(target)
  403. this.to.consume(target)
  404. return this.lines.run(user, target, { from: this.from, to: this.to })
  405. }
  406. }