| @@ -1,5 +1,5 @@ | |||
| <template> | |||
| <button class="action-button" @click="execute"> | |||
| <button @mouseover="describe" class="action-button" @click="execute"> | |||
| <div class="action-title">{{ action.name }}</div> | |||
| <div class="action-desc">{{ action.desc }}</div> | |||
| </button> | |||
| @@ -26,6 +26,11 @@ export default class ActionButton extends Vue { | |||
| execute () { | |||
| this.$emit('executed', this.action.execute(this.user, this.target)) | |||
| } | |||
| @Emit("describe") | |||
| describe () { | |||
| this.$emit('described', this.action.describe(this.user, this.target)) | |||
| } | |||
| } | |||
| </script> | |||
| @@ -35,7 +40,7 @@ export default class ActionButton extends Vue { | |||
| .action-button { | |||
| width: 100pt; | |||
| background: #333; | |||
| } | |||
| }q | |||
| .action-button:hover { | |||
| background: #555; | |||
| @@ -17,20 +17,22 @@ | |||
| <div class="left-actions"> | |||
| <div class="vert-display"> | |||
| <h2>Moves</h2> | |||
| <ActionButton @executed="executedLeft" v-for="action in left.validActions(right)" :key="'left' + action.name" :action="action" :user="left" :target="right" /> | |||
| <ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(right)" :key="'left' + action.name" :action="action" :user="left" :target="right" /> | |||
| <h2>Self-moves</h2> | |||
| <ActionButton @executed="executedLeft" v-for="action in left.validActions(left)" :key="'left' + action.name" :action="action" :user="left" :target="right" /> | |||
| <ActionButton @described="described" @executed="executedLeft" v-for="action in left.validActions(left)" :key="'left' + action.name" :action="action" :user="left" :target="right" /> | |||
| </div> | |||
| <div>{{actionDescription}}</div> | |||
| </div> | |||
| <div class="right-actions"> | |||
| <div class="vert-display"> | |||
| <h2>Moves</h2> | |||
| <ActionButton @executed="executedRight" v-for="action in right.validActions(left)" :key="'right' + action.name" :action="action" :user="right" :target="left" /> | |||
| <ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(left)" :key="'right' + action.name" :action="action" :user="right" :target="left" /> | |||
| <h2>Self-moves</h2> | |||
| <ActionButton @executed="executedRight" v-for="action in right.validActions(right)" :key="'right' + action.name" :action="action" :user="right" :target="left" /> | |||
| <ActionButton @described="described" @executed="executedRight" v-for="action in right.validActions(right)" :key="'right' + action.name" :action="action" :user="right" :target="left" /> | |||
| </div> | |||
| </div> | |||
| <div id="action-desc"> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| @@ -91,6 +93,20 @@ export default class Combat extends Vue { | |||
| log.scrollTo({ top: 10000000000, left: 0 }) | |||
| } | |||
| } | |||
| @Emit("described") | |||
| described (entry: LogEntry) { | |||
| const actionDesc = document.querySelector("#action-desc") | |||
| if (actionDesc !== null) { | |||
| const holder = document.createElement("div") | |||
| entry.render().forEach(element => { | |||
| holder.appendChild(element) | |||
| }) | |||
| actionDesc.innerHTML = '' | |||
| actionDesc.appendChild(holder) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @@ -115,7 +131,7 @@ export default class Combat extends Vue { | |||
| #log { | |||
| grid-area: main-row-start / main-col-start / main-row-end / main-col-end; | |||
| overflow-y: auto; | |||
| overflow-y: scroll; | |||
| font-size: 16pt; | |||
| width: 100%; | |||
| max-height: 100%; | |||
| @@ -138,6 +154,10 @@ export default class Combat extends Vue { | |||
| grid-area: 4 / 3 / 6 / 4; | |||
| } | |||
| #action-desc { | |||
| grid-area: main-row-end / main-col-start / 6 / main-col-end | |||
| } | |||
| h3 { | |||
| margin: 40px 0 0; | |||
| } | |||
| @@ -25,15 +25,15 @@ export enum Vigor { | |||
| } | |||
| export const VigorIcons: {[key in Vigor]: string} = { | |||
| [Vigor.Health]: "fas fa-heart", | |||
| [Vigor.Stamina]: "fas fa-bolt", | |||
| [Vigor.Resolve]: "fas fa-brain" | |||
| Health: "fas fa-heart", | |||
| Stamina: "fas fa-bolt", | |||
| Resolve: "fas fa-brain" | |||
| } | |||
| export const VigorDescs: {[key in Vigor]: string} = { | |||
| [Vigor.Health]: "How much damage you can take", | |||
| [Vigor.Stamina]: "How much energy you have", | |||
| [Vigor.Resolve]: "How much dominance you can resist" | |||
| Health: "How much damage you can take", | |||
| Stamina: "How much energy you have", | |||
| Resolve: "How much dominance you can resist" | |||
| } | |||
| export type Vigors = {[key in Vigor]: number} | |||
| @@ -49,19 +49,19 @@ export enum Stat { | |||
| export type Stats = {[key in Stat]: number} | |||
| export const StatIcons: {[key in Stat]: string} = { | |||
| [Stat.Toughness]: 'fas fa-heartbeat', | |||
| [Stat.Power]: 'fas fa-fist-raised', | |||
| [Stat.Speed]: 'fas fa-feather', | |||
| [Stat.Willpower]: 'fas fa-book', | |||
| [Stat.Charm]: 'fas fa-comments' | |||
| Toughness: 'fas fa-heartbeat', | |||
| Power: 'fas fa-fist-raised', | |||
| Speed: 'fas fa-feather', | |||
| Willpower: 'fas fa-book', | |||
| Charm: 'fas fa-comments' | |||
| } | |||
| export const StatDescs: {[key in Stat]: string} = { | |||
| [Stat.Toughness]: 'Your physical resistance', | |||
| [Stat.Power]: 'Your physical power', | |||
| [Stat.Speed]: 'How quickly you can act', | |||
| [Stat.Willpower]: 'Your mental resistance', | |||
| [Stat.Charm]: 'Your mental power' | |||
| Toughness: 'Your physical resistance', | |||
| Power: 'Your physical power', | |||
| Speed: 'How quickly you can act', | |||
| Willpower: 'Your mental resistance', | |||
| Charm: 'Your mental power' | |||
| } | |||
| export interface CombatTest { | |||
| @@ -129,7 +129,7 @@ export class StatVigorTest extends RandomTest { | |||
| result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%' | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| @@ -159,7 +159,7 @@ export class StatTest extends RandomTest { | |||
| result = 'You have ' + delta + ' more ' + this.stat + ' than you foe.' | |||
| } | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)) + '%' | |||
| result += ' Your odds of success are ' + (100 * this.odds(user, target)).toFixed(1) + '%' | |||
| return new LogLines(result) | |||
| } | |||
| @@ -221,6 +221,39 @@ export class Damage { | |||
| } | |||
| } | |||
| export interface DamageFormula { | |||
| calc (user: Creature, target: Creature): Damage; | |||
| describe (user: Creature, target: Creature): LogEntry; | |||
| } | |||
| export class ConstantDamageFormula implements DamageFormula { | |||
| calc (user: Creature, target: Creature): Damage { | |||
| return this.damage | |||
| } | |||
| constructor (private damage: Damage) { | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Deal ', this.damage.renderShort(), ' damage') | |||
| } | |||
| } | |||
| export class UniformRandomDamageFormula implements DamageFormula { | |||
| calc (user: Creature, target: Creature): Damage { | |||
| return this.damage.scale(Math.random() * this.variance * 2 - this.variance + 1) | |||
| } | |||
| constructor (private damage: Damage, private variance: number) { | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine('Deal between ', this.damage.scale(1 - this.variance).renderShort(), ' and ', this.damage.scale(1 + this.variance).renderShort(), ' damage.') | |||
| } | |||
| } | |||
| export interface Combatant { | |||
| actions: Array<Action>; | |||
| } | |||
| @@ -232,7 +265,9 @@ export abstract class Action { | |||
| abstract execute(user: Creature, target: Creature): LogEntry | |||
| constructor (public name: TextLike, public desc: string, private conditions: Array<Condition> = []) { | |||
| abstract describe (user: Creature, target: Creature): LogEntry | |||
| constructor (public name: TextLike, public desc: TextLike, private conditions: Array<Condition> = []) { | |||
| } | |||
| @@ -328,20 +363,25 @@ export class AttackAction extends TogetherAction { | |||
| [[POV.Third, POV.Third], (user, target) => new LogLines(`${user.name.capital} misses ${target.name}`)] | |||
| ]) | |||
| constructor (protected damage: Damage) { | |||
| constructor (protected damage: DamageFormula) { | |||
| super('Attack', 'Attack the enemy', [new CapableCondition()]) | |||
| this.test = new StatTest(Stat.Power) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| if (this.test.test(user, target)) { | |||
| const targetResult = target.takeDamage(this.damage) | |||
| const ownResult = this.successLines.run(user, target, { damage: this.damage }) | |||
| const damage = this.damage.calc(user, target) | |||
| const targetResult = target.takeDamage(damage) | |||
| const ownResult = this.successLines.run(user, target, { damage: damage }) | |||
| return new CompositeLog(ownResult, targetResult) | |||
| } else { | |||
| return this.failLines.run(user, target) | |||
| } | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Attack ${target.name}. `, this.damage.describe(user, target), '. ', this.test.explain(user, target)) | |||
| } | |||
| } | |||
| export class DevourAction extends TogetherAction { | |||
| @@ -377,6 +417,10 @@ export class DevourAction extends TogetherAction { | |||
| return this.failLines.run(user, target) | |||
| } | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Try to consume your opponent, sending them to your ${this.container.name}. `, this.test.explain(user, target)) | |||
| } | |||
| } | |||
| export class FeedAction extends TogetherAction { | |||
| @@ -409,6 +453,10 @@ export class FeedAction extends TogetherAction { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return this.container.consume(user) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Your willpower is drained, and you really feel like shoving yourself into ${target.name}'s ${this.container.name}...`) | |||
| } | |||
| } | |||
| export class StruggleAction extends PairAction { | |||
| @@ -444,6 +492,10 @@ export class StruggleAction extends PairAction { | |||
| return new LogLines("Vore's bugged!") | |||
| } | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Try to escape from ${target.name}'s ${this.container.name}. `, this.test.explain(user, target)) | |||
| } | |||
| } | |||
| export abstract class EatenAction extends PairAction { | |||
| @@ -458,7 +510,7 @@ export abstract class EatenAction extends PairAction { | |||
| } | |||
| constructor (public container: Container, name: TextLike, desc: string) { | |||
| super(new DynText(name, ' (', new LiveText(container, x => x.name.all), ')'), 'Do something to your prey!', [new CapableCondition()]) | |||
| super(new DynText(name, ' (', new LiveText(container, x => x.name.all), ')'), desc, [new CapableCondition()]) | |||
| } | |||
| } | |||
| export class DigestAction extends SelfAction { | |||
| @@ -480,6 +532,10 @@ export class DigestAction extends SelfAction { | |||
| const results = this.container.tick(60) | |||
| return new CompositeLog(results) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Digest everyone inside of your ${this.container.name}.`) | |||
| } | |||
| } | |||
| export class ReleaseAction extends PairAction { | |||
| @@ -498,6 +554,10 @@ export class ReleaseAction extends PairAction { | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| return this.container.release(target) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Release ${target.name} from your ${this.container.name}.`) | |||
| } | |||
| } | |||
| export class TransferAction extends PairAction { | |||
| @@ -516,7 +576,7 @@ export class TransferAction extends PairAction { | |||
| } | |||
| constructor (protected from: Container, protected to: Container) { | |||
| super('Transfer', `Shove your prey from your ${from.name} to your ${to.name}`, [new CapableCondition()]) | |||
| super('Transfer', `Move from your ${from.name} to your ${to.name}`, [new CapableCondition()]) | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| @@ -524,4 +584,8 @@ export class TransferAction extends PairAction { | |||
| this.to.consume(target) | |||
| return this.lines.run(user, target, { from: this.from, to: this.to }) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Push ${target.name} from your ${this.from.name} to your ${this.to.name}`) | |||
| } | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import { Creature, POV, Entity } from '../entity' | |||
| import { Stat, Damage, DamageType, TransferAction, Vigor, StatTest, FeedAction, DigestAction, EatenAction, AttackAction } from '../combat' | |||
| import { Stat, Damage, DamageType, TransferAction, Vigor, StatTest, FeedAction, DigestAction, EatenAction, AttackAction, DamageFormula, ConstantDamageFormula } from '../combat' | |||
| import { ProperNoun, TheyPronouns, ImproperNoun, POVPair, FemalePronouns, POVPairArgs } from '../language' | |||
| import { VoreType, Stomach, InnerStomach, Container, Bowels } from '../vore' | |||
| import { LogLine, LogLines, LogEntry, FAElem, CompositeLog, ImgElem } from '../interface' | |||
| @@ -21,15 +21,17 @@ class BellyCrushAction extends AttackAction { | |||
| ), new ImgElem('./media/cafat/images/belly-crush.webp'))] | |||
| ]) | |||
| constructor (private _damage: Damage) { | |||
| super(_damage) | |||
| constructor (_damage: Damage) { | |||
| super({ | |||
| calc (user, target) { return _damage.scale(user.bulk / 25) }, | |||
| describe (user, target) { return new LogLine('Deal ', _damage.scale(user.bulk / 25).renderShort(), ` with your ${user.bulk} `, new FAElem('fas fa-weight-hanging')) } | |||
| }) | |||
| this.name = 'Belly Crush' | |||
| this.desc = 'Deal damage proportional to your bulk' | |||
| this.desc = 'Use your weight!' | |||
| } | |||
| execute (user: Creature, target: Creature): LogEntry { | |||
| this.damage = this._damage.scale(user.bulk / 25 + 1) | |||
| return super.execute(user, target) | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Crush ${target.name} under your gut. `, this.damage.describe(user, target)) | |||
| } | |||
| } | |||
| @@ -50,7 +52,7 @@ class BelchAction extends AttackAction { | |||
| ]) | |||
| constructor (damage: Damage) { | |||
| super(damage) | |||
| super(new ConstantDamageFormula(damage)) | |||
| this.name = 'Belch' | |||
| this.desc = 'Drain your foe\'s willpower with a solid BELCH' | |||
| } | |||
| @@ -75,6 +77,10 @@ class CrushAction extends EatenAction { | |||
| target.takeDamage(this.damage) | |||
| return this.lines.run(user, target) | |||
| } | |||
| describe (user: Creature, target: Creature): LogEntry { | |||
| return new LogLine(`Crush ${target.name} in your ${this.container.name} for massive, unavoidable damage.`) | |||
| } | |||
| } | |||
| export class Cafat extends Creature { | |||
| @@ -132,7 +138,7 @@ export class Cafat extends Creature { | |||
| this.actions.push(transfer) | |||
| this.actions.push(new TransferAction(lowerStomach, stomach)) | |||
| this.actions.push(new AttackAction(new Damage({ amount: 40, type: DamageType.Crush, target: Vigor.Health }))) | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ amount: 40, type: DamageType.Crush, target: Vigor.Health })))) | |||
| this.actions.push(new BellyCrushAction(new Damage({ amount: 10, type: DamageType.Crush, target: Vigor.Health }, { amount: 10, type: DamageType.Dominance, target: Vigor.Resolve }))) | |||
| this.actions.push(new BelchAction(new Damage( | |||
| { amount: 100, target: Vigor.Resolve, type: DamageType.Acid } | |||
| @@ -1,13 +1,13 @@ | |||
| import { Creature, POV } from '../entity' | |||
| import { ProperNoun, TheyPronouns } from '../language' | |||
| import { Stat, Damage, AttackAction, DamageType, Vigor } from '../combat' | |||
| import { Stat, Damage, AttackAction, DamageType, Vigor, ConstantDamageFormula } from '../combat' | |||
| import { Stomach, Bowels, VoreType } from '../vore' | |||
| export class Player extends Creature { | |||
| constructor () { | |||
| super(new ProperNoun('The Dude'), TheyPronouns, { Toughness: 20, Power: 20, Speed: 20, Willpower: 20, Charm: 20 }, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 50) | |||
| this.actions.push(new AttackAction(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina }))) | |||
| this.actions.push(new AttackAction(new ConstantDamageFormula(new Damage({ type: DamageType.Pierce, amount: 20, target: Vigor.Health }, { type: DamageType.Pierce, amount: 20, target: Vigor.Stamina })))) | |||
| const stomach = new Stomach(this, 100, new Damage({ amount: 20, type: DamageType.Acid, target: Vigor.Health }, { amount: 10, type: DamageType.Crush, target: Vigor.Health })) | |||
| @@ -1,12 +1,12 @@ | |||
| import { Creature, POV, Entity } from '../entity' | |||
| import { Stat, Damage, DamageType, AttackAction, TransferAction, Vigor, StatTest, FeedAction } from '../combat' | |||
| import { Stat, Damage, DamageType, AttackAction, TransferAction, Vigor, StatTest, FeedAction, ConstantDamageFormula } from '../combat' | |||
| import { MalePronouns, ImproperNoun, POVPair, POVPairArgs } from '../language' | |||
| import { LogLine, LogLines } from '../interface' | |||
| import { VoreType, Stomach, Bowels } from '../vore' | |||
| class BiteAction extends AttackAction { | |||
| constructor () { | |||
| super(new Damage({ amount: 10, type: DamageType.Slash, target: Vigor.Health })) | |||
| super(new ConstantDamageFormula(new Damage({ amount: 10, type: DamageType.Slash, target: Vigor.Health }))) | |||
| this.name = "Bite" | |||
| } | |||
| } | |||
| @@ -34,7 +34,7 @@ class HypnoAction extends AttackAction { | |||
| ]) | |||
| constructor () { | |||
| super(new Damage({ amount: 30, type: DamageType.Dominance, target: Vigor.Resolve })) | |||
| super(new ConstantDamageFormula(new Damage({ amount: 30, type: DamageType.Dominance, target: Vigor.Resolve }))) | |||
| this.test = new StatTest(Stat.Willpower) | |||
| this.name = "Hypnotize" | |||
| } | |||
| @@ -1,3 +1,5 @@ | |||
| import { Stat, Vigor } from './combat' | |||
| export interface LogEntry { | |||
| render: () => HTMLElement[]; | |||
| } | |||
| @@ -62,7 +64,7 @@ export class LogLine implements LogEntry { | |||
| } | |||
| render (): HTMLElement[] { | |||
| const div = document.createElement("div") | |||
| const div = document.createElement("span") | |||
| this.parts.forEach(part => { | |||
| if (typeof part === "string") { | |||
| @@ -92,6 +94,37 @@ export class FAElem implements LogEntry { | |||
| } | |||
| } | |||
| export class PropElem implements LogEntry { | |||
| constructor (private value: number, private prop: Stat | Vigor) { | |||
| } | |||
| render (): HTMLElement[] { | |||
| let cls: string | |||
| switch (this.prop) { | |||
| case Vigor.Health: cls = "fas fa-heart"; break | |||
| case Vigor.Stamina: cls = "fas fa-bolt"; break | |||
| case Vigor.Resolve: cls = "fas fa-brain"; break | |||
| case Stat.Toughness: cls = "fas fa-heartbeat"; break | |||
| case Stat.Power: cls = "fas fa-fist-raised"; break | |||
| case Stat.Speed: cls = "fas fa-weather"; break | |||
| case Stat.Willpower: cls = "fas fa-book"; break | |||
| case Stat.Charm: cls = "fas fa-comments"; break | |||
| } | |||
| const span = document.createElement("span") | |||
| span.classList.add("stat-entry") | |||
| span.textContent = this.value.toString() | |||
| new FAElem(cls).render().forEach(elem => { | |||
| span.appendChild(elem) | |||
| }) | |||
| span.dataset.tooltip = this.prop | |||
| span.dataset["tooltip-full"] = this.prop | |||
| return [span] | |||
| } | |||
| } | |||
| export class ImgElem implements LogEntry { | |||
| constructor (private url: string) { | |||