Feast 2.0!
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 
 
 

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