Closed killface closed 6 months ago
Updated PlayerTradeComponent.tsx as proposed (compiles, but is untested since I haven't yet looked into how to load a save with a Caravansary):
import Tippy from "@tippyjs/react";
import classNames from "classnames";
import { useState } from "react";
import { NoPrice, NoStorage, type Resource } from "../../../shared/definitions/ResourceDefinitions";
import { getStorageFor } from "../../../shared/logic/BuildingLogic";
import { Config } from "../../../shared/logic/Config";
import { TRADE_CANCEL_REFUND_PERCENT } from "../../../shared/logic/Constants";
import { unlockedResources } from "../../../shared/logic/IntraTickCache";
import { getTradePercentage } from "../../../shared/logic/PlayerTradeLogic";
import {
CURRENCY_PERCENT_EPSILON,
formatPercent,
keysOf,
mathSign,
safeAdd,
} from "../../../shared/utilities/Helper";
import { L, t } from "../../../shared/utilities/i18n";
import { AccountLevelImages, AccountLevelNames } from "../logic/AccountLevel";
import { client, useTrades, useUser } from "../rpc/RPCClient";
import { getMyMapXy } from "../scenes/PathFinder";
import { PlayerMapScene } from "../scenes/PlayerMapScene";
import { getCountryName, getFlagUrl } from "../utilities/CountryCode";
import { Singleton } from "../utilities/Singleton";
import { playError, playKaching } from "../visuals/Sound";
import { AddTradeComponent } from "./AddTradeComponent";
import type { IBuildingComponentProps } from "./BuildingPage";
import { ConfirmModal } from "./ConfirmModal";
import { FillPlayerTradeModal } from "./FillPlayerTradeModal";
import { FixedLengthText } from "./FixedLengthText";
import { showModal, showToast } from "./GlobalModal";
import { FormatNumber } from "./HelperComponents";
import { PendingClaimComponent } from "./PendingClaimComponent";
import { TableView } from "./TableView";
import { WarningComponent } from "./WarningComponent";
const savedBuyFilters: Set<Resource> = new Set();
const savedSellFilters: Set<Resource> = new Set();
const playerTradesSortingState = { column: 0, asc: true };
export function PlayerTradeComponent({ gameState, xy }: IBuildingComponentProps): React.ReactNode {
const building = gameState.tiles.get(xy)?.building;
const [buyFilters, setWantsFilters] = useState(savedBuyFilters);
const [sellFilters, setOffersFilters] = useState(savedSellFilters);
const [showFilters, setShowFilters] = useState(false);
if (!building) {
return null;
}
const trades = useTrades();
const user = useUser();
const myXy = getMyMapXy();
if (!myXy) {
return (
<>
<WarningComponent icon="info">
<div>{t(L.PlayerTradeClaimTileFirstWarning)}</div>
<div
className="text-strong text-link row"
onClick={() => Singleton().sceneManager.loadScene(PlayerMapScene)}
>
{t(L.PlayerTradeClaimTileFirst)}
</div>
</WarningComponent>
<div className="sep10"></div>
</>
);
}
const resources = keysOf(unlockedResources(gameState)).filter((r) => !NoStorage[r] && !NoPrice[r]);
return (
<fieldset>
<legend>{t(L.PlayerTrade)}</legend>
<PendingClaimComponent gameState={gameState} xy={xy} />
<AddTradeComponent gameState={gameState} xy={xy} />
{showFilters ? (
<fieldset>
<legend className="text-strong">{t(L.PlayerTradeFilters)}</legend>
<div className="table-view" style={{ overflowY: "auto", maxHeight: "200px" }}>
<table>
<thead>
<td>{t(L.PlayerTradeResource)}</td>
<td>{t(L.PlayerTradeOffer)}</td>
<td>{t(L.PlayerTradeWant)}</td>
</thead>
<tbody>
{resources
.sort((a, b) => Config.Resource[a].name().localeCompare(Config.Resource[b].name()))
.map((res) => (
<tr key={res}>
<td>{Config.Resource[res].name()}</td>
<td
style={{ width: 0 }}
className="text-strong"
onClick={() => {
if (savedSellFilters.has(res)) {
savedSellFilters.delete(res);
} else {
savedSellFilters.add(res);
}
setOffersFilters(new Set(savedSellFilters));
}}
>
{savedSellFilters.has(res) ? (
<div className="m-icon small text-blue">check_box</div>
) : (
<div className="m-icon small text-desc">check_box_outline_blank</div>
)}
</td>
<td
style={{ width: 0 }}
className="text-strong"
onClick={() => {
if (savedBuyFilters.has(res)) {
savedBuyFilters.delete(res);
} else {
savedBuyFilters.add(res);
}
setWantsFilters(new Set(savedBuyFilters));
}}
>
{savedBuyFilters.has(res) ? (
<div className="m-icon small text-blue">check_box</div>
) : (
<div className="m-icon small text-desc">check_box_outline_blank</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="sep10"></div>
<div className="row">
<button
className="f1 text-center text-strong"
onClick={() => {
setShowFilters(false);
}}
>
{t(L.PlayerTradeFiltersApply)}
</button>
<div style={{ width: 10 }} />
<button
className="f1 text-center"
onClick={() => {
savedSellFilters.clear();
savedBuyFilters.clear();
setOffersFilters(new Set(savedSellFilters));
setWantsFilters(new Set(savedBuyFilters));
setShowFilters(false);
}}
>
{t(L.PlayerTradeFiltersClear)}
</button>
</div>
</fieldset>
) : (
<button
className="row w100 jcc mb10"
onClick={() => {
setShowFilters(true);
}}
>
<div className="m-icon small">filter_list</div>
<div className="text-strong f1">
{t(L.PlayerTradeFilters)} ({sellFilters.size + buyFilters.size})
</div>
</button>
)}
<TableView
header={[
{ name: t(L.PlayerTradeWant), sortable: true },
{ name: t(L.PlayerTradeOffer), sortable: true },
{ name: "", sortable: true },
{ name: t(L.PlayerTradeFrom), sortable: true },
{ name: "", sortable: false },
]}
sortingState={playerTradesSortingState}
data={trades.filter(
(trade) =>
(sellFilters.size === 0 && buyFilters.size === 0) ||
buyFilters.has(trade.buyResource) ||
sellFilters.has(trade.sellResource),
)}
compareFunc={(a, b, col) => {
switch (col) {
case 0:
return Config.Resource[a.buyResource]
.name()
.localeCompare(Config.Resource[b.buyResource].name());
case 1:
return Config.Resource[a.sellResource]
.name()
.localeCompare(Config.Resource[b.sellResource].name());
case 2:
return getTradePercentage(a) - getTradePercentage(b);
case 3:
return a.from.localeCompare(b.from);
default:
return 0;
}
}}
renderRow={(trade) => {
const disableFill = user === null || trade.fromId === user.userId;
const percentage = getTradePercentage(trade);
return (
<tr key={trade.id} className={classNames({ "text-strong": trade.fromId === user?.userId })}>
<td>
<div className={classNames({ "text-strong": building.resources[trade.buyResource] })}>
{Config.Resource[trade.buyResource].name()}
</div>
<div className="text-small text-strong text-desc">
<FormatNumber value={trade.buyAmount} />
</div>
</td>
<td>
<div>{Config.Resource[trade.sellResource].name()}</div>
<div className="text-small text-strong text-desc">
<FormatNumber value={trade.sellAmount} />
</div>
</td>
<td
className={classNames({
"text-small text-right": true,
"text-red": percentage <= -CURRENCY_PERCENT_EPSILON,
"text-green": percentage >= CURRENCY_PERCENT_EPSILON,
"text-desc": Math.abs(percentage) < CURRENCY_PERCENT_EPSILON,
})}
>
<Tippy content={t(L.MarketValueDesc, { value: formatPercent(percentage, 0) })}>
<div>
{mathSign(percentage, CURRENCY_PERCENT_EPSILON)}
{formatPercent(Math.abs(percentage), 0)}
</div>
</Tippy>
</td>
<td>
<div className="row">
<img
src={getFlagUrl(trade.fromFlag)}
className="player-flag game-cursor"
title={getCountryName(trade.fromFlag)}
/>
{trade.fromLevel > 0 ? (
<img
src={AccountLevelImages[trade.fromLevel]}
className="player-flag"
title={AccountLevelNames[trade.fromLevel]()}
/>
) : null}
</div>
<div className="text-small">
<FixedLengthText text={trade.from} length={10} />
</div>
</td>
<td>
{trade.fromId === user?.userId ? (
<div
className="m-icon small text-link"
onClick={() => {
showModal(
<ConfirmModal
title={t(L.PlayerTradeCancelTrade)}
onConfirm={async () => {
try {
const { total, used } = getStorageFor(xy, gameState);
if (
used + trade.sellAmount * TRADE_CANCEL_REFUND_PERCENT >
total
) {
throw new Error(t(L.PlayerTradeCancelTradeNotEnoughStorage));
}
const cancelledTrade = await client.cancelTrade(trade.id);
safeAdd(
building.resources,
cancelledTrade.sellResource,
cancelledTrade.sellAmount * TRADE_CANCEL_REFUND_PERCENT,
);
playKaching();
} catch (error) {
showToast(String(error));
playError();
}
}}
>
{t(L.PlayerTradeCancelDesc, {
percent: formatPercent(TRADE_CANCEL_REFUND_PERCENT),
})}
</ConfirmModal>,
);
}}
>
delete
</div>
) : (
<div
className={classNames({
"text-link": !disableFill,
"text-strong": true,
"text-desc": disableFill,
})}
onClick={() => {
if (!disableFill) {
showModal(<FillPlayerTradeModal tradeId={trade.id} xy={xy} />);
}
}}
>
{t(L.PlayerTradeFill)}
</div>
)}
</td>
</tr>
);
}}
/>
</fieldset>
);
}
The more I've played since submitting this issue, the less I want it - it's helpful enough seeing the trades that are "competing" with my own that I don't think cluttering up this UI is worthwhile. I'm closing this issue due to lack of other interest and my own gradual change of heart.
Currently in the Caravansary, when you filter by a resource you see trades that are both offering AND wanting that resource. Sometimes this is useful, but most of the time it's just a bunch of noise - players wanting the same resource as you.
I'd like to update the PlayerTradeComponent so that instead of a single checkbox per resource, there are two - one for "offer" and one for "want". I could split
resourceFilters
into something likebuyFilters
andsellFilters
(or better names) and go from there. I'm a web developer by day (React these days), so I'd be happy to make the changes myself and create a PR if this is worthwhile to you.