Skip to content

Commit

Permalink
Merge pull request #7 from FightCore/feature/hitbox-timing
Browse files Browse the repository at this point in the history
Feature/hitbox timing
  • Loading branch information
bartdebever authored Aug 3, 2024
2 parents 1307d94 + c23982f commit 3226dcb
Show file tree
Hide file tree
Showing 59 changed files with 4,397 additions and 3,177 deletions.
4 changes: 1 addition & 3 deletions website/components/moves/crouch-cancel-section.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Hitbox } from "@/models/hitbox";
import { CrouchCancelTable } from "./crouch-cancel-table";
import { Hit } from "@/models/hit";

Expand All @@ -7,15 +6,14 @@ export interface CrouchCancelSectionParams {
}

export function CrouchCancelSection(params: Readonly<CrouchCancelSectionParams>) {
const hitboxes = params.hits.flatMap((hit) => hit.hitboxes);
return (
<div>
<h2 className="text-xl font-bold">Crouch Cancel Percentages</h2>
<p className="mb-2">
The following percentages indicate when ASDI Down and Crouch Cancel are broken for this move. This is dependant
which hitbox you are hit with.
</p>
<CrouchCancelTable hitboxes={hitboxes} />
<CrouchCancelTable hits={params.hits} />
</div>
);
}
186 changes: 124 additions & 62 deletions website/components/moves/crouch-cancel-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,113 @@ import { Radio, RadioGroup } from "@nextui-org/radio";
import React, { useEffect } from "react";
import { LOCAL_STORAGE_PREFERRED_CC_FLOOR, LOCAL_STORAGE_PREFERRED_CC_SORT } from "@/keys/local-storage-keys";
import { Checkbox } from "@nextui-org/checkbox";
import { Tooltip } from "@nextui-org/tooltip";
import { FaCircleQuestion } from "react-icons/fa6";
import { Hit } from "@/models/hit";
import { areAllHitboxesEqual, areHitboxesEqual } from "@/utilities/hitbox-utils";

export interface CrouchCancelTableParams {
hitboxes: Hitbox[];
hits: Hit[];
}

export enum CrouchCancelSort {
ALPHABETICAL = "alphabetical",
WEIGHT = "weight",
}

function generateCard(
knockbackTarget: number,
title: string,
hitbox: Hitbox,
sortedCharacters: CharacterBase[],
floorPercentages: boolean
) {
return (
<div className="w-full md:w-1/2 p-2">
<Card className="dark:bg-gray-800">
<CardHeader>{title}</CardHeader>
<CardBody>
<div className="grid md:grid-cols-5 grid-cols-3">
{sortedCharacters.map((character) => {
let percentage = calculateCrouchCancelPercentage(hitbox, character, knockbackTarget, floorPercentages);
return (
<div key={knockbackTarget + character.fightCoreId}>
<Image alt={character.name} width={40} height={40} src={"/newicons/" + character.name + ".webp"} />
<span className="d-inline">{percentage}</span>
</div>
);
})}
</div>
</CardBody>
</Card>
</div>
);
}

function generateUnableToCCTab(hitbox: Hitbox) {
return (
<Tab key={hitbox.id} title={hitbox.name} className="md:flex">
<Card className="dark:bg-gray-800">
<CardBody>{getCrouchCancelImpossibleReason(hitbox)}</CardBody>
</Card>
</Tab>
);
}

function hitName(hit: Hit): string {
if (hit.name) {
return hit.name;
}

return `${hit.start} - ${hit.end}`;
}

function sortCharacters(characterA: CharacterBase, characterB: CharacterBase, sort: CrouchCancelSort): number {
if (sort === CrouchCancelSort.WEIGHT) {
return characterA.characterStatistics.weight < characterB.characterStatistics.weight ? 1 : -1;
}

return characterA.name > characterB.name ? 1 : -1;
}

function preprocessHits(hits: Hit[]): Hit[] {
const newHits: Hit[] = [];
for (const hit of hits) {
if (areAllHitboxesEqual(hit.hitboxes)) {
const newHitbox = structuredClone(hit.hitboxes[0]);
newHitbox.name = "All Hitboxes";
const newHit = structuredClone(hit);
newHit.hitboxes = [newHitbox];
newHits.push(newHit);
} else {
newHits.push(structuredClone(hit));
}
}

// Merge hits together if the hitboxes are the same
for (let i = 0; i < newHits.length; i++) {
for (let j = i + 1; j < newHits.length; j++) {
const areAllHitboxesEqual = newHits[i].hitboxes.every((hitA) => {
const correspondingHitbox = newHits[j].hitboxes.find((hitboxTwo) => hitboxTwo.name === hitA.name);
return correspondingHitbox && areHitboxesEqual(hitA, correspondingHitbox);
});

if (areAllHitboxesEqual) {
newHits[i].end = newHits[j].end;
newHits[i].name = `Hits ${newHits[i].start} - ${newHits[i].end}`;
newHits.splice(j, 1);
j--;
}
}
}

if (hits.length !== 1 && newHits.length === 1) {
newHits[0].name = "All hits";
}

return newHits;
}

export function CrouchCancelTable(params: Readonly<CrouchCancelTableParams>) {
const data = preprocessHits(params.hits);
const [selected, setSelected] = React.useState(CrouchCancelSort.ALPHABETICAL);
const [floorPercentages, setFloorPercentages] = React.useState(true);
const localCharacters = characters
Expand Down Expand Up @@ -84,66 +178,34 @@ export function CrouchCancelTable(params: Readonly<CrouchCancelTableParams>) {
</Checkbox>
</div>

<Tabs aria-label="Crouch Cancel and ASDI Tabs" disableAnimation>
{params.hitboxes.map((hitbox) => {
if (isCrouchCancelPossible(hitbox)) {
return (
<Tab key={hitbox.id} title={hitbox.name} className="md:flex">
{GenerateCard(80, "ASDI Down", hitbox, sortedCharacters, floorPercentages)}
{GenerateCard(120, "Crouch Cancel", hitbox, sortedCharacters, floorPercentages)}
</Tab>
);
}
return generateUnableToCCTab(hitbox);
})}
<Tabs
aria-label="Crouch Cancel and ASDI Tabs"
disableAnimation
placement="top"
className="max-w-full w-max overflow-x-scroll"
>
{data.map((hit) => (
<Tab key={hit.id} title={hitName(hit)}>
<Tabs
aria-label="Crouch Cancel and ASDI Tabs"
disableAnimation
className="grid grid-cols-1 md:grid-cols-2 mb-2"
>
{hit.hitboxes.map((hitbox) => {
if (isCrouchCancelPossible(hitbox)) {
return (
<Tab key={hitbox.id} title={hitbox.name} className="md:flex">
{generateCard(80, "ASDI Down", hitbox, sortedCharacters, floorPercentages)}
{generateCard(120, "Crouch Cancel", hitbox, sortedCharacters, floorPercentages)}
</Tab>
);
}
return generateUnableToCCTab(hitbox);
})}
</Tabs>
</Tab>
))}
</Tabs>
</>
);
}

function GenerateCard(
knockbackTarget: number,
title: string,
hitbox: Hitbox,
sortedCharacters: CharacterBase[],
floorPercentages: boolean
) {
return (
<div className="w-full md:w-1/2 p-2">
<Card className="dark:bg-gray-800">
<CardHeader>{title}</CardHeader>
<CardBody>
<div className="grid md:grid-cols-5 grid-cols-3">
{sortedCharacters.map((character) => {
let percentage = calculateCrouchCancelPercentage(hitbox, character, knockbackTarget, floorPercentages);
return (
<div key={knockbackTarget + character.fightCoreId}>
<Image alt={character.name} width={40} height={40} src={"/newicons/" + character.name + ".webp"} />
<span className="d-inline">{percentage}</span>
</div>
);
})}
</div>
</CardBody>
</Card>
</div>
);
}

function generateUnableToCCTab(hitbox: Hitbox) {
return (
<Tab key={hitbox.id} title={hitbox.name} className="md:flex">
<Card className="dark:bg-gray-800">
<CardBody>{getCrouchCancelImpossibleReason(hitbox)}</CardBody>
</Card>
</Tab>
);
}

function sortCharacters(characterA: CharacterBase, characterB: CharacterBase, sort: CrouchCancelSort): number {
if (sort === CrouchCancelSort.WEIGHT) {
return characterA.characterStatistics.weight < characterB.characterStatistics.weight ? 1 : -1;
}

return characterA.name > characterB.name ? 1 : -1;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { CharacterBase } from "@/models/character";
import { Move } from "@/models/move";
import { calculateCrouchCancelPercentage } from "@/utilities/crouch-cancel-calculator";
import {
calculateCrouchCancelPercentage,
getCrouchCancelImpossibleReason,
isCrouchCancelPossible,
} from "@/utilities/crouch-cancel-calculator";
import { processDuplicateHitboxes, processDuplicateHits } from "@/utilities/hitbox-utils";
import { moveRoute } from "@/utilities/routes";
import { Link } from "@nextui-org/link";
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@nextui-org/table";
import { flattenData, FlattenedHitbox } from "../hitboxes/hitbox-table-columns";
import { Hitbox } from "@/models/hitbox";

export interface CrouchCancelMoveOverviewTableParams {
character: CharacterBase;
Expand All @@ -13,44 +20,72 @@ export interface CrouchCancelMoveOverviewTableParams {
floorPercentage: boolean;
}

export function CrouchCancelMoveOverviewTable(data: Readonly<CrouchCancelMoveOverviewTableParams>) {
interface ExpandedFlattenedHitbox extends FlattenedHitbox {
move: Move;
}

function generateHitboxPercentage(hitbox: Hitbox, target: CharacterBase, knockbackTarget: number, floor: boolean) {
if (!isCrouchCancelPossible(hitbox)) {
return getCrouchCancelImpossibleReason(hitbox);
}

return calculateCrouchCancelPercentage(hitbox, target, knockbackTarget, floor);
}

export function CrouchCancelMoveOverviewTable(params: Readonly<CrouchCancelMoveOverviewTableParams>) {
const classNames = {
wrapper: ["dark:bg-gray-800", "border-0", "shadow-none", "p-0"],
th: ["bg-transparent", "text-default-500", "border-b", "border-divider"],
td: ["text-default-600", "py-1", "group-data-[odd=true]:before:dark:bg-gray-700"],
};

const flattenedHits = params.moves.flatMap((move) => {
if (!move.hits || move.hits.length === 0) {
return [];
}

const processedHits = processDuplicateHitboxes(move.hits!);
const data = processDuplicateHits(flattenData(processedHits));
return data.map((hitbox) => ({
...hitbox,
move: move,
}));
});

let previousName = "";

return (
<Table classNames={classNames} isStriped>
<TableHeader>
<TableColumn>Name</TableColumn>
<TableColumn>Hit</TableColumn>
<TableColumn>Hitbox</TableColumn>
<TableColumn>Breaks at percentage</TableColumn>
</TableHeader>
<TableBody>
{data.moves
.filter((move) => move.hitboxes && move.hitboxes.length > 0)
.map((move) => {
return (
<TableRow key={move.id.toString()}>
<TableCell>
<Link href={moveRoute(data.character, move)}>{move.name}</Link>
</TableCell>
<TableCell>
{move.hitboxes?.map((hitbox) => (
<div key={hitbox.id.toString() + "name"}>{hitbox.name}</div>
))}
</TableCell>
<TableCell>
{move.hitboxes?.map((hitbox) => (
<div key={hitbox.id + "percentage"}>
{calculateCrouchCancelPercentage(hitbox, data.target, data.knockbackTarget, data.floorPercentage)}
</div>
))}
</TableCell>
</TableRow>
);
})}
{flattenedHits.map((hitbox) => {
const html = (
<TableRow key={hitbox.id.toString()}>
<TableCell>
{previousName === hitbox.move.name ? (
""
) : (
<Link href={moveRoute(params.character, hitbox.move)}>{hitbox.move.name}</Link>
)}
</TableCell>
<TableCell width={100} className="md:text-center">
{hitbox.hit}
</TableCell>
<TableCell>{hitbox.name}</TableCell>
<TableCell className="md:text-center">
{generateHitboxPercentage(hitbox, params.target, params.knockbackTarget, params.floorPercentage)}
</TableCell>
</TableRow>
);

previousName = hitbox.move.name;
return html;
})}
</TableBody>
</Table>
);
Expand Down
15 changes: 2 additions & 13 deletions website/components/moves/hitbox-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Hitbox } from "@/models/hitbox";
import HitboxTable from "./hitbox-table";
import { Hit } from "@/models/hit";
import HitboxTable from "./hitboxes/hitbox-table";

export interface HitboxSectionParams {
hits: Hit[];
Expand All @@ -10,17 +9,7 @@ export function HitboxSection(params: Readonly<HitboxSectionParams>) {
return (
<div>
<h2 className="text-xl font-bold">Hitboxes</h2>
{params.hits.map((hit) => {
return (
<div key={hit.id}>
<h2>DEBUG ID {hit.id}</h2>{" "}
<span className="text-xl text-bold">
Frame {hit.start} - {hit.end}
</span>
<HitboxTable hitboxes={hit.hitboxes} />
</div>
);
})}
<HitboxTable hits={params.hits}></HitboxTable>
</div>
);
}
Loading

0 comments on commit 3226dcb

Please sign in to comment.