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.
 
 
 
 
 

508 lines
16 KiB

  1. import { Creature, POV, Entity } from './entity'
  2. import { POVPair, POVPairArgs, TextLike, DynText, LiveText } 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: TextLike, public desc: string, private conditions: Array<Condition> = []) {
  170. }
  171. toString (): string {
  172. return this.name.toString()
  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(`${user.name.capital} 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(`${user.name.capital} 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(new DynText('Devour (', new LiveText(container, x => x.name.all), ')'), 'Try to consume your foe', [new CapableCondition()])
  280. this.test = new StatVigorTest(Stat.STR)
  281. }
  282. execute (user: Creature, target: Creature): LogEntry {
  283. if (this.test.test(user, target)) {
  284. return this.container.consume(target)
  285. } else {
  286. return this.failLines.run(user, target)
  287. }
  288. }
  289. }
  290. export class FeedAction extends TogetherAction {
  291. private test: StatTest
  292. protected failLines: POVPair<Entity, Entity> = new POVPair([
  293. [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to feed yourself to ${target.name}`)],
  294. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to feed ${user.pronouns.possessive} to you, but fails`)],
  295. [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} unsuccessfully tries to feed ${user.pronouns.possessive} to ${target.name}`)]
  296. ])
  297. allowed (user: Creature, target: Creature): boolean {
  298. const owner = this.container.owner === target
  299. const predOk = Array.from(this.container.voreTypes).every(pref => user.predPrefs.has(pref))
  300. const preyOk = Array.from(this.container.voreTypes).every(pref => target.preyPrefs.has(pref))
  301. if (owner && predOk && preyOk) {
  302. return super.allowed(user, target)
  303. } else {
  304. return false
  305. }
  306. }
  307. constructor (protected container: Container) {
  308. super('Feed', 'Feed yourself to your opponent', [new DrainedVigorCondition(Vigor.Willpower)])
  309. this.name += ` (${container.name})`
  310. this.test = new StatTest(Stat.STR)
  311. }
  312. execute (user: Creature, target: Creature): LogEntry {
  313. return this.container.consume(user)
  314. }
  315. }
  316. export class StruggleAction extends PairAction {
  317. private test: StatVigorTest
  318. protected failLines: POVPair<Entity, Entity> = new POVPair([
  319. [[POV.First, POV.Third], (user, target) => new LogLines(`You fail to escape from ${target.name}`)],
  320. [[POV.Third, POV.First], (user, target) => new LogLines(`${user.name.capital} tries to escape from you, but fails`)],
  321. [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} unsuccessfully struggles within ${target.name}`)]
  322. ])
  323. allowed (user: Creature, target: Creature) {
  324. if (user.containedIn === this.container && this.container.owner === target) {
  325. return super.allowed(user, target)
  326. } else {
  327. return false
  328. }
  329. }
  330. constructor (public container: Container) {
  331. super(new DynText('Struggle (', new LiveText(container, x => x.name.all), ')'), 'Try to escape from your foe', [new CapableCondition()])
  332. this.test = new StatVigorTest(Stat.STR)
  333. }
  334. execute (user: Creature, target: Creature): LogEntry {
  335. if (user.containedIn !== null) {
  336. if (this.test.test(user, target)) {
  337. return user.containedIn.release(user)
  338. } else {
  339. return this.failLines.run(user, target)
  340. }
  341. } else {
  342. return new LogLines("Vore's bugged!")
  343. }
  344. }
  345. }
  346. export abstract class EatenAction extends PairAction {
  347. protected lines: POVPair<Entity, Entity> = new POVPair([])
  348. allowed (user: Creature, target: Creature) {
  349. if (target.containedIn === this.container) {
  350. return super.allowed(user, target)
  351. } else {
  352. return false
  353. }
  354. }
  355. constructor (public container: Container, name: TextLike, desc: string) {
  356. super(new DynText(name, ' (', new LiveText(container, x => x.name.all), ')'), 'Do something to your prey!', [new CapableCondition()])
  357. }
  358. }
  359. export class DigestAction extends SelfAction {
  360. protected lines: POVPair<Entity, Entity> = new POVPair([])
  361. allowed (user: Creature, target: Creature) {
  362. if (this.container.owner === user && this.container.contents.length > 0) {
  363. return super.allowed(user, target)
  364. } else {
  365. return false
  366. }
  367. }
  368. constructor (protected container: Container) {
  369. super(new DynText('Digest (', new LiveText(container, container => container.name.all), ')'), 'Digest your prey', [new CapableCondition()])
  370. }
  371. execute (user: Creature, target: Creature): LogEntry {
  372. const results = this.container.tick(60)
  373. return new CompositeLog(results)
  374. }
  375. }
  376. export class ReleaseAction extends PairAction {
  377. allowed (user: Creature, target: Creature) {
  378. if (target.containedIn === this.container && this.container.contents.indexOf(target) >= 0) {
  379. return super.allowed(user, target)
  380. } else {
  381. return false
  382. }
  383. }
  384. constructor (protected container: Container) {
  385. super(new DynText('Release (', new LiveText(container, x => x.name.all), ')'), 'Release one of your prey', [new CapableCondition()])
  386. }
  387. execute (user: Creature, target: Creature): LogEntry {
  388. return this.container.release(target)
  389. }
  390. }
  391. export class TransferAction extends PairAction {
  392. lines: POVPairArgs<Entity, Entity, { from: Container; to: Container }> = new POVPairArgs([
  393. [[POV.First, POV.Third], (user, target, args) => new LogLine(`You squeeze ${target.name} from your ${args.from.name} to your ${args.to.name}`)],
  394. [[POV.Third, POV.First], (user, target, args) => new LogLine(`You're squeezed from ${user.name}'s ${args.from.name} to ${user.pronouns.possessive} ${args.to.name}`)],
  395. [[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}`)]
  396. ])
  397. allowed (user: Creature, target: Creature) {
  398. if (target.containedIn === this.from) {
  399. return super.allowed(user, target)
  400. } else {
  401. return false
  402. }
  403. }
  404. constructor (protected from: Container, protected to: Container) {
  405. super('Transfer', `Shove your prey from your ${from.name} to your ${to.name}`, [new CapableCondition()])
  406. }
  407. execute (user: Creature, target: Creature): LogEntry {
  408. this.from.release(target)
  409. this.to.consume(target)
  410. return this.lines.run(user, target, { from: this.from, to: this.to })
  411. }
  412. }