Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #389: Jutsu Level Transfer #390

Merged
merged 4 commits into from
Feb 22, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/drizzle/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const JutsuTypes = [
"EVENT",
"AI",
] as const;
export type JutsuType = (typeof JutsuTypes)[number];

export const UserStatNames = [
"ninjutsuOffence",
Expand Down Expand Up @@ -442,6 +443,15 @@ export const COST_REROLL_ELEMENT = 20;
export const MAX_EXTRA_JUTSU_SLOTS = 2;
export const BLOODLINE_ROLL_TYPES = ["NATURAL", "ITEM"] as const;

// Jutsu level transfer config
export const JUTSU_TRANSFER_DAYS = 20;
export const JUTSU_TRANSFER_COST = 20;
export const JUTSU_TRANSFER_MAX_LEVEL = 20;
export const JUTSU_TRANSFER_FREE_AMOUNT = 2;
export const JUTSU_TRANSFER_FREE_NORMAL = 3;
export const JUTSU_TRANSFER_FREE_SILVER = 4;
export const JUTSU_TRANSFER_FREE_GOLD = 5;

// Village config
export const VILLAGE_LEAVE_REQUIRED_RANK = "CHUNIN";
export const VILLAGE_REDUCED_GAINS_DAYS = 7;
Expand Down
103 changes: 101 additions & 2 deletions app/src/app/jutsus/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState } from "react";
import { Trash2, CircleFadingArrowUp } from "lucide-react";
import { Trash2, CircleFadingArrowUp, ArrowRightLeft } from "lucide-react";
import ItemWithEffects from "@/layout/ItemWithEffects";
import ContentBox from "@/layout/ContentBox";
import Modal from "@/layout/Modal";
Expand Down Expand Up @@ -29,8 +29,13 @@ import { showMutationToast } from "@/libs/toast";
import { JUTSU_XP_TO_LEVEL } from "@/drizzle/constants";
import { COST_EXTRA_JUTSU_SLOT } from "@/drizzle/constants";
import { MAX_EXTRA_JUTSU_SLOTS } from "@/drizzle/constants";
import { JUTSU_TRANSFER_COST } from "@/drizzle/constants";
import { JUTSU_TRANSFER_MAX_LEVEL } from "@/drizzle/constants";
import { JUTSU_TRANSFER_DAYS } from "@/drizzle/constants";
import { getFreeTransfers } from "@/libs/jutsu";
import JutsuFiltering, { useFiltering, getFilter } from "@/layout/JutsuFiltering";
import type { Jutsu, UserJutsu } from "@/drizzle/schema";
import type { RouterOutputs } from "@/app/_trpc/client";

export default function MyJutsu() {
// tRPC utility
Expand All @@ -46,6 +51,9 @@ export default function MyJutsu() {
const [userjutsu, setUserJutsu] = useState<(Jutsu & UserJutsu) | undefined>(
undefined,
);
const [transferTarget, setTransferTarget] = useState<(Jutsu & UserJutsu) | undefined>(
undefined,
);

// User Jutsus & items
const { data: userJutsus, isFetching: l1 } = api.jutsu.getUserJutsus.useQuery(
Expand All @@ -56,6 +64,9 @@ export default function MyJutsu() {
undefined,
{ enabled: !!userData },
);
const { data: recentTransfers } = api.jutsu.getRecentTransfers.useQuery(undefined, {
enabled: !!userData,
});

const userJutsuCounts = userJutsus?.map((userJutsu) => {
return {
Expand All @@ -67,10 +78,15 @@ export default function MyJutsu() {
};
});

// Transfer costs
const freeTransfers = getFreeTransfers(userData?.federalStatus || "NONE");
const usedTransfers = recentTransfers?.length || 0;

const onSettled = () => {
document.body.style.cursor = "default";
setIsOpen(false);
setUserJutsu(undefined);
setTransferTarget(undefined);
};

// Mutations
Expand Down Expand Up @@ -133,7 +149,24 @@ export default function MyJutsu() {
},
});

const isPending = isToggling || isForgetting || isUpgrading || isUnequipping;
const { mutate: transferLevel, isPending: isTransferring } =
api.jutsu.transferLevel.useMutation({
onSuccess: async (data) => {
showMutationToast(data);
if (data.success && userData) {
await utils.jutsu.getUserJutsus.invalidate();
if (usedTransfers >= freeTransfers) {
await updateUser({
reputationPoints: userData.reputationPoints - JUTSU_TRANSFER_COST,
});
}
}
},
onSettled,
});

const isPending =
isToggling || isForgetting || isUpgrading || isUnequipping || isTransferring;
const isFetching = l1 || l2;

// Collapse UserItem and Item
Expand Down Expand Up @@ -325,6 +358,72 @@ export default function MyJutsu() {
)}

<div className="grow"></div>
{userjutsu.level <= JUTSU_TRANSFER_MAX_LEVEL && (
<Confirm
title="Transfer Level"
button={
<Button id="transfer" variant="secondary">
<ArrowRightLeft className="h-6 w-6 mr-2" />
Transfer Level
</Button>
}
proceed_label={
transferTarget ? "Confirm Transfer" : "Select Target"
}
onClose={() => {
setTransferTarget(undefined);
}}
onAccept={(e) => {
e.preventDefault();
if (transferTarget) {
transferLevel({
fromJutsuId: userjutsu.jutsuId,
toJutsuId: transferTarget.jutsuId,
});
}
}}
>
{transferTarget ? (
<>
<p>
Transfer level {userjutsu.level} from {userjutsu.name} to{" "}
{transferTarget.name}?
</p>
<p>
This will reset {userjutsu.name} to level 1 and set{" "}
{transferTarget.name} to level {userjutsu.level}.
</p>
<p>
Cost:{" "}
{usedTransfers >= freeTransfers
? `${JUTSU_TRANSFER_COST} reputation points`
: "Free"}
</p>
</>
) : (
<div className="flex flex-col gap-2">
<p>Select a jutsu to transfer the level to.</p>
<ActionSelector
items={allJutsu?.filter(
(jutsu) =>
jutsu.jutsuType === userjutsu.jutsuType &&
jutsu.jutsuRank === userjutsu.jutsuRank &&
jutsu.id !== userjutsu.id,
)}
counts={userJutsuCounts}
labelSingles={true}
showBgColor={false}
showLabels={true}
onClick={(id) => {
setTransferTarget(
allJutsu?.find((jutsu) => jutsu.id === id),
);
}}
/>
</div>
)}
</Confirm>
)}
<Confirm
title="Forget Jutsu"
button={
Expand Down
2 changes: 2 additions & 0 deletions app/src/layout/Confirm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ConfirmProps {
| React.MouseEvent<HTMLButtonElement, MouseEvent>
| React.KeyboardEvent<KeyboardEvent>,
) => void;
onClose?: () => void;
}

const Confirm: React.FC<ConfirmProps> = (props) => {
Expand All @@ -29,6 +30,7 @@ const Confirm: React.FC<ConfirmProps> = (props) => {
onAccept={props.onAccept}
className={props.className}
isValid={props.isValid}
onClose={props.onClose}
>
{props.children}
</Modal>
Expand Down
17 changes: 17 additions & 0 deletions app/src/layout/JutsuFiltering.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {
ElementName,
UserRank,
StatType,
JutsuType,
AttackMethod,
AttackTarget,
} from "@/drizzle/constants";
Expand Down Expand Up @@ -197,6 +198,7 @@ export const useFiltering = () => {
const [classification, setClassification] = useState<StatType | None>("None");
const [effect, setEffect] = useState<string[]>([]);
const [element, setElement] = useState<string[]>([]);
const [jutsuType, setJutsuType] = useState<JutsuType[]>([]);
const [method, setMethod] = useState<AttackMethod | None>("None");
const [name, setName] = useState<string>("");
const [rank, setRank] = useState<UserRank>("NONE");
Expand Down Expand Up @@ -233,6 +235,7 @@ export const useFiltering = () => {
effect,
element,
hidden,
jutsuType,
method,
name,
rank,
Expand Down Expand Up @@ -262,6 +265,7 @@ export const useFiltering = () => {
setEffect,
setElement,
setHidden,
setJutsuType,
setMethod,
setName,
setRank,
Expand Down Expand Up @@ -305,6 +309,7 @@ const JutsuFiltering: React.FC<JutsuFilteringProps> = (props) => {
effect,
element,
hidden,
jutsuType,
method,
name,
rank,
Expand Down Expand Up @@ -334,6 +339,7 @@ const JutsuFiltering: React.FC<JutsuFilteringProps> = (props) => {
setEffect,
setElement,
setHidden,
setJutsuType,
setMethod,
setName,
setRank,
Expand Down Expand Up @@ -646,6 +652,16 @@ const JutsuFiltering: React.FC<JutsuFilteringProps> = (props) => {
}))}
/>

{/* Jutsu Type */}
<div>
<Label>Jutsu Type</Label>
<MultiSelect
selected={jutsuType}
options={JutsuTypes.map((type) => ({ value: type, label: type }))}
onChange={(e) => setJutsuType(e as JutsuType[])}
/>
</div>

{/* Target */}
<FilterSelect
label="Target"
Expand Down Expand Up @@ -854,6 +870,7 @@ export const getFilter = (state: JutsuFilteringState) => {
disappear: state.removeAnim === "None" ? undefined : state.removeAnim,
effect: processArray(state.effect as EffectType[]),
element: processArray(state.element as ElementName[]),
jutsuType: processArray(state.jutsuType),
method: state.method === "None" ? undefined : state.method,
name: state.name || undefined,
rank: state.rank === "NONE" ? undefined : state.rank,
Expand Down
3 changes: 3 additions & 0 deletions app/src/layout/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ModalProps {
| React.MouseEvent<HTMLButtonElement, MouseEvent>
| React.KeyboardEvent<KeyboardEvent>,
) => void;
onClose?: () => void;
}

const Modal: React.FC<ModalProps> = (props) => {
Expand Down Expand Up @@ -62,6 +63,7 @@ const Modal: React.FC<ModalProps> = (props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (props.onClose) props.onClose();
props.setIsOpen(false);
}}
>
Expand Down Expand Up @@ -92,6 +94,7 @@ const Modal: React.FC<ModalProps> = (props) => {
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (props.onClose) props.onClose();
props.setIsOpen(false);
}}
className="z-30 rounded-lg border border-gray-500 bg-gray-700 text-sm font-medium text-gray-300 hover:bg-gray-600 hover:text-white"
Expand Down
23 changes: 23 additions & 0 deletions app/src/libs/jutsu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import { JutsuTypes } from "@/drizzle/constants";
import { UserRanks } from "@/drizzle/constants";
import { StatTypes } from "@/drizzle/constants";
import { showMutationToast, showFormErrorsToast } from "@/libs/toast";
import { JUTSU_TRANSFER_FREE_AMOUNT } from "@/drizzle/constants";
import { JUTSU_TRANSFER_FREE_NORMAL } from "@/drizzle/constants";
import { JUTSU_TRANSFER_FREE_SILVER } from "@/drizzle/constants";
import { JUTSU_TRANSFER_FREE_GOLD } from "@/drizzle/constants";
import type { FederalStatus } from "@/drizzle/constants";
import type { ZodAllTags } from "@/libs/combat/types";
import type { ZodJutsuType } from "@/libs/combat/types";
import type { FormEntry } from "@/layout/EditContent";
Expand Down Expand Up @@ -103,3 +108,21 @@ export const useJutsuEditForm = (data: Jutsu, refetch: () => void) => {

return { jutsu, effects, form, formData, loading, setEffects, handleJutsuSubmit };
};

/**
* Get the number of free jutsu level transfers based on the federal status
* @param federalStatus
* @returns
*/
export const getFreeTransfers = (federalStatus: FederalStatus) => {
switch (federalStatus) {
case "GOLD":
return JUTSU_TRANSFER_FREE_GOLD;
case "SILVER":
return JUTSU_TRANSFER_FREE_SILVER;
case "NORMAL":
return JUTSU_TRANSFER_FREE_NORMAL;
default:
return JUTSU_TRANSFER_FREE_AMOUNT;
}
};
Loading
Loading