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.
 
 
 
 
 

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