Skip to content

Commit

Permalink
support for all out attack and feint
Browse files Browse the repository at this point in the history
  • Loading branch information
danielrab-work committed Oct 29, 2021
1 parent 06d7919 commit c16370d
Show file tree
Hide file tree
Showing 24 changed files with 585 additions and 256 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- defense selector

## To Do list:
- make sure game.actors.get is never used
- all out attack/defense
- postures
- attack/defense/damage modifiers
Expand Down
18 changes: 10 additions & 8 deletions src/module/applications/abstract/BaseActorController.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
export default class BaseActorController extends Application {
actor: Actor;
import { ensureDefined } from '../../util/miscellaneous';

export default class BaseActorController extends Application {
static apps = new Map<string, BaseActorController>();

constructor(appName: string, actor: Actor, options: Partial<Application.Options>) {
const id = `${appName}-${actor.id}`;
token: Token;
actor: Actor;

constructor(appName: string, token: Token, options: Partial<Application.Options>) {
const id = `${appName}-${token.id}`;
super(mergeObject(Application.defaultOptions, { resizable: true, width: 600, id, ...options }));
this.actor = actor;
if (!this.actor) {
throw new Error('no actor');
}
this.token = token;
BaseActorController.apps.set(id, this);
ensureDefined(token.actor, 'token has no actor');
this.actor = token.actor;
}

async close(options?: Application.CloseOptions): Promise<void> {
Expand Down
83 changes: 83 additions & 0 deletions src/module/applications/abstract/BaseManeuverChooser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ChooserData } from '../../types.js';
import { MODULE_NAME } from '../../util/constants.js';
import { activateChooser } from '../../util/miscellaneous.js';
import BaseActorController from '../abstract/BaseActorController.js';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Maneuvers from '/systems/gurps/module/actor/maneuver.js';

//#region types
export interface ManeuverInfo {
tooltip: string;
page: string;
callback?: (token: Token) => void;
}
interface Maneuver extends ManeuverInfo {
name: string;
canAttack: boolean;
key: string;
}
interface GurpsManeuver {
changes: {
key: string;
mode: number;
priority: number;
value: string;
}[];
flags: {
gurps: {
alt: null;
defense: 'any' | 'none' | 'dodge-block';
fullturn: boolean;
icon: string;
move: 'step' | 'half' | 'full';
name: string;
};
};
icon: string;
id: 'maneuver';
label: string;
}
//#endregion

export default abstract class BaseManeuverChooser extends BaseActorController {
abstract maneuversInfo: Record<string, ManeuverInfo>;

constructor(appName: string, token: Token, options: Partial<Application.Options>) {
super(appName, token, {
title: `Maneuver Chooser - ${token.name}`,
template: `modules/${MODULE_NAME}/templates/maneuverChooser.hbs`,
...options,
});
}
getData(): ChooserData<['Maneuver', 'Description']> {
const maneuversDescriptions = this.getManeuversData().map((maneuver) => ({
Maneuver: maneuver.name,
Description: maneuver.tooltip,
}));
return { items: maneuversDescriptions, headers: ['Maneuver', 'Description'], id: 'manuever_choice' };
}
activateListeners(html: JQuery): void {
activateChooser(html, 'manuever_choice', (index) => {
const maneuver = this.getManeuversData()[index];
this.token.setManeuver(maneuver.key);
ChatMessage.create({
content: `${this.token.name} uses the "${maneuver.name}" maneuver [PDF:${maneuver.page}]`,
});
this.closeForEveryone();
maneuver.callback?.(this.token);
});
}

getManeuversData(): Maneuver[] {
const gurpsManeuvers: Record<string, GurpsManeuver> = Maneuvers.getAllData();
return Object.entries(this.maneuversInfo).map(([key, maneuverInfo]: [string, ManeuverInfo]) => {
return {
...maneuverInfo,
name: game.i18n.localize(gurpsManeuvers[key].label),
canAttack: key.includes('attack'),
key,
};
});
}
}
44 changes: 44 additions & 0 deletions src/module/applications/abstract/ControllerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export default function controllerFactory() {
class Controller extends Application {
actor: Actor;

static apps = new Map<string, Controller>();

constructor(appName: string, actor: Actor, options: Partial<Application.Options>) {
const id = `${appName}-${actor.id}`;
super(mergeObject(Application.defaultOptions, { resizable: true, width: 600, id, ...options }));
this.actor = actor;
if (!this.actor) {
throw new Error('no actor');
}
Controller.apps.set(id, this);
}

async close(options?: Application.CloseOptions): Promise<void> {
await super.close(options);
Controller.apps.delete(this.id);
}

static closeById(id: string): boolean {
const instance = Controller.apps.get(id);
if (!instance) return false;
instance.close();
return true;
}

closeForEveryone(): void {
EasyCombat.socket.executeForEveryone('closeController', this.id);
}

static async closeAll(): Promise<void> {
await Promise.all(
[...this.apps.values()].map(async (app) => {
if (app instanceof this) {
await app.close();
}
}),
);
}
}
return Controller;
}
72 changes: 34 additions & 38 deletions src/module/applications/attackChooser.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
import { getMeleeModifiers, getRangedModifiers, makeAttackInner } from '../attackWorkflow.js';
import { TEMPLATES_FOLDER } from '../util/constants.js';
import { getAttacks } from '../dataExtractor.js';
import { ChooserData, MeleeAttack, RangedAttack } from '../types.js';
import { ChooserData, MeleeAttack, PromiseFunctions, RangedAttack } from '../types.js';
import BaseActorController from './abstract/BaseActorController.js';
import { activateChooser, ensureDefined, getManeuver, getTargets } from '../util/miscellaneous.js';
import { activateChooser, checkSingleTarget, ensureDefined, getTargets } from '../util/miscellaneous.js';

interface AttackData {
isMoving: boolean;
meleeOnly?: boolean;
rangedOnly?: boolean;
keepOpen?: boolean;
}

export default class AttackChooser extends BaseActorController {
static modifiersGetters = {
melee: getMeleeModifiers,
ranged: getRangedModifiers,
};

data: AttackData;
attacks: {
melee: MeleeAttack[];
ranged: RangedAttack[];
};
promiseFuncs: PromiseFunctions<void> | undefined;

static modifiersGetters = {
melee: getMeleeModifiers,
ranged: getRangedModifiers,
};

constructor(actor: Actor, data: AttackData) {
super('AttackChooser', actor, {
title: `Attack Chooser - ${actor.name}`,
constructor(token: Token, data: AttackData = {}, promiseFuncs?: PromiseFunctions<void>) {
super('AttackChooser', token, {
title: `Attack Chooser - ${token.name}`,
template: `${TEMPLATES_FOLDER}/attackChooser.hbs`,
});
this.data = data;
this.attacks = getAttacks(this.actor);
this.promiseFuncs = promiseFuncs;
}
getData(): {
melee: ChooserData<['weapon', 'mode', 'level', 'damage', 'reach']>;
Expand Down Expand Up @@ -66,41 +70,33 @@ export default class AttackChooser extends BaseActorController {
};
}
activateListeners(html: JQuery): void {
const applicationBox = html[0].getBoundingClientRect();
html.find('.easy-combat-tooltiptext').each((_, e) => {
const tooltipBox = e.getBoundingClientRect();
// check if the tooltip clips the bottom of the dialog and move it up if needed
if (tooltipBox.bottom > applicationBox.bottom) {
$(e).css({ top: applicationBox.bottom - tooltipBox.bottom }); // the resulting number is negarive, moving the tooltip up
}
});
activateChooser(html, 'melee_attacks', (index) => this.makeAttack('melee', index));
activateChooser(html, 'ranged_attacks', (index) => this.makeAttack('ranged', index));
html.on('click', '#is_moving', (event) => {
this.data.isMoving = event.target.checked;
html.on('change', '#keepOpen', (event) => {
this.data.keepOpen = $(event.currentTarget).is(':checked');
});
}

makeAttack(mode: 'ranged' | 'melee', index: number): void {
async makeAttack(mode: 'ranged' | 'melee', index: number): Promise<void> {
ensureDefined(game.user, 'game not initialized');
if (!this.checkTargets(game.user)) return;
if (!checkSingleTarget(game.user)) return;
const target = getTargets(game.user)[0];
ensureDefined(target.actor, 'target has no actor');
const attack = getAttacks(this.actor)[mode][index];
const modifiers = AttackChooser.modifiersGetters[mode](attack as RangedAttack & MeleeAttack, {
maneuver: getManeuver(this.actor),
isMoving: this.data.isMoving,
});
makeAttackInner(this.actor, getTargets(game.user)[0], attack, mode, modifiers);
}

checkTargets(user: User): boolean {
if (user.targets.size === 0) {
ui.notifications?.warn('you must select a target');
return false;
const modifiers = AttackChooser.modifiersGetters[mode](attack as RangedAttack & MeleeAttack, this.token, target);
if (!this.data.keepOpen) {
this.close();
}
if (user.targets.size > 1) {
ui.notifications?.warn('you must select only one target');
return false;
await makeAttackInner(this.actor, target, attack, mode, modifiers);
if (this.promiseFuncs) {
this.promiseFuncs.resolve();
}
return true;
}

static request(token: Token, data?: AttackData): Promise<void> {
const promise = new Promise<void>((resolve, reject) => {
new AttackChooser(token, data, { resolve, reject }).render(true);
});
return promise;
}
}
58 changes: 34 additions & 24 deletions src/module/applications/defenseChooser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { TEMPLATES_FOLDER } from '../util/constants';
import { allOutAttackManeuvers, TEMPLATES_FOLDER } from '../util/constants';
import { getBlocks, getDodge, getParries } from '../dataExtractor';
import BaseActorController from './abstract/BaseActorController';
import { highestPriorityUsers, isDefined, smartRace } from '../util/miscellaneous';
import {
ensureDefined,
getManeuver,
getToken,
highestPriorityUsers,
isDefined,
smartRace,
} from '../util/miscellaneous';
import { Modifier } from '../types';
import { applyModifiers } from '../util/actions';

interface DefenseData {
resolve(value: boolean | PromiseLike<boolean>): void;
Expand All @@ -13,9 +21,9 @@ interface DefenseData {
export default class DefenseChooser extends BaseActorController {
data: DefenseData;

constructor(actor: Actor, data: DefenseData) {
super('DefenseChooser', actor, {
title: `Defense Chooser - ${actor.name}`,
constructor(token: Token, data: DefenseData) {
super('DefenseChooser', token, {
title: `Defense Chooser - ${token.name}`,
template: `${TEMPLATES_FOLDER}/defenseChooser.hbs`,
});
this.data = data;
Expand All @@ -29,6 +37,7 @@ export default class DefenseChooser extends BaseActorController {
}
activateListeners(html: JQuery): void {
html.on('click', '#dodge', () => {
applyModifiers(this.data.modifiers);
const result = GURPS.performAction(
{
orig: 'Dodge',
Expand All @@ -38,9 +47,10 @@ export default class DefenseChooser extends BaseActorController {
this.actor,
);
this.data.resolve(result);
this.close();
this.closeForEveryone();
});
html.on('click', '.parryRow', (event) => {
applyModifiers(this.data.modifiers);
const weapon = $(event.currentTarget).attr('weapon');
if (!weapon) {
ui.notifications?.error('no weapon attribute on clicked element');
Expand All @@ -55,9 +65,10 @@ export default class DefenseChooser extends BaseActorController {
this.actor,
);
this.data.resolve(result);
this.close();
this.closeForEveryone();
});
html.on('click', '.blockRow', (event) => {
applyModifiers(this.data.modifiers);
const weapon = $(event.currentTarget).attr('weapon');
if (!weapon) {
ui.notifications?.error('no weapon attribute on clicked element');
Expand All @@ -72,36 +83,35 @@ export default class DefenseChooser extends BaseActorController {
this.actor,
);
this.data.resolve(result);
this.close();
this.closeForEveryone();
});
}
static async attemptDefense(actorId: string, modifiers: Modifier[]): Promise<boolean> {
const actor = game.actors?.get(actorId);
if (!actor) {
throw new Error(`can't find actor with id ${actorId}`);
static async attemptDefense(sceneId: string, tokenId: string, modifiers: Modifier[]): Promise<boolean> {
const token = getToken(sceneId, tokenId);
const actor = token.actor;
ensureDefined(actor, 'token without actor');
if (allOutAttackManeuvers.includes(getManeuver(actor))) {
ChatMessage.create({ content: `${actor.name} can't defend because he is using all out attack` });
return false;
}
const promise = new Promise<boolean>((resolve, reject) => {
const instance = new DefenseChooser(actor, { resolve, reject, modifiers });
const instance = new DefenseChooser(token, { resolve, reject, modifiers });
instance.render(true);
});
console.log(promise);
return promise;
}
static async requestDefense(actor: Actor, modifiers: Modifier[]): Promise<boolean> {
static async requestDefense(token: Token, modifiers: Modifier[]): Promise<boolean> {
const actor = token.actor;
ensureDefined(actor, 'token has no actor');
const users = highestPriorityUsers(actor);
const result = await smartRace(
users
.filter((user) => actor.testUserPermission(user, 'OWNER'))
.map(async (user) => {
if (!user.id) {
ui.notifications?.error('user without id');
throw new Error('user without id');
}
if (!actor.id) {
ui.notifications?.error('target without id');
throw new Error('target without id');
}
return EasyCombat.socket.executeAsUser('attemptDefense', user.id, actor.id, modifiers);
ensureDefined(user.id, 'user without id');
ensureDefined(token.id, 'token without id');
ensureDefined(token.scene.id, 'scene without id');
return EasyCombat.socket.executeAsUser('attemptDefense', user.id, token.scene.id, token.id, modifiers);
}),
{ allowRejects: false, default: false, filter: isDefined },
);
Expand Down
Loading

0 comments on commit c16370d

Please sign in to comment.