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

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