| @@ -11,7 +11,7 @@ import Combat from './components/Combat.vue' | |||||
| import Header from './components/Header.vue' | import Header from './components/Header.vue' | ||||
| import * as Creatures from '@/game/creatures' | import * as Creatures from '@/game/creatures' | ||||
| import { Creature, POV } from '@/game/entity' | import { Creature, POV } from '@/game/entity' | ||||
| import { ProperNoun } from '@/game/language' | |||||
| import { ProperNoun, TheyPronouns, FemalePronouns, MalePronouns } from '@/game/language' | |||||
| @Component({ | @Component({ | ||||
| components: { | components: { | ||||
| @@ -24,15 +24,38 @@ export default class App extends Vue { | |||||
| combatants: Array<Creature> | combatants: Array<Creature> | ||||
| constructor () { | constructor () { | ||||
| super() | super() | ||||
| this.left = new Creatures.Wolf() | |||||
| this.left.name = new ProperNoun("Wolf") | |||||
| this.right = new Creatures.Cafat() | |||||
| const wolf1 = new Creatures.Wolf() | |||||
| wolf1.name = new ProperNoun("Innermost Wolf") | |||||
| const wolf2 = new Creatures.Wolf() | |||||
| wolf2.name = new ProperNoun("Inner Wolf") | |||||
| this.combatants = [this.left, this.right, wolf1, wolf2] | |||||
| this.left.perspective = POV.First | |||||
| const fighter = new Creatures.Human(new ProperNoun('Fighter'), TheyPronouns, { | |||||
| stats: { | |||||
| Toughness: 40, | |||||
| Power: 50, | |||||
| Speed: 30, | |||||
| Willpower: 40, | |||||
| Charm: 20 | |||||
| } | |||||
| }) | |||||
| const rogue = new Creatures.Human(new ProperNoun('Wizard'), MalePronouns, { | |||||
| stats: { | |||||
| Toughness: 25, | |||||
| Power: 40, | |||||
| Speed: 70, | |||||
| Willpower: 50, | |||||
| Charm: 80 | |||||
| } | |||||
| }) | |||||
| const wizard = new Creatures.Human(new ProperNoun('Rogue'), FemalePronouns, { | |||||
| stats: { | |||||
| Toughness: 30, | |||||
| Power: 20, | |||||
| Speed: 50, | |||||
| Willpower: 80, | |||||
| Charm: 60 | |||||
| } | |||||
| }) | |||||
| this.left = fighter | |||||
| this.right = new Creatures.Withers() | |||||
| this.combatants = [this.left, this.right, wizard, rogue] | |||||
| console.log(this.left) | console.log(this.left) | ||||
| console.log(this.right) | console.log(this.right) | ||||
| } | } | ||||
| @@ -1,17 +1,11 @@ | |||||
| <template> | <template> | ||||
| <div class="combat-layout"> | <div class="combat-layout"> | ||||
| <div class="left-selector"> | |||||
| <button class="combatant-picker" @click="left = combatant" v-for="(combatant, index) in combatants.filter(c => c !== right && c !== left)" :key="'left' + index"> | |||||
| {{ combatant.name }} | |||||
| </button> | |||||
| <div id="left-stats"> | |||||
| <Statblock v-on:click.native="left = combatant" class="left-stats" :data-active="combatant === left" v-for="combatant in combatants.filter(c => c.side == Side.Heroes && c.vigors.Health > 0)" v-bind:key="combatant.name" :subject="combatant" /> | |||||
| </div> | </div> | ||||
| <div class="right-selector"> | |||||
| <button class="combatant-picker" @click="right = combatant" v-for="(combatant, index) in combatants.filter(c => c !== left && c !== right)" :key="'right' + index"> | |||||
| {{ combatant.name }} | |||||
| </button> | |||||
| <div id="right-stats"> | |||||
| <Statblock v-on:click.native="right = combatant" class="right-stats" :data-active="combatant === right" v-for="combatant in combatants.filter(c => c.side == Side.Monsters && c.vigors.Health > 0)" v-bind:key="combatant.name" :subject="combatant" /> | |||||
| </div> | </div> | ||||
| <Statblock class="left-stats" :subject="left" /> | |||||
| <Statblock class="right-stats" :subject="right" /> | |||||
| <div id="log"> | <div id="log"> | ||||
| </div> | </div> | ||||
| <div class="left-actions"> | <div class="left-actions"> | ||||
| @@ -42,6 +36,7 @@ import { Creature, POV } from '@/game/entity' | |||||
| import { LogEntry } from '@/game/interface' | import { LogEntry } from '@/game/interface' | ||||
| import Statblock from './Statblock.vue' | import Statblock from './Statblock.vue' | ||||
| import ActionButton from './ActionButton.vue' | import ActionButton from './ActionButton.vue' | ||||
| import { Side } from '@/game/combat' | |||||
| @Component( | @Component( | ||||
| { | { | ||||
| @@ -58,6 +53,8 @@ export default class Combat extends Vue { | |||||
| @Prop() | @Prop() | ||||
| combatants!: Array<Creature> | combatants!: Array<Creature> | ||||
| Side = Side | |||||
| actionDescription = '' | actionDescription = '' | ||||
| constructor () { | constructor () { | ||||
| @@ -121,14 +118,6 @@ export default class Combat extends Vue { | |||||
| flex: 10; | flex: 10; | ||||
| } | } | ||||
| .left-stats { | |||||
| grid-area: 2 / 1 / 3 / 2 | |||||
| } | |||||
| .right-stats { | |||||
| grid-area: 2 / 3 / 3 / 4; | |||||
| } | |||||
| #log { | #log { | ||||
| grid-area: main-row-start / main-col-start / main-row-end / main-col-end; | grid-area: main-row-start / main-col-start / main-row-end / main-col-end; | ||||
| overflow-y: scroll; | overflow-y: scroll; | ||||
| @@ -146,12 +135,20 @@ export default class Combat extends Vue { | |||||
| grid-area: 1 / 3 / 2 / 4; | grid-area: 1 / 3 / 2 / 4; | ||||
| } | } | ||||
| #left-stats { | |||||
| grid-area: 2 / 1 / 4 / 2 | |||||
| } | |||||
| #right-stats { | |||||
| grid-area: 2 / 3 / 4 / 4; | |||||
| } | |||||
| .left-actions { | .left-actions { | ||||
| grid-area: 4 / 1 / 6 / 2; | |||||
| grid-area: 5 / 1 / 6 / 2; | |||||
| } | } | ||||
| .right-actions { | .right-actions { | ||||
| grid-area: 4 / 3 / 6 / 4; | |||||
| grid-area: 5 / 3 / 6 / 4; | |||||
| } | } | ||||
| #action-desc { | #action-desc { | ||||
| @@ -2,7 +2,7 @@ | |||||
| <div class="statblock"> | <div class="statblock"> | ||||
| <h2 v-if="subject.perspective === firstperson">You</h2> | <h2 v-if="subject.perspective === firstperson">You</h2> | ||||
| <h2 v-if="subject.perspective !== firstperson">{{subject.name.all.capital}}</h2> | <h2 v-if="subject.perspective !== firstperson">{{subject.name.all.capital}}</h2> | ||||
| <div class="stat-line"> | |||||
| <div class="stat-line vigors"> | |||||
| <div :class="vigorClass(subject.vigors[vigor], subject.maxVigors[vigor])" v-for="vigor in Object.keys(subject.vigors)" v-bind:key="vigor"> | <div :class="vigorClass(subject.vigors[vigor], subject.maxVigors[vigor])" v-for="vigor in Object.keys(subject.vigors)" v-bind:key="vigor"> | ||||
| <i :class="vigorIcons[vigor]" /> | <i :class="vigorIcons[vigor]" /> | ||||
| <div>{{subject.vigors[vigor]}}</div> | <div>{{subject.vigors[vigor]}}</div> | ||||
| @@ -12,8 +12,7 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <br> | |||||
| <div class="stat-line"> | |||||
| <div class="stat-line stats"> | |||||
| <div :class="statClass(subject.stats[stat], subject.baseStats[stat])" v-for="stat in Object.keys(subject.stats)" v-bind:key="stat"> | <div :class="statClass(subject.stats[stat], subject.baseStats[stat])" v-for="stat in Object.keys(subject.stats)" v-bind:key="stat"> | ||||
| <i :class="statIcons[stat]" /> | <i :class="statIcons[stat]" /> | ||||
| <div>{{subject.stats[stat]}}</div> | <div>{{subject.stats[stat]}}</div> | ||||
| @@ -23,8 +22,7 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <br> | |||||
| <div class="stat-line"> | |||||
| <div class="stat-line vore-stats"> | |||||
| <div class="stat-entry" v-for="stat in Object.keys(subject.voreStats)" v-bind:key="stat"> | <div class="stat-entry" v-for="stat in Object.keys(subject.voreStats)" v-bind:key="stat"> | ||||
| <i :class="voreStatIcons[stat]" /> | <i :class="voreStatIcons[stat]" /> | ||||
| <div>{{subject.voreStats[stat]}}</div> | <div>{{subject.voreStats[stat]}}</div> | ||||
| @@ -34,9 +32,7 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <br> | |||||
| <div>Status: {{subject.status}}</div> | <div>Status: {{subject.status}}</div> | ||||
| <ContainerView v-for="container in subject.containers" :key="container.name.toString()" :container="container" /> | |||||
| </div> | </div> | ||||
| </template> | </template> | ||||
| @@ -45,7 +41,7 @@ import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator' | |||||
| import { Creature, POV } from '@/game/entity' | import { Creature, POV } from '@/game/entity' | ||||
| import { Stats, Stat, StatIcons, StatDescs, Vigor, VigorIcons, VigorDescs, VoreStatDescs, VoreStatIcons } from '@/game/combat' | import { Stats, Stat, StatIcons, StatDescs, Vigor, VigorIcons, VigorDescs, VoreStatDescs, VoreStatIcons } from '@/game/combat' | ||||
| import ContainerView from './ContainerView.vue' | import ContainerView from './ContainerView.vue' | ||||
| import tippy from 'tippy.js' | |||||
| import tippy, { createSingleton } from 'tippy.js' | |||||
| import 'tippy.js/dist/tippy.css' | import 'tippy.js/dist/tippy.css' | ||||
| @Component({ | @Component({ | ||||
| @@ -88,13 +84,14 @@ export default class Statblock extends Vue { | |||||
| firstperson: POV = POV.First | firstperson: POV = POV.First | ||||
| mounted () { | mounted () { | ||||
| this.$el.querySelectorAll(".stat-entry").forEach(elem => { | |||||
| const tippyInstances = Array.from(this.$el.querySelectorAll(".stat-entry")).map(elem => { | |||||
| const tooltip = elem.querySelector(".tooltip-template") as HTMLElement | const tooltip = elem.querySelector(".tooltip-template") as HTMLElement | ||||
| tippy(elem, { | |||||
| return tippy(elem, { | |||||
| content: tooltip | content: tooltip | ||||
| }) | }) | ||||
| }) | }) | ||||
| createSingleton(tippyInstances, { delay: 500 }) | |||||
| } | } | ||||
| } | } | ||||
| </script> | </script> | ||||
| @@ -102,7 +99,7 @@ export default class Statblock extends Vue { | |||||
| <!-- Add "scoped" attribute to limit CSS to this component only --> | <!-- Add "scoped" attribute to limit CSS to this component only --> | ||||
| <style scoped> | <style scoped> | ||||
| h2 { | h2 { | ||||
| margin-bottom: 16pt; | |||||
| margin-bottom: 8pt; | |||||
| font-size: 200%; | font-size: 200%; | ||||
| } | } | ||||
| ul { | ul { | ||||
| @@ -118,6 +115,7 @@ a { | |||||
| } | } | ||||
| .statblock { | .statblock { | ||||
| margin: 16px; | margin: 16px; | ||||
| user-select: none; | |||||
| } | } | ||||
| .stat-line { | .stat-line { | ||||
| @@ -127,6 +125,11 @@ a { | |||||
| flex-wrap: wrap; | flex-wrap: wrap; | ||||
| } | } | ||||
| .stats, | |||||
| .vore-stats { | |||||
| display: none; | |||||
| } | |||||
| .stat-entry { | .stat-entry { | ||||
| position: relative; | position: relative; | ||||
| font-size: 16pt; | font-size: 16pt; | ||||
| @@ -149,6 +152,16 @@ a { | |||||
| .stat-entry.buff { | .stat-entry.buff { | ||||
| color: green; | color: green; | ||||
| } | } | ||||
| .statblock[data-active] { | |||||
| background: #444; | |||||
| border-radius: 4px; | |||||
| } | |||||
| .statblock[data-active] .stats, | |||||
| .statblock[data-active] .vore-stats { | |||||
| display: flex; | |||||
| } | |||||
| </style> | </style> | ||||
| <style> | <style> | ||||
| @@ -197,13 +197,17 @@ export class UniformRandomDamageFormula implements DamageFormula { | |||||
| } | } | ||||
| } | } | ||||
| export enum Side { | |||||
| Heroes, | |||||
| Monsters | |||||
| } | |||||
| /** | /** | ||||
| * A Combatant has a list of possible actions to take. | |||||
| * | |||||
| * This may be merged with [[Actionable]] | |||||
| * A Combatant has a list of possible actions to take, as well as a side. | |||||
| */ | */ | ||||
| export interface Combatant { | export interface Combatant { | ||||
| actions: Array<Action>; | actions: Array<Action>; | ||||
| side: Side; | |||||
| } | } | ||||
| /** | /** | ||||
| @@ -1,5 +1,7 @@ | |||||
| import { Wolf } from './creatures/wolf' | import { Wolf } from './creatures/wolf' | ||||
| import { Player } from './creatures/player' | import { Player } from './creatures/player' | ||||
| import { Cafat } from './creatures/cafat' | import { Cafat } from './creatures/cafat' | ||||
| import { Human } from './creatures/human' | |||||
| import { Withers } from './creatures/withers' | |||||
| export { Wolf, Player, Cafat } | |||||
| export { Wolf, Player, Cafat, Human, Withers } | |||||
| @@ -0,0 +1,26 @@ | |||||
| import { Creature, POV, Entity } from '../entity' | |||||
| import { Stat, Damage, DamageType, ConstantDamageFormula, Vigor, Stats } from '../combat' | |||||
| import { MalePronouns, ImproperNoun, POVPair, POVPairArgs, Noun, Pronoun } from '../language' | |||||
| import { LogLine, LogLines } from '../interface' | |||||
| import { VoreType, Stomach, Bowels } from '../vore' | |||||
| import { StatTest } from '../combat/tests' | |||||
| import { AttackAction, TransferAction, FeedAction } from '../combat/actions' | |||||
| export class Human extends Creature { | |||||
| constructor (name: Noun, pronouns: Pronoun, options: { | |||||
| stats?: Stats; | |||||
| } = {}) { | |||||
| let stats | |||||
| if (options.stats === undefined) { | |||||
| stats = { Toughness: 20, Power: 20, Speed: 20, Willpower: 20, Charm: 20 } | |||||
| } else { | |||||
| stats = options.stats | |||||
| } | |||||
| super(name, MalePronouns, stats, new Set([VoreType.Oral, VoreType.Anal]), new Set([VoreType.Oral, VoreType.Anal]), 25) | |||||
| this.actions.push(new AttackAction(new ConstantDamageFormula( | |||||
| new Damage( | |||||
| { amount: 20, target: Vigor.Health, type: DamageType.Slash } | |||||
| ) | |||||
| ))) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| import { Creature, POV, Entity } from '../entity' | |||||
| import { Stat, Damage, DamageType, ConstantDamageFormula, Vigor, Side, PairAction, CombatTest } from '../combat' | |||||
| import { MalePronouns, ImproperNoun, POVPair, POVPairArgs, ProperNoun, TheyPronouns } from '../language' | |||||
| import { LogLine, LogLines, LogEntry } from '../interface' | |||||
| import { VoreType, Stomach, Bowels } from '../vore' | |||||
| import { StatTest } from '../combat/tests' | |||||
| import { AttackAction, TransferAction, FeedAction } from '../combat/actions' | |||||
| class BiteAction extends AttackAction { | |||||
| constructor () { | |||||
| super(new ConstantDamageFormula(new Damage({ amount: 50, type: DamageType.Slash, target: Vigor.Health }))) | |||||
| this.name = "Bite" | |||||
| } | |||||
| } | |||||
| export class Withers extends Creature { | |||||
| constructor () { | |||||
| super( | |||||
| new ProperNoun('Withers'), | |||||
| TheyPronouns, | |||||
| { Toughness: 60, Power: 100, Speed: 40, Willpower: 60, Charm: 120 }, | |||||
| new Set(), | |||||
| new Set([VoreType.Oral]), | |||||
| 5000) | |||||
| this.actions.push(new BiteAction()) | |||||
| this.side = Side.Monsters | |||||
| const stomach = new Stomach(this, 50, new Damage( | |||||
| { amount: 30, type: DamageType.Acid, target: Vigor.Health }, | |||||
| { amount: 20, type: DamageType.Crush, target: Vigor.Stamina }, | |||||
| { amount: 20, type: DamageType.Dominance, target: Vigor.Resolve } | |||||
| )) | |||||
| this.containers.push(stomach) | |||||
| this.otherActions.push(new FeedAction(stomach)) | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,4 @@ | |||||
| import { DamageType, Damage, Combatant, Stats, Action, Vigor, VoreStats, VoreStat, Stat } from './combat' | |||||
| import { DamageType, Damage, Combatant, Stats, Action, Vigor, VoreStats, VoreStat, Stat, Side } from './combat' | |||||
| import { Noun, Pronoun } from './language' | import { Noun, Pronoun } from './language' | ||||
| import { LogEntry, LogLine } from './interface' | import { LogEntry, LogLine } from './interface' | ||||
| import { Vore, Container, VoreType } from './vore' | import { Vore, Container, VoreType } from './vore' | ||||
| @@ -38,6 +38,7 @@ export class Creature extends Vore implements Combatant { | |||||
| baseStats: Stats | baseStats: Stats | ||||
| voreStats: VoreStats | voreStats: VoreStats | ||||
| side: Side | |||||
| get disabled (): boolean { | get disabled (): boolean { | ||||
| return Object.values(this.vigors).some(val => val <= 0) | return Object.values(this.vigors).some(val => val <= 0) | ||||
| @@ -59,7 +60,7 @@ export class Creature extends Vore implements Combatant { | |||||
| super() | super() | ||||
| const containers = this.containers | const containers = this.containers | ||||
| this.baseStats = Object.keys(Stat).reduce((base: any, key) => { base[key] = stats[key as Stat]; return base }, {}) | this.baseStats = Object.keys(Stat).reduce((base: any, key) => { base[key] = stats[key as Stat]; return base }, {}) | ||||
| this.side = Side.Heroes | |||||
| this.voreStats = { | this.voreStats = { | ||||
| get [VoreStat.Bulk] () { | get [VoreStat.Bulk] () { | ||||
| console.log(containers) | console.log(containers) | ||||
| @@ -137,7 +138,7 @@ export class Creature extends Vore implements Combatant { | |||||
| return "Broken" | return "Broken" | ||||
| } | } | ||||
| if (this.containedIn !== null) { | if (this.containedIn !== null) { | ||||
| return "Devoured" | |||||
| return `Devoured by ${this.containedIn.owner.name}` | |||||
| } | } | ||||
| return "Normal" | return "Normal" | ||||