diff --git a/README.md b/README.md index f9b530449..38d6e3d50 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,13 @@ flowchart TD C --> |Buys / Sells drugs on local markets|D[Select next location to travel to] D --> |Player travels without incident|END[Turn ends] D --> F[Player is Mugged] - F --> F1[Fight] + F --> F1[Pay] F --> F2[Run] F2 --> F12[Win] --> END F2 --> L[Lose] L --> |Player loses their stash|END D --> G[Chased by Cops] - G --> F1[Fight] + G --> F1[Pay] G --> F2[Run] F1 --> F12 F1 --> L diff --git a/scripts/default_auth.sh b/scripts/default_auth.sh index c5aeb3d81..328ffb57a 100755 --- a/scripts/default_auth.sh +++ b/scripts/default_auth.sh @@ -2,9 +2,8 @@ set -euo pipefail pushd $(dirname "$0")/.. -#export RPC_URL="http://localhost:5050"; -export RPC_URL="https://api.cartridge.gg/x/rollyourown/katana"; - +export RPC_URL="http://localhost:5050"; +#export RPC_URL="https://api.cartridge.gg/x/rollyourown/katana"; export WORLD_ADDRESS="0x3c3dfeb374720dfd73554dc2b9e0583cb9668efb3055d07d1533afa5d219fd5"; # enable system -> component authorizations diff --git a/src/components/location.cairo b/src/components/location.cairo index dd6937ead..4364a5c41 100644 --- a/src/components/location.cairo +++ b/src/components/location.cairo @@ -19,7 +19,8 @@ impl LocationImpl of LocationTrait { locations.span() } - fn random(seed: felt252) -> felt252 { + fn random() -> felt252 { + let seed = starknet::get_tx_info().unbox().transaction_hash; let locations = LocationImpl::all(); let seed: u256 = seed.into(); let len: u128 = locations.len().into(); diff --git a/src/components/market.cairo b/src/components/market.cairo index 239fa50dd..23411d046 100644 --- a/src/components/market.cairo +++ b/src/components/market.cairo @@ -45,45 +45,45 @@ impl MarketImpl of MarketTrait { #[inline(always)] fn get_pricing_info(drug_id: felt252) -> PricingInfos { - if drug_id == 'Acid' { - PricingInfos { - min_price: 500 * SCALING_FACTOR, - max_price: 1000 * SCALING_FACTOR, - min_qty: 400, - max_qty: 900, - } - } else if drug_id == 'Weed' { - PricingInfos { - min_price: 250 * SCALING_FACTOR, - max_price: 500 * SCALING_FACTOR, - min_qty: 500, - max_qty: 1000, - } - } else if drug_id == 'Ludes' { + if drug_id == 'Ludes' { PricingInfos { min_price: 10 * SCALING_FACTOR, - max_price: 50 * SCALING_FACTOR, + max_price: 60 * SCALING_FACTOR, min_qty: 800, max_qty: 2000, } } else if drug_id == 'Speed' { PricingInfos { min_price: 50 * SCALING_FACTOR, - max_price: 250 * SCALING_FACTOR, + max_price: 300 * SCALING_FACTOR, min_qty: 600, max_qty: 1500, } + } else if drug_id == 'Weed' { + PricingInfos { + min_price: 200 * SCALING_FACTOR, + max_price: 700 * SCALING_FACTOR, + min_qty: 500, + max_qty: 1000, + } + } else if drug_id == 'Acid' { + PricingInfos { + min_price: 500 * SCALING_FACTOR, + max_price: 1800 * SCALING_FACTOR, + min_qty: 400, + max_qty: 900, + } } else if drug_id == 'Heroin' { PricingInfos { - min_price: 1000 * SCALING_FACTOR, - max_price: 2000 * SCALING_FACTOR, + min_price: 1200 * SCALING_FACTOR, + max_price: 4000 * SCALING_FACTOR, min_qty: 300, max_qty: 700, } } else if drug_id == 'Cocaine' { PricingInfos { - min_price: 2000 * SCALING_FACTOR, - max_price: 6000 * SCALING_FACTOR, + min_price: 3000 * SCALING_FACTOR, + max_price: 8000 * SCALING_FACTOR, min_qty: 250, max_qty: 600, } diff --git a/src/components/player.cairo b/src/components/player.cairo index df06edc4a..a3bebd38a 100644 --- a/src/components/player.cairo +++ b/src/components/player.cairo @@ -11,6 +11,7 @@ struct Player { location_id: felt252, cash: u128, health: u8, + run_attempts: u8, drug_count: usize, bag_limit: usize, turns_remaining: usize, diff --git a/src/components/risks.cairo b/src/components/risks.cairo index 8f2a872df..671bf38f9 100644 --- a/src/components/risks.cairo +++ b/src/components/risks.cairo @@ -2,7 +2,7 @@ use traits::{Into, TryInto}; use option::OptionTrait; use debug::PrintTrait; -use rollyourown::constants::SCALING_FACTOR; +use rollyourown::constants::{SCALING_FACTOR, COPS_DRUG_THRESHOLD, ENCOUNTER_BIAS_GANGS}; use rollyourown::PlayerStatus; #[derive(Component, Copy, Drop, Serde)] @@ -12,31 +12,39 @@ struct Risks { #[key] location_id: felt252, travel: u8, - run: u8, + capture: u8, } #[generate_trait] impl RisksImpl of RisksTrait { #[inline(always)] - fn travel(ref self: Risks, seed: felt252) -> PlayerStatus { + fn travel(ref self: Risks, seed: felt252, cash: u128, drug_count: usize) -> PlayerStatus { if occurs(seed, self.travel) { let seed = pedersen::pedersen(seed, seed); let entropy: u256 = seed.into(); let result: u128 = entropy.low % 100; // more bias towards gang encounter - return match result <= 40 { - bool::False => PlayerStatus::BeingMugged(()), - bool::True => PlayerStatus::BeingArrested(()), + return match result <= ENCOUNTER_BIAS_GANGS { + bool::False => { + if drug_count < COPS_DRUG_THRESHOLD { + return PlayerStatus::BeingMugged; + } + + PlayerStatus::BeingArrested + }, + bool::True => { + PlayerStatus::BeingMugged + } }; } - return PlayerStatus::Normal(()); + return PlayerStatus::Normal; } #[inline(always)] fn run(ref self: Risks, seed: felt252) -> bool { - occurs(seed, self.run) + occurs(seed, self.capture) } } @@ -55,8 +63,8 @@ fn occurs(seed: felt252, likelihood: u8) -> bool { #[available_gas(1000000)] fn test_never_occurs() { let seed = pedersen::pedersen(1, 1); - let mut risks = Risks { game_id: 0, location_id: 0, travel: 0, run: 0 }; - let player_status = risks.travel(seed); + let mut risks = Risks { game_id: 0, location_id: 0, travel: 0, capture: 0 }; + let player_status = risks.travel(seed, 0, 0); assert(player_status == PlayerStatus::Normal(()), 'event occured'); } @@ -65,8 +73,8 @@ fn test_never_occurs() { #[available_gas(1000000)] fn test_always_occurs() { let seed = pedersen::pedersen(1, 1); - let mut risks = Risks { game_id: 0, location_id: 0, travel: 100, run: 0 }; - let player_status = risks.travel(seed); + let mut risks = Risks { game_id: 0, location_id: 0, travel: 100, capture: 0 }; + let player_status = risks.travel(seed, 1, COPS_DRUG_THRESHOLD); assert(player_status != PlayerStatus::Normal(()), 'event did not occur'); } diff --git a/src/constants.cairo b/src/constants.cairo index 832c3aa98..3b64e52bb 100644 --- a/src/constants.cairo +++ b/src/constants.cairo @@ -1,17 +1,20 @@ const SCALING_FACTOR: u128 = 10_000; -const TRAVEL_RISK: u8 = 30; // 30% chance of mugged -const RUN_CHANCE: u8 = 50; // 50% chance of successfully getting away +const TRAVEL_RISK: u8 = 60; // 60% chance of travel encounter +const CAPTURE_RISK: u8 = 60; // 60% chance of capture +const ENCOUNTER_BIAS_GANGS: u128 = 75; +const COPS_DRUG_THRESHOLD: usize = 5; // cops encounter threshold -const BASE_PAYMENT: u128 = 400_0000; // base payment is $400 +const HEALTH_IMPACT: u8 = 10; +const GANGS_PAYMENT: usize = 20; // starting stats -const STARTING_CASH: u128 = 4000_0000; // $4000 +const STARTING_CASH: u128 = 2000_0000; // $2000 const STARTING_BAG_LIMIT: usize = 100; // inventory size const STARTING_HEALTH: u8 = 100; -// market eventsks +// market events const PRICE_VAR: u8 = 3; // 3% chance const MIN_PRICE_VAR: u8 = 20; // 20% const MAX_PRICE_VAR: u8 = 50; // 50% diff --git a/src/systems/create.cairo b/src/systems/create.cairo index c1053f64b..c04e9f535 100644 --- a/src/systems/create.cairo +++ b/src/systems/create.cairo @@ -19,7 +19,8 @@ mod create_game { use rollyourown::components::location::{Location, LocationTrait}; use rollyourown::components::market::{MarketTrait}; use rollyourown::constants::{ - SCALING_FACTOR, TRAVEL_RISK, RUN_CHANCE, STARTING_CASH, STARTING_HEALTH, STARTING_BAG_LIMIT + SCALING_FACTOR, TRAVEL_RISK, CAPTURE_RISK, STARTING_CASH, STARTING_HEALTH, + STARTING_BAG_LIMIT }; use rollyourown::utils::random; use debug::PrintTrait; @@ -51,19 +52,19 @@ mod create_game { ctx: Context, start_time: u64, max_players: usize, max_turns: usize ) -> (u32, ContractAddress) { let game_id = ctx.world.uuid(); - let seed = starknet::get_tx_info().unbox().transaction_hash; - let location_id = LocationTrait::random(seed); + let location_id = LocationTrait::random(); let player = Player { game_id, player_id: ctx.origin, + status: PlayerStatus::Normal(()), location_id, cash: STARTING_CASH, health: STARTING_HEALTH, + run_attempts: 0, drug_count: 0, bag_limit: STARTING_BAG_LIMIT, turns_remaining: max_turns, - status: PlayerStatus::Normal(()), }; let game = Game { @@ -86,7 +87,10 @@ mod create_game { set!( ctx.world, (Risks { - game_id, location_id: *location_id, travel: TRAVEL_RISK, run: RUN_CHANCE + game_id, + location_id: *location_id, + travel: TRAVEL_RISK, + capture: CAPTURE_RISK }) ); diff --git a/src/systems/decide.cairo b/src/systems/decide.cairo index e08b0415f..4365c5b1d 100644 --- a/src/systems/decide.cairo +++ b/src/systems/decide.cairo @@ -8,7 +8,7 @@ mod decide { use dojo::world::Context; use rollyourown::PlayerStatus; - use rollyourown::constants::BASE_PAYMENT; + use rollyourown::constants::{GANGS_PAYMENT, HEALTH_IMPACT, COPS_DRUG_THRESHOLD}; use rollyourown::components::game::{Game, GameTrait}; use rollyourown::components::risks::{Risks, RisksTrait}; use rollyourown::components::player::{Player, PlayerTrait}; @@ -18,14 +18,12 @@ mod decide { enum Action { Run: (), Pay: (), - Fight: (), } #[derive(Copy, Drop, Serde, PartialEq)] enum Outcome { Died: (), Paid: (), - Fought: (), Escaped: (), Captured: (), Unsupported: (), @@ -36,15 +34,13 @@ mod decide { enum Event { Decision: Decision, Consequence: Consequence, - CashLoss: CashLoss, - DrugLoss: DrugLoss, } #[derive(Drop, starknet::Event)] struct Decision { game_id: u32, player_id: ContractAddress, - action: Action, + action: Action } #[derive(Drop, starknet::Event)] @@ -52,21 +48,9 @@ mod decide { game_id: u32, player_id: ContractAddress, outcome: Outcome, - } - - #[derive(Drop, starknet::Event)] - struct CashLoss { - game_id: u32, - player_id: ContractAddress, - amount: u128 - } - - #[derive(Drop, starknet::Event)] - struct DrugLoss { - game_id: u32, - player_id: ContractAddress, - drug_id: felt252, - quantity: usize + health_loss: u8, + drug_loss: usize, + cash_loss: u128 } fn execute(ctx: Context, game_id: u32, action: Action, next_location_id: felt252) { @@ -74,105 +58,77 @@ mod decide { let mut player = get!(ctx.world, (game_id, player_id).into(), Player); assert(player.status != PlayerStatus::Normal, 'player response not needed'); - let (mut outcome, cash_loss, drug_count_loss, health_loss) = match player.status { - PlayerStatus::Normal => (Outcome::Unsupported, 0, 0, 0), - PlayerStatus::BeingMugged => match action { - Action::Run => run( - ctx, game_id, player_id, player.status, player.cash, player.location_id - ), - Action::Pay => (Outcome::Unsupported, 0, 0, 0), // can't pay muggers - Action::Fight => fight(ctx, game_id, player_id), + let (mut outcome, cash_loss, drug_loss, health_loss) = match action { + Action::Run => { + let mut risks = get!(ctx.world, (game_id, player.location_id).into(), Risks); + let seed = starknet::get_tx_info().unbox().transaction_hash; + match risks.run(seed) { + bool::False => (Outcome::Escaped, 0, 0, 0), + bool::True => ( + Outcome::Captured, 0, 0, HEALTH_IMPACT * (1 + player.run_attempts) + ) + } }, - PlayerStatus::BeingArrested => match action { - Action::Run => run( - ctx, game_id, player_id, player.status, player.cash, player.location_id - ), - Action::Pay => pay(ctx, game_id, player_id, player.cash), - Action::Fight => (Outcome::Unsupported, 0, 0, 0), // can't fight officers + Action::Pay => { + match player.status { + PlayerStatus::Normal => (Outcome::Unsupported, 0, 0, 0), + PlayerStatus::BeingMugged => { + let drug_loss = take_drugs(ctx, game_id, player_id, GANGS_PAYMENT); + let cash_loss = (player.cash * GANGS_PAYMENT.into()) / 100; + (Outcome::Paid, cash_loss, drug_loss, 0) + }, + PlayerStatus::BeingArrested => { + let cash_loss = cops_payment(player.drug_count); + assert(cash_loss <= player.cash, 'not enough cash to pay cops'); + (Outcome::Paid, cash_loss, 0, 0) + } + } }, }; - // you can only bribe cops and fight muggers, not the other way around - assert(outcome != Outcome::Unsupported, 'unsupported action'); + if outcome == Outcome::Captured { + player.run_attempts += 1; + } else { + player.status = PlayerStatus::Normal; + player.turns_remaining -= 1; + player.location_id = next_location_id; + player.run_attempts = 0; + } - // update player data - player.status = PlayerStatus::Normal; player.cash -= cash_loss; - player.drug_count -= drug_count_loss; - + player.drug_count -= drug_loss; if health_loss >= player.health { player.health = 0; player.turns_remaining = 0; outcome = Outcome::Died; } else { - player.health -= health_loss; - player.turns_remaining -= 1; - player.location_id = next_location_id; + player.health -= health_loss } set!(ctx.world, (player)); - emit!(ctx.world, Consequence { game_id, player_id, outcome }); - } - // Player will fight muggers, but it kinda hurts, taking 10hp of your health. You - // might also die if not enough health - fn fight(ctx: Context, game_id: u32, player_id: ContractAddress) -> (Outcome, u128, u32, u8) { - (Outcome::Fought, 0, 0, 10) + emit!(ctx.world, Decision { game_id, player_id, action }); + emit!( + ctx.world, + Consequence { game_id, player_id, outcome, health_loss, drug_loss, cash_loss } + ); } - // Player will hand over either 20% of their cash or $400, which ever is more - fn pay( - ctx: Context, game_id: u32, player_id: ContractAddress, player_cash: u128 - ) -> (Outcome, u128, u32, u8) { - assert(player_cash >= BASE_PAYMENT, 'not enough cash kid'); - let cash_loss = cmp::max(player_cash / 5, BASE_PAYMENT); - - emit!(ctx.world, Decision { game_id, player_id, action: Action::Pay }); - emit!(ctx.world, CashLoss { game_id, player_id, amount: cash_loss }); - (Outcome::Paid, cash_loss, 0, 0) - } - - // Player will try to run and can escape without consequence. However, if you - // are caught be ready to face the consequences: - // - caught escaping an officer - lose half your drugs - // - caught escaping muggers - lose half your cash and hp - fn run( - ctx: Context, - game_id: u32, - player_id: ContractAddress, - player_status: PlayerStatus, - player_cash: u128, - location_id: felt252 - ) -> (Outcome, u128, u32, u8) { - let mut risks = get!(ctx.world, (game_id, location_id).into(), Risks); - let seed = starknet::get_tx_info().unbox().transaction_hash; - let got_away = risks.run(seed); - - emit!(ctx.world, Decision { game_id, player_id, action: Action::Run }); - match got_away { - bool::False => match player_status { - PlayerStatus::Normal => { - (Outcome::Unsupported, 0, 0, 0) - }, - PlayerStatus::BeingMugged => { - let cash_loss = player_cash / 2; - - emit!(ctx.world, CashLoss { game_id, player_id, amount: cash_loss }); - (Outcome::Captured, cash_loss, 0, 20) - }, - PlayerStatus::BeingArrested => { - let drug_count_loss = halve_drugs(ctx, game_id, player_id); - (Outcome::Captured, 0, drug_count_loss, 0) - } - }, - bool::True => { - (Outcome::Escaped, 0, 0, 0) - } + fn cops_payment(drug_count: u32) -> u128 { + if drug_count < COPS_DRUG_THRESHOLD + 20 { + 1000_0000 // $1000 + } else if drug_count < COPS_DRUG_THRESHOLD + 50 { + 5000_0000 // $5000 + } else if drug_count < COPS_DRUG_THRESHOLD + 80 { + 10000_0000 // $10000 + } else { + 20000_0000 // $20000 } } - // ngmi - fn halve_drugs(ctx: Context, game_id: u32, player_id: ContractAddress) -> u32 { + fn take_drugs( + ctx: Context, game_id: u32, player_id: ContractAddress, percentage: usize + ) -> usize { let mut drugs = DrugTrait::all(); let mut total_drug_loss = 0; loop { @@ -180,15 +136,15 @@ mod decide { Option::Some(drug_id) => { let mut drug = get!(ctx.world, (game_id, player_id, *drug_id), Drug); if (drug.quantity != 0) { - drug.quantity /= 2; - total_drug_loss += drug.quantity; - - emit!( - ctx.world, - DrugLoss { - game_id, player_id, drug_id: *drug_id, quantity: drug.quantity - } - ); + let mut drug_loss = (drug.quantity * percentage) / 100; + drug_loss = if drug_loss == 0 { + 1 + } else { + drug_loss + }; + drug.quantity -= drug_loss; + total_drug_loss += drug_loss; + set!(ctx.world, (drug)); } }, diff --git a/src/systems/join.cairo b/src/systems/join.cairo index 28dd135b9..e13d5ffe4 100644 --- a/src/systems/join.cairo +++ b/src/systems/join.cairo @@ -39,19 +39,19 @@ mod join_game { game.num_players += 1; - let seed = starknet::get_tx_info().unbox().transaction_hash; - let location_id = LocationTrait::random(seed); + let location_id = LocationTrait::random(); let player = Player { game_id, player_id, + status: PlayerStatus::Normal(()), location_id, cash: STARTING_CASH, health: STARTING_HEALTH, + run_attempts: 0, drug_count: 0, bag_limit: STARTING_BAG_LIMIT, turns_remaining: game.max_turns, - status: PlayerStatus::Normal(()), }; set!(ctx.world, (game, player)); diff --git a/src/systems/travel.cairo b/src/systems/travel.cairo index 4a4ac19e3..aa1d432a1 100644 --- a/src/systems/travel.cairo +++ b/src/systems/travel.cairo @@ -4,6 +4,7 @@ mod travel { use box::BoxTrait; use array::ArrayTrait; use starknet::ContractAddress; + use debug::PrintTrait; use dojo::world::{Context}; @@ -52,9 +53,8 @@ mod travel { let mut risks = get!(ctx.world, (game_id, next_location_id).into(), Risks); let seed = starknet::get_tx_info().unbox().transaction_hash; - - player.status = risks.travel(seed); - if player.status != PlayerStatus::Normal(()) { + player.status = risks.travel(seed, player.cash, player.drug_count); + if player.status != PlayerStatus::Normal { set!(ctx.world, (player)); emit!(ctx.world, AdverseEvent { game_id, player_id, player_status: player.status }); diff --git a/src/tests/travel.cairo b/src/tests/travel.cairo index 5bf1e915a..b7ce52879 100644 --- a/src/tests/travel.cairo +++ b/src/tests/travel.cairo @@ -14,6 +14,7 @@ use dojo::test_utils::spawn_test_world; use rollyourown::PlayerStatus; use rollyourown::components::player::Player; use rollyourown::tests::create::{spawn_game, spawn_player}; +use rollyourown::constants::{TRAVEL_RISK}; #[test] #[available_gas(110000000)] @@ -26,19 +27,21 @@ fn test_travel_and_decision() { travel_calldata.append(game_id.into()); travel_calldata.append(brooklyn_id); + starknet::testing::set_transaction_hash(TRAVEL_RISK.into()); world.execute('travel', travel_calldata); let player = get!(world, (game_id, player_id).into(), (Player)); - assert(player.status == PlayerStatus::BeingMugged(()), 'incorrect status'); + assert(player.status != PlayerStatus::Normal, 'incorrect status'); assert(player.location_id != brooklyn_id, 'should not have traveled'); + let queens_id = 'Queens'; let mut decision_calldata = array::ArrayTrait::::new(); decision_calldata.append(game_id.into()); decision_calldata.append(0.into()); // 0 = pay - decision_calldata.append(brooklyn_id); + decision_calldata.append(queens_id); world.execute('decide', decision_calldata); let player = get!(world, (game_id, player_id).into(), (Player)); - assert(player.location_id == brooklyn_id, 'should have traveled'); + assert(player.location_id == queens_id, 'should have traveled'); } diff --git a/web/src/components/Inventory.tsx b/web/src/components/Inventory.tsx index c34505b60..95016e8bb 100644 --- a/web/src/components/Inventory.tsx +++ b/web/src/components/Inventory.tsx @@ -39,29 +39,28 @@ export const Inventory = ({ ...props }: StyleProps) => { }, }} > - + {playerEntity?.drugCount === 0 ? ( Your bag is empty ) : ( - playerEntity?.drugs.map((drug, index) => { + playerEntity?.drugs.map((drug) => { return ( drug.quantity > 0 && ( - - - {getDrugById(drug.id).icon({ boxSize: "26" })} - {drug.quantity} - - {index < playerEntity.drugs.length - 1 && ( - - + <> + + + {getDrugById(drug.id)?.icon({ boxSize: "26" })} + {drug.quantity} - )} - + + + ) ); }) diff --git a/web/src/components/Layout.tsx b/web/src/components/Layout.tsx index 4d9be1155..352e78661 100644 --- a/web/src/components/Layout.tsx +++ b/web/src/components/Layout.tsx @@ -19,7 +19,6 @@ export interface LayoutProps { leftPanelProps?: LeftPanelProps; showBack?: boolean; actions?: ReactNode; - showMap?: boolean; children: ReactNode; isSinglePanel?: boolean; } @@ -35,7 +34,6 @@ const Layout = ({ CustomLeftPanel, leftPanelProps, showBack, - showMap, children, isSinglePanel = false, }: LayoutProps) => { @@ -57,7 +55,9 @@ const Layout = ({ ) : ( ))} - {children} + + {children} + diff --git a/web/src/components/Tutorial.tsx b/web/src/components/Tutorial.tsx index 35291fe14..caf400d9d 100644 --- a/web/src/components/Tutorial.tsx +++ b/web/src/components/Tutorial.tsx @@ -44,7 +44,6 @@ const steps = [ }, ]; - const TutorialStep = ({ step, }: { @@ -95,7 +94,7 @@ const Tutorial = ({ return ( - + {steps.map((step) => { @@ -105,7 +104,7 @@ const Tutorial = ({ - + {steps.map((step) => { return ( @@ -137,4 +136,4 @@ const Tutorial = ({ ); }; -export default Tutorial; \ No newline at end of file +export default Tutorial; diff --git a/web/src/components/map/Callout.tsx b/web/src/components/map/Callout.tsx index 2bac0d044..ccd9bfea0 100644 --- a/web/src/components/map/Callout.tsx +++ b/web/src/components/map/Callout.tsx @@ -19,11 +19,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M121 110.997H123.01V111.997H124.01V114.007H123.01V115.007H121V114.007H120V111.997H121V110.997ZM123 113.997H121.01V112.007H123V113.997Z" - fill="#11ED83" + fill="#FBCB4A" /> @@ -36,11 +36,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M118 176.995H120.01V177.995H121.01V180.005H120.01V181.005H118V180.005H117V177.995H118V176.995ZM120 179.995H118.01V178.005H120V179.995Z" - fill="#11ED83" + fill="#FBCB4A" /> @@ -53,11 +53,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M192.009 198H194.019V199H195.019V201.01H194.019V202.01H192.009V201.01H191.009V199H192.009V198ZM194.009 201H192.019V199.01H194.009V201Z" - fill="#11ED83" + fill="#FBCB4A" /> @@ -73,11 +73,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M189.004 129.994H191.014V130.994H192.014V133.004H191.014V134.004H189.004V133.004H188.004V130.994H189.004V129.994ZM191.004 132.994H189.014V131.004H191.004V132.994Z" - fill="#11ED83" + fill="#FBCB4A" /> @@ -91,11 +91,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M137 38.9948H139.01V39.9948H140.01V42.0048H139.01V43.0048H137V42.0048H136V39.9948H137V38.9948ZM139 41.9948H137.01V40.0048H139V41.9948Z" - fill="#11ED83" + fill="#FBCB4A" /> @@ -108,11 +108,11 @@ export const Callout = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M71.0061 91.99H73.0161V92.99H74.0161V95H73.0161V96H71.0061V95H70.0061V92.99H71.0061V91.99ZM73.0061 94.99H71.0161V93H73.0061V94.99Z" - fill="#11ED83" + fill="#FBCB4A" /> diff --git a/web/src/components/map/Map.tsx b/web/src/components/map/Map.tsx index 0ec6cdc42..b5b6d26dc 100644 --- a/web/src/components/map/Map.tsx +++ b/web/src/components/map/Map.tsx @@ -20,21 +20,21 @@ const coordinate: CoordinateType = { }; export const Map = ({ - highlight, + target, + current, onSelect, }: { - highlight?: Location; + target?: Location; + current?: Location; onSelect: (selected: Location) => void; }) => { const [scope, animate] = useAnimate(); const isMobile = useBreakpointValue([true, false]); useEffect(() => { - console.log({ highlight }); - if (highlight !== undefined) { - console.log("got here"); + if (target !== undefined) { const animation = isMobile - ? { scale: 1.75, ...coordinate[highlight] } + ? { scale: 1.75, ...coordinate[target] } : { scale: 1, x: 0, y: 0 }; animate( scope.current, @@ -45,7 +45,7 @@ export const Map = ({ }, ); } - }, [highlight, isMobile, animate, scope]); + }, [target, isMobile, animate, scope]); return ( - - + ); diff --git a/web/src/components/map/Outline.tsx b/web/src/components/map/Outline.tsx index 947ae3082..b95a77a3c 100644 --- a/web/src/components/map/Outline.tsx +++ b/web/src/components/map/Outline.tsx @@ -8,11 +8,19 @@ const ChakraBox = chakra(motion.div, { isValidMotionProp(prop) || shouldForwardProp(prop), }); -export const Outline = ({ location }: { location?: Location }) => { - if (location === undefined) { +export const Outline = ({ + target, + current, +}: { + target?: Location; + current?: Location; +}) => { + if (target === undefined || current === undefined) { return <>; } + const selfSelected = target === current; + const transition = { repeat: Infinity, duration: 1, @@ -21,13 +29,18 @@ export const Outline = ({ location }: { location?: Location }) => { return ( <> - - + {!selfSelected && ( + + + + )} + + { // @ts-ignore transition={transition} > - + ); }; -const SvgHighlight = ({ location }: { location: Location }) => { +const SvgHighlight = ({ + location, + fill, +}: { + location: Location; + fill: string; +}) => { return ( { @@ -94,7 +113,7 @@ const SvgHighlight = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M190 177.998V178.998H189V184.998H185V185.998H184V186.998H183.01V185.998H182.01V184.998H181.01V183.998H176V184.998H175V185.998H174V197.998H173.01V196.998H164V198.998H163V202.998H162V208.008H163V210.008H164V211.008H167V212.008H168V213.008H176V218.008H177V219.008H180.01V218.008H181.01V217.008H183V218.008H184V219.008H185V220.008H186V221.008H187V222.008H190.01V221.008H193V225.008H194V226.008H198V228.008H199V229.008H202.01V228.008H203.01V223.008H204V224.008H205V225.008H211V226.008H215V227.008H218.01V226.008H223.01V225.008H224.01V224.008H225.01V220.998H224.01V219.998H223.01V216.008H224.01V211.998H223.01V208.998H222.01V207.998H220.01V206.998H218.01V204.998H217.01V203.998H216.01V199.998H215.01V198.998H207.01V197.998H206.01V195.998H205.01V194.998H204.01V193.998H203.01V192.998H199.01V178.998H198.01V177.998H190ZM202 227.998V221.998H204.01V222.998H205.01V223.998H211.01V224.998H215.01V225.998H218V224.998H223V223.998H224V221.008H222V215.998H223V212.008H222V209.008H220V208.008H217V205.008H215V200.008H209.01V201.008H208V200.008H207V199.008H206V198.008H205V196.008H203V194.008H198V179.008H190.01V186.008H185.01V188.008H182V186.008H181V185.008H176.01V186.008H175.01V198.008H174.01V199.008H173V198.008H165.01V199.008H164.01V203.008H163.01V207.998H164.01V209.998H167V208.998H168.01V211.998H176V210.998H177.01V212.998H178.01V214.008H177.01V217.998H180V216.998H181V215.998H183.01V216.998H184.01V217.998H185.01V218.998H187.01V220.998H190V219.998H194.01V224.998H199.01V227.998H202Z" - fill="#11ED83" + fill={fill} /> @@ -102,31 +121,31 @@ const SvgHighlight = ({ location }: { location: Location }) => { fillRule="evenodd" clipRule="evenodd" d="M67 25H70.01V26H71.01V28H72.01V29H74.01V30H79.01V31H80V30H81V29H84.01V30H86.01V31H88V28H89V27H92.01V28H93.01V29H94.01V32H96.01V35H98V34H102.01V35H103.01V39H104.01V49H105.01V54.01H104V49.01H103V39.01H102V35.01H98.01V36.01H95V33.01H93V29.01H92V28.01H89.01V32.01H85V31.01H84V30.01H81.01V32.01H80.01V34.01H79V31.01H74V30.01H71V28.01H70V26.01H67.01V29.01H62.01V31.01H60V30.01H57.01V32.01H56.01V35H58.01V36H70V35H77.01V41.01H75V38.01H70.01V39.01H69.01V43.01H67.01V44.01H64.01V49.01H61.01V50.01H60.01V52.01H57.01V55H60.01V57H61.01V58.01H60.01V60.01H58.01V63H64V62H65V61H66.01V63H67.01V65H68.01V66.01H67.01V72.01H66V63.01H64.01V64.01H58V63.01H57V60H58V59H59V58H60V57.01H59V56.01H57V55.01H56V52H57V51H59V50H60V49H61V48H63V44H64V43H67V42H68V39H69V38H70V37.01H58V36.01H56V35.01H55V32H56V30H57V29H60.01V30H61V29H62V28H66V26H67V25ZM76 38H75.01V37H70.01V36.01H76V38Z" - fill="#11ED83" + fill={fill} /> - + diff --git a/web/src/dojo/components/useGlobalScores.tsx b/web/src/dojo/components/useGlobalScores.tsx index 796a218d5..fa0e0988f 100644 --- a/web/src/dojo/components/useGlobalScores.tsx +++ b/web/src/dojo/components/useGlobalScores.tsx @@ -1,5 +1,5 @@ import { PlayerEdge, Name, useGlobalScoresQuery } from "@/generated/graphql"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { shortString } from "starknet"; import { SCALING_FACTOR } from ".."; @@ -42,20 +42,16 @@ export class GlobalScores { } export const useGlobalScores = (offset?: number, limit?: number) => { - const [scores, setScores] = useState([]); // Gets top 100 // TODO: paginate with cursor for more scores const { data, isFetched, refetch } = useGlobalScoresQuery({ limit: limit || 100, }); - useEffect(() => { - const scores = GlobalScores.create( - data?.playerComponents?.edges as PlayerEdge[], + const scores: Score[] = useMemo(() => { + return ( + GlobalScores.create(data?.playerComponents?.edges as PlayerEdge[]) || [] ); - if (scores) { - setScores(scores); - } }, [data]); return { diff --git a/web/src/dojo/components/useMarkets.tsx b/web/src/dojo/components/useMarkets.tsx new file mode 100644 index 000000000..5cad76970 --- /dev/null +++ b/web/src/dojo/components/useMarkets.tsx @@ -0,0 +1,68 @@ +import { Market, MarketEdge, useMarketPricesQuery } from "@/generated/graphql"; +import { useEffect, useMemo, useState } from "react"; +import { num } from "starknet"; +import { REFETCH_INTERVAL, SCALING_FACTOR } from ".."; +import { LocationPrices, DrugMarket } from "../types"; + +export class MarketPrices { + locationPrices: LocationPrices; + + constructor(locationMarkets: LocationPrices) { + this.locationPrices = locationMarkets; + } + + static create(edges: MarketEdge[]): LocationPrices | undefined { + if (!edges || edges.length === 0) return undefined; + + const locationPrices: LocationPrices = new Map(); + + for (let edge of edges) { + const node = edge.node; + const locationId = num.toHexString(node?.location_id); + const drugId = num.toHexString(node?.drug_id); + const price = + Number(node?.cash) / Number(node?.quantity) / SCALING_FACTOR; + + const drugMarket: DrugMarket = { + id: drugId, + price: price, + marketPool: node as Market, + }; + + if (locationPrices.has(locationId)) { + locationPrices.get(locationId)?.push(drugMarket); + } else { + locationPrices.set(locationId, [drugMarket]); + } + } + + return locationPrices; + } +} + +export interface MarketsInterface { + locationPrices?: LocationPrices; +} + +export const useMarketPrices = ({ + gameId, +}: { + gameId?: string; +}): MarketsInterface => { + const { data } = useMarketPricesQuery( + { gameId: Number(gameId) }, + + { + enabled: !!gameId, + refetchInterval: REFETCH_INTERVAL, + }, + ); + + const locationPrices = useMemo(() => { + return MarketPrices.create(data?.marketComponents?.edges as MarketEdge[]); + }, [data]); + + return { + locationPrices, + }; +}; diff --git a/web/src/dojo/entities/useGameEntity.tsx b/web/src/dojo/entities/useGameEntity.tsx index 1c2b8ef5c..d556f4d17 100644 --- a/web/src/dojo/entities/useGameEntity.tsx +++ b/web/src/dojo/entities/useGameEntity.tsx @@ -1,5 +1,5 @@ import { Game, useGameEntityQuery } from "@/generated/graphql"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ec, num } from "starknet"; import { REFETCH_INTERVAL } from ".."; @@ -50,8 +50,11 @@ export const useGameEntity = ({ }: { gameId?: string; }): GameInterface => { - const [game, setGame] = useState(); - const [key, setKey] = useState(""); + const key: string = useMemo(() => { + return num.toHex( + ec.starkCurve.poseidonHashMany([num.toBigInt(gameId || "")]), + ); + }, [gameId]); const { data, isFetched } = useGameEntityQuery( { id: key }, @@ -60,16 +63,8 @@ export const useGameEntity = ({ }, ); - useEffect(() => { - if (gameId) { - const key_ = ec.starkCurve.poseidonHashMany([num.toBigInt(gameId)]); - setKey(num.toHex(key_)); - } - }, [gameId]); - - useEffect(() => { - const game_ = GameEntity.create(data as GameEntityData); - if (game_) setGame(game_); + const game = useMemo(() => { + return GameEntity.create(data as GameEntityData); }, [data]); return { diff --git a/web/src/dojo/entities/useLocationEntity.tsx b/web/src/dojo/entities/useLocationEntity.tsx index df433e33a..8f5211d81 100644 --- a/web/src/dojo/entities/useLocationEntity.tsx +++ b/web/src/dojo/entities/useLocationEntity.tsx @@ -1,20 +1,13 @@ import { - Name, Market, Risks, useLocationEntitiesQuery, - Entity, EntityEdge, } from "@/generated/graphql"; -import { useEffect, useState } from "react"; -import { num, shortString } from "starknet"; +import { useMemo } from "react"; +import { num } from "starknet"; import { REFETCH_INTERVAL, SCALING_FACTOR } from ".."; - -export type DrugMarket = { - id: string; // id is hex encoded drug name - price: number; - marketPool: Market; -}; +import { DrugMarket } from "../types"; export class LocationEntity { id: string; // id is hex encoded location name @@ -68,9 +61,6 @@ export class LocationEntity { if (!risksComponent || drugMarkets.length === 0) return undefined; - // sort by price - drugMarkets.sort((a, b) => a.price - b.price); - return { id: locationId, risks: risksComponent, @@ -91,8 +81,6 @@ export const useLocationEntity = ({ gameId?: string; locationId?: string; }): LocationInterface => { - const [location, setLocation] = useState(); - const { data, isFetched } = useLocationEntitiesQuery( { gameId: gameId || "", @@ -104,11 +92,8 @@ export const useLocationEntity = ({ }, ); - useEffect(() => { - const location_ = LocationEntity.create( - data?.entities?.edges as EntityEdge[], - ); - if (location_) setLocation(location_); + const location = useMemo(() => { + return LocationEntity.create(data?.entities?.edges as EntityEdge[]); }, [data]); return { diff --git a/web/src/dojo/entities/usePlayerEntity.tsx b/web/src/dojo/entities/usePlayerEntity.tsx index 08616bf5e..595276697 100644 --- a/web/src/dojo/entities/usePlayerEntity.tsx +++ b/web/src/dojo/entities/usePlayerEntity.tsx @@ -4,7 +4,7 @@ import { usePlayerEntityQuery, EntityEdge, } from "@/generated/graphql"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { REFETCH_INTERVAL, SCALING_FACTOR } from ".."; import { PlayerStatus } from "../types"; @@ -80,9 +80,8 @@ export const usePlayerEntity = ({ gameId?: string; address?: string; }): PlayerInterface => { - const [player, setPlayer] = useState(); // TODO: remove leading zeros in address, maybe implemented in torii - const { data, isFetched, refetch } = usePlayerEntityQuery( + const { data, isFetched } = usePlayerEntityQuery( { gameId: gameId || "", playerId: address || "" }, { enabled: !!gameId && !!address, @@ -90,9 +89,8 @@ export const usePlayerEntity = ({ }, ); - useEffect(() => { - const player_ = PlayerEntity.create(data?.entities?.edges as EntityEdge[]); - if (player_) setPlayer(player_); + const player = useMemo(() => { + return PlayerEntity.create(data?.entities?.edges as EntityEdge[]); }, [data]); return { diff --git a/web/src/dojo/events.ts b/web/src/dojo/events.ts index e07f27b55..52a7dc62e 100644 --- a/web/src/dojo/events.ts +++ b/web/src/dojo/events.ts @@ -60,6 +60,9 @@ export interface DecisionEventData extends BaseEventData { export interface ConsequenceEventData extends BaseEventData { playerId: string; outcome: Outcome; + healthLoss: number; + drugLoss: number; + cashLoss: number; } export const parseEvent = ( @@ -110,6 +113,9 @@ export const parseEvent = ( gameId: num.toHexString(raw.data[0]), playerId: num.toHexString(raw.data[1]), outcome: Number(raw.data[2]), + healthLoss: Number(raw.data[3]), + drugLoss: Number(raw.data[4]), + cashLoss: Number(raw.data[5]), } as ConsequenceEventData; case RyoEvents.Traveled: case RyoEvents.Bought: diff --git a/web/src/dojo/helpers.ts b/web/src/dojo/helpers.ts index 828d08605..71e50191e 100644 --- a/web/src/dojo/helpers.ts +++ b/web/src/dojo/helpers.ts @@ -19,6 +19,7 @@ import { import { Drug, DrugInfo, + DrugMarket, Location, LocationInfo, Outcome, @@ -119,7 +120,7 @@ const drugs: DrugInfo[] = [ export const outcomes: OutcomeInfo[] = [ { - name: "Bribed the Cop", + name: "Paid the Cop", type: Outcome.Paid, status: PlayerStatus.BeingArrested, imageSrc: "/images/sunset.png", @@ -129,17 +130,7 @@ export const outcomes: OutcomeInfo[] = [ color: "yellow.400", }, { - name: "Got Arrested", - type: Outcome.Captured, - status: PlayerStatus.BeingArrested, - imageSrc: "/images/events/police_cruiser.gif", - description: "You lost 50% of all your drugs", - getResponse: (isInitial: boolean) => - getCopResponses(Outcome.Captured, isInitial), - color: "red", - }, - { - name: "Escaped the Cops", + name: "Escaped", type: Outcome.Escaped, status: PlayerStatus.BeingArrested, imageSrc: "/images/events/escaped.png", @@ -148,27 +139,17 @@ export const outcomes: OutcomeInfo[] = [ color: "neon.200", }, { - name: "Fought the Gang", - type: Outcome.Fought, - status: PlayerStatus.BeingMugged, - imageSrc: "/images/events/fought.png", - description: "You lost some health", - getResponse: (isInitial: boolean) => - getMuggerResponses(Outcome.Fought, isInitial), - color: "yellow.400", - }, - { - name: "Got Captured", - type: Outcome.Captured, + name: "Paid the Gang", + type: Outcome.Paid, status: PlayerStatus.BeingMugged, imageSrc: "/images/sunset.png", - description: "You lost 50% of all your cash", + description: "You paid the gang off", getResponse: (isInitial: boolean) => - getMuggerResponses(Outcome.Captured, isInitial), - color: "red", + getMuggerResponses(Outcome.Escaped, isInitial), + color: "neon.200", }, { - name: "Escaped the Gang", + name: "Escaped", type: Outcome.Escaped, status: PlayerStatus.BeingMugged, imageSrc: "/images/events/escaped.png", @@ -182,28 +163,28 @@ function findBy(array: T[], key: keyof T, value: any): T | undefined { return array.find((item) => item[key] === value); } -export function getLocationByType(type: Location): LocationInfo { - return findBy(locations, "type", type) || locations[0]; +export function getLocationByType(type: Location) { + return findBy(locations, "type", type); } -export function getLocationById(id: string): LocationInfo { - return findBy(locations, "id", id) || locations[0]; +export function getLocationById(id: string) { + return findBy(locations, "id", id); } -export function getLocationBySlug(slug: string): LocationInfo { - return findBy(locations, "slug", slug) || locations[0]; +export function getLocationBySlug(slug: string) { + return findBy(locations, "slug", slug); } -export function getDrugById(id: string): DrugInfo { - return findBy(drugs, "id", id) || drugs[0]; +export function getDrugById(id: string) { + return findBy(drugs, "id", id); } -export function getDrugBySlug(slug: string): DrugInfo { - return findBy(drugs, "slug", slug) || drugs[0]; +export function getDrugBySlug(slug: string) { + return findBy(drugs, "slug", slug); } -export function getDrugByType(type: Drug): DrugInfo { - return findBy(drugs, "type", type) || drugs[0]; +export function getDrugByType(type: Drug) { + return findBy(drugs, "type", type); } export function getOutcomeInfo( @@ -216,3 +197,29 @@ export function getOutcomeInfo( }) || outcomes[0] ); } + +export function sortDrugMarkets(drugMarkets?: DrugMarket[]): DrugMarket[] { + if (!drugMarkets) { + return []; + } + + const ludes = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Ludes, + )!; + const speed = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Speed, + )!; + const weed = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Weed, + )!; + const acid = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Acid, + )!; + const heroin = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Heroin, + )!; + const cocaine = drugMarkets.find( + (drug) => getDrugById(drug.id)?.type === Drug.Cocaine, + )!; + return [ludes, speed, weed, acid, heroin, cocaine]; +} diff --git a/web/src/dojo/types.ts b/web/src/dojo/types.ts index 322d93a16..137c3c989 100644 --- a/web/src/dojo/types.ts +++ b/web/src/dojo/types.ts @@ -1,3 +1,5 @@ +import { Market } from "@/generated/graphql"; + export enum Location { Queens, Bronx, @@ -25,13 +27,11 @@ export enum PlayerStatus { export enum Action { Run, Pay, - Fight, } export enum Outcome { Died, Paid, - Fought, Escaped, Captured, } @@ -61,3 +61,11 @@ export interface OutcomeInfo { getResponse: (isInitial: boolean) => string; color: string; } + +export type DrugMarket = { + id: string; // id is hex encoded drug name + price: number; + marketPool: Market; +}; + +export type LocationPrices = Map; diff --git a/web/src/generated/graphql.ts b/web/src/generated/graphql.ts index 3d37ed642..b407071d9 100644 --- a/web/src/generated/graphql.ts +++ b/web/src/generated/graphql.ts @@ -383,6 +383,7 @@ export type Player = { health?: Maybe; location_id?: Maybe; player_id?: Maybe; + run_attempts?: Maybe; status?: Maybe; turns_remaining?: Maybe; }; @@ -412,6 +413,7 @@ export enum PlayerOrderOrderField { Health = "HEALTH", LocationId = "LOCATION_ID", PlayerId = "PLAYER_ID", + RunAttempts = "RUN_ATTEMPTS", Status = "STATUS", TurnsRemaining = "TURNS_REMAINING", } @@ -459,6 +461,12 @@ export type PlayerWhereInput = { player_idLT?: InputMaybe; player_idLTE?: InputMaybe; player_idNEQ?: InputMaybe; + run_attempts?: InputMaybe; + run_attemptsGT?: InputMaybe; + run_attemptsGTE?: InputMaybe; + run_attemptsLT?: InputMaybe; + run_attemptsLTE?: InputMaybe; + run_attemptsNEQ?: InputMaybe; status?: InputMaybe; statusGT?: InputMaybe; statusGTE?: InputMaybe; @@ -577,10 +585,10 @@ export type QuerySystemCallArgs = { export type Risks = { __typename?: "Risks"; + capture?: Maybe; entity?: Maybe; game_id?: Maybe; location_id?: Maybe; - run?: Maybe; travel?: Maybe; }; @@ -602,13 +610,19 @@ export type RisksOrder = { }; export enum RisksOrderOrderField { + Capture = "CAPTURE", GameId = "GAME_ID", LocationId = "LOCATION_ID", - Run = "RUN", Travel = "TRAVEL", } export type RisksWhereInput = { + capture?: InputMaybe; + captureGT?: InputMaybe; + captureGTE?: InputMaybe; + captureLT?: InputMaybe; + captureLTE?: InputMaybe; + captureNEQ?: InputMaybe; game_id?: InputMaybe; game_idGT?: InputMaybe; game_idGTE?: InputMaybe; @@ -621,12 +635,6 @@ export type RisksWhereInput = { location_idLT?: InputMaybe; location_idLTE?: InputMaybe; location_idNEQ?: InputMaybe; - run?: InputMaybe; - runGT?: InputMaybe; - runGTE?: InputMaybe; - runLT?: InputMaybe; - runLTE?: InputMaybe; - runNEQ?: InputMaybe; travel?: InputMaybe; travelGT?: InputMaybe; travelGTE?: InputMaybe; @@ -740,6 +748,27 @@ export type GlobalScoresQuery = { } | null; }; +export type MarketPricesQueryVariables = Exact<{ + gameId?: InputMaybe; +}>; + +export type MarketPricesQuery = { + __typename?: "Query"; + marketComponents?: { + __typename?: "MarketConnection"; + edges?: Array<{ + __typename?: "MarketEdge"; + node?: { + __typename?: "Market"; + drug_id?: any | null; + location_id?: any | null; + quantity?: any | null; + cash?: any | null; + } | null; + } | null> | null; + } | null; +}; + export type GameEntityQueryVariables = Exact<{ id: Scalars["ID"]; }>; @@ -792,12 +821,12 @@ export type PlayerEntityQuery = { | { __typename: "Player"; cash?: any | null; + status?: any | null; health?: any | null; - turns_remaining?: any | null; drug_count?: any | null; bag_limit?: any | null; location_id?: any | null; - status?: any | null; + turns_remaining?: any | null; } | { __typename: "Risks" } | null @@ -829,7 +858,7 @@ export type LocationEntitiesQuery = { | { __typename: "Market"; cash?: any | null; quantity?: any | null } | { __typename: "Name" } | { __typename: "Player" } - | { __typename: "Risks"; travel?: any | null; run?: any | null } + | { __typename: "Risks"; travel?: any | null } | null > | null; } | null; @@ -966,6 +995,62 @@ useInfiniteGlobalScoresQuery.getKey = ( variables === undefined ? ["GlobalScores.infinite"] : ["GlobalScores.infinite", variables]; +export const MarketPricesDocument = ` + query MarketPrices($gameId: Int) { + marketComponents(first: 36, where: {game_id: $gameId}) { + edges { + node { + drug_id + location_id + quantity + cash + } + } + } +} + `; +export const useMarketPricesQuery = < + TData = MarketPricesQuery, + TError = unknown, +>( + variables?: MarketPricesQueryVariables, + options?: UseQueryOptions, +) => + useQuery( + variables === undefined ? ["MarketPrices"] : ["MarketPrices", variables], + useFetchData( + MarketPricesDocument, + ).bind(null, variables), + options, + ); + +useMarketPricesQuery.getKey = (variables?: MarketPricesQueryVariables) => + variables === undefined ? ["MarketPrices"] : ["MarketPrices", variables]; +export const useInfiniteMarketPricesQuery = < + TData = MarketPricesQuery, + TError = unknown, +>( + variables?: MarketPricesQueryVariables, + options?: UseInfiniteQueryOptions, +) => { + const query = useFetchData( + MarketPricesDocument, + ); + return useInfiniteQuery( + variables === undefined + ? ["MarketPrices.infinite"] + : ["MarketPrices.infinite", variables], + (metaData) => query({ ...variables, ...(metaData.pageParam ?? {}) }), + options, + ); +}; + +useInfiniteMarketPricesQuery.getKey = ( + variables?: MarketPricesQueryVariables, +) => + variables === undefined + ? ["MarketPrices.infinite"] + : ["MarketPrices.infinite", variables]; export const GameEntityDocument = ` query GameEntity($id: ID!) { entity(id: $id) { @@ -1031,12 +1116,12 @@ export const PlayerEntityDocument = ` __typename ... on Player { cash + status health - turns_remaining drug_count bag_limit location_id - status + turns_remaining } ... on Drug { drug_id @@ -1103,7 +1188,6 @@ export const LocationEntitiesDocument = ` } ... on Risks { travel - run } } } diff --git a/web/src/graphql/components.graphql b/web/src/graphql/components.graphql index e3647705e..96435f0c1 100644 --- a/web/src/graphql/components.graphql +++ b/web/src/graphql/components.graphql @@ -38,3 +38,16 @@ query GlobalScores($limit: Int) { } } } + +query MarketPrices($gameId: Int) { + marketComponents(first: 36, where: { game_id: $gameId }) { + edges { + node { + drug_id + location_id + quantity + cash + } + } + } +} diff --git a/web/src/graphql/entities.graphql b/web/src/graphql/entities.graphql index 8d05acacd..48fc5ce48 100644 --- a/web/src/graphql/entities.graphql +++ b/web/src/graphql/entities.graphql @@ -56,7 +56,7 @@ query LocationEntities($gameId: String!, $locationId: String!) { } ... on Risks { travel - run + #capture } } } diff --git a/web/src/pages/[gameId]/[locationSlug]/[drugSlug]/[tradeDirection].tsx b/web/src/pages/[gameId]/[locationSlug]/[drugSlug]/[tradeDirection].tsx index e71b0230b..e88947c73 100644 --- a/web/src/pages/[gameId]/[locationSlug]/[drugSlug]/[tradeDirection].tsx +++ b/web/src/pages/[gameId]/[locationSlug]/[drugSlug]/[tradeDirection].tsx @@ -19,17 +19,14 @@ import { Slider, SliderTrack, SliderFilledTrack } from "@chakra-ui/react"; import { Sounds, playSound } from "@/hooks/sound"; import { TradeDirection, TradeType, usePlayerStore } from "@/hooks/state"; import AlertMessage from "@/components/AlertMessage"; -import { - DrugMarket, - useLocationEntity, -} from "@/dojo/entities/useLocationEntity"; +import { useLocationEntity } from "@/dojo/entities/useLocationEntity"; import { PlayerEntity, usePlayerEntity } from "@/dojo/entities/usePlayerEntity"; import { formatQuantity, formatCash } from "@/utils/ui"; import { useSystems } from "@/dojo/systems/useSystems"; import { calculateMaxQuantity, calculateSlippage } from "@/utils/market"; import { useToast } from "@/hooks/toast"; import { getDrugBySlug, getLocationBySlug } from "@/dojo/helpers"; -import { DrugInfo } from "@/dojo/types"; +import { DrugInfo, DrugMarket } from "@/dojo/types"; import { useDojo } from "@/dojo"; import { cardPixelatedStyle } from "@/theme/styles"; @@ -48,12 +45,15 @@ export default function Market() { const [quantitySell, setQuantitySell] = useState(0); const [canSell, setCanSell] = useState(false); const [canBuy, setCanBuy] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const { buy, sell, error: txError } = useSystems(); + const { addTrade } = usePlayerStore(); const { account } = useDojo(); const { location: locationEntity } = useLocationEntity({ gameId, - locationId: location.id, + locationId: location?.id, }); const { player: playerEntity } = usePlayerEntity({ gameId, @@ -66,49 +66,44 @@ export default function Market() { useEffect(() => { if (!locationEntity || !playerEntity || isSubmitting) return; - const market = locationEntity.drugMarkets.find((d) => d.id === drug.id); + const market = locationEntity.drugMarkets.find((d) => d.id === drug?.id); if (!market) return; - const playerDrug = playerEntity.drugs.find((d) => d.id === drug.id); + const playerDrug = playerEntity.drugs.find((d) => d.id === drug?.id); if (playerDrug) { setCanSell(playerDrug.quantity > 0); } setCanBuy(playerEntity.cash > market.price); setMarket(market); - }, [locationEntity, playerEntity, drug]); - - const [isSubmitting, setIsSubmitting] = useState(false); - const { buy, sell, error: txError } = useSystems(); - const { addTrade } = usePlayerStore(); + }, [locationEntity, playerEntity, drug, isSubmitting]); const onTrade = useCallback(async () => { setIsSubmitting(true); playSound(Sounds.Trade); - router.push(`/${gameId}/${location.slug}`); + router.push(`/${gameId}/${location!.slug}`); let toastMessage = "", hash = "", quantity; if (tradeDirection === TradeDirection.Buy) { - ({ hash } = await buy(gameId, location.name, drug.name, quantityBuy)); - toastMessage = `You bought ${quantityBuy} ${drug.name}`; + ({ hash } = await buy(gameId, location!.name, drug!.name, quantityBuy)); + toastMessage = `You bought ${quantityBuy} ${drug!.name}`; quantity = quantityBuy; } else if (tradeDirection === TradeDirection.Sell) { - ({ hash } = await sell(gameId, location.name, drug.name, quantitySell)); - toastMessage = `You sold ${quantitySell} ${drug.name}`; + ({ hash } = await sell(gameId, location!.name, drug!.name, quantitySell)); + toastMessage = `You sold ${quantitySell} ${drug!.name}`; quantity = quantitySell; } toast(toastMessage, Cart, `http://amazing_explorer/${hash}`); - addTrade(drug.type, { + addTrade(drug!.type, { direction: tradeDirection, quantity, } as TradeType); - }, [ tradeDirection, quantityBuy, @@ -236,6 +231,7 @@ const QuantitySelector = ({ (d) => d.id === drug.id, )?.quantity; setMax(playerQuantity || 0); + setQuantity(playerQuantity || 0); } }, [type, drug, player, market]); diff --git a/web/src/pages/[gameId]/[locationSlug]/index.tsx b/web/src/pages/[gameId]/[locationSlug]/index.tsx index 510e7eb8b..cd41f2253 100644 --- a/web/src/pages/[gameId]/[locationSlug]/index.tsx +++ b/web/src/pages/[gameId]/[locationSlug]/index.tsx @@ -12,14 +12,12 @@ import { SimpleGrid, StyleProps, useDisclosure, - useBreakpointValue, Flex, } from "@chakra-ui/react"; import Layout from "@/components/Layout"; import { useRouter } from "next/router"; import { Cart } from "@/components/icons"; import { Footer } from "@/components/Footer"; -import { Sounds, playSound } from "@/hooks/sound"; import { useLocationEntity } from "@/dojo/entities/useLocationEntity"; import { usePlayerEntity } from "@/dojo/entities/usePlayerEntity"; import { formatQuantity, formatCash } from "@/utils/ui"; @@ -32,13 +30,14 @@ import { getDrugById, getLocationById, getLocationBySlug, + sortDrugMarkets, } from "@/dojo/helpers"; import { motion } from "framer-motion"; export default function Location() { const router = useRouter(); const gameId = router.query.gameId as string; - const locationId = getLocationBySlug(router.query.locationSlug as string).id; + const locationId = getLocationBySlug(router.query.locationSlug as string)?.id; const { account } = useDojo(); const { location: locationEntity } = useLocationEntity({ @@ -58,7 +57,7 @@ export default function Location() { // check if player at right location if (locationId !== playerEntity.locationId) { router.replace( - `/${gameId}/${getLocationById(playerEntity.locationId).slug}`, + `/${gameId}/${getLocationById(playerEntity.locationId)?.slug}`, ); return; } @@ -82,7 +81,7 @@ export default function Location() { title: shortString.decodeShortString(locationEntity.id), prefixTitle: prefixTitle, imageSrc: `/images/locations/${ - getLocationById(locationEntity.id).slug + getLocationById(locationEntity.id)?.slug }.png`, }} > @@ -103,8 +102,8 @@ export default function Location() { Market - {locationEntity.drugMarkets.map((drug, index) => { - const drugInfo = getDrugById(drug.id); + {sortDrugMarkets(locationEntity.drugMarkets).map((drug, index) => { + const drugInfo = getDrugById(drug.id)!; const canBuy = drug.price <= playerEntity.cash && playerEntity.drugCount < playerEntity.bagLimit; diff --git a/web/src/pages/[gameId]/end.tsx b/web/src/pages/[gameId]/end.tsx index c45eba20a..1f0a2b5e7 100644 --- a/web/src/pages/[gameId]/end.tsx +++ b/web/src/pages/[gameId]/end.tsx @@ -19,7 +19,7 @@ import { ModalContent, ModalHeader, ModalBody, - ModalFooter, + ModalFooter, UnorderedList, ListItem, Link, @@ -33,12 +33,15 @@ import { ReactNode, useCallback, useState } from "react"; export default function End() { const router = useRouter(); const gameId = router.query.gameId as string; - const { setName: submitSetName, isPending } = useSystems(); + const { setName: submitSetName } = useSystems(); const [name, setName] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const [isCreditOpen, setIsCreditOpen] = useState(false); const onSubmitName = useCallback(async () => { if (!name) return; + + setIsSubmitting(true); await submitSetName(gameId, name); router.push("/"); }, [name, gameId, router, submitSetName]); @@ -126,7 +129,7 @@ export default function End() { @@ -137,7 +140,9 @@ export default function End() { - CREDITS + + CREDITS + @@ -171,7 +176,9 @@ export default function End() { - + diff --git a/web/src/pages/[gameId]/event/consequence.tsx b/web/src/pages/[gameId]/event/consequence.tsx index 1e52b4e9e..6ce94c748 100644 --- a/web/src/pages/[gameId]/event/consequence.tsx +++ b/web/src/pages/[gameId]/event/consequence.tsx @@ -5,6 +5,10 @@ import { getOutcomeInfo } from "@/dojo/helpers"; import { Heading, Text, VStack } from "@chakra-ui/react"; import { useRouter } from "next/router"; import Button from "@/components/Button"; +import { Outcome } from "@/dojo/types"; +import { usePlayerEntity } from "@/dojo/entities/usePlayerEntity"; +import { useDojo } from "@/dojo"; +import { useMemo } from "react"; export default function Consequence() { const router = useRouter(); @@ -14,9 +18,15 @@ export default function Consequence() { Number(router.query.outcome), ); - const response = outcome.getResponse(true); + const { account } = useDojo(); + const { player: playerEntity } = usePlayerEntity({ + gameId, + address: account?.address, + }); - if (!router.isReady) { + const response = useMemo(() => outcome.getResponse(true), [outcome]); + + if (!router.isReady || !playerEntity) { return <>; } @@ -41,7 +51,7 @@ export default function Consequence() { width={400} height={400} /> - + {response} @@ -52,6 +62,13 @@ export default function Consequence() { - {pay && ( - - )} - {fight && ( - - )} + diff --git a/web/src/pages/[gameId]/travel.tsx b/web/src/pages/[gameId]/travel.tsx index 441b318bf..fa0e34d53 100644 --- a/web/src/pages/[gameId]/travel.tsx +++ b/web/src/pages/[gameId]/travel.tsx @@ -8,20 +8,31 @@ import { Text, Divider, useEventListener, + Card, + Grid, + GridItem, Spacer, Image, + useDisclosure, } from "@chakra-ui/react"; import { useRouter } from "next/router"; import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { generatePixelBorderPath } from "@/utils/ui"; +import { formatCash, generatePixelBorderPath } from "@/utils/ui"; import { Map } from "@/components/map"; import { motion } from "framer-motion"; import { useSystems } from "@/dojo/systems/useSystems"; import { usePlayerEntity } from "@/dojo/entities/usePlayerEntity"; import { useToast } from "@/hooks/toast"; import { useDojo } from "@/dojo"; -import { getLocationById, getLocationByType, locations } from "@/dojo/helpers"; +import { + getDrugById, + getLocationById, + getLocationByType, + locations, + sortDrugMarkets, +} from "@/dojo/helpers"; import { LocationInfo } from "@/dojo/types"; +import { useMarketPrices } from "@/dojo/components/useMarkets"; export default function Travel() { const router = useRouter(); @@ -29,6 +40,7 @@ export default function Travel() { const [targetId, setTargetId] = useState(""); const [currentLocationId, setCurrentLocationId] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const { isOpen: isPercentage, onToggle: togglePercentage } = useDisclosure(); const { toast } = useToast(); const { account } = useDojo(); @@ -38,14 +50,43 @@ export default function Travel() { address: account?.address, }); + const { locationPrices } = useMarketPrices({ + gameId, + }); + useEffect(() => { if (playerEntity && !isSubmitting) { - const location = getLocationById(playerEntity.locationId); - setCurrentLocationId(location.id); - setTargetId(location.id); + setCurrentLocationId(playerEntity.locationId); + setTargetId(playerEntity.locationId); } }, [playerEntity, isSubmitting]); + const targetMarkets = useMemo(() => { + if (locationPrices) { + const currentMarkets = sortDrugMarkets( + locationPrices.get(currentLocationId), + ); + const targetMarkets = sortDrugMarkets(locationPrices.get(targetId)); + + return targetMarkets.map((drug, index) => { + const diff = drug.price - currentMarkets[index].price; + const percentage = + (Math.abs(drug.price - currentMarkets[index].price) / + currentMarkets[index].price) * + 100; + + return { + id: drug.id, + price: drug.price, + diff, + percentage, + }; + }); + } + + return []; + }, [locationPrices, targetId, currentLocationId]); + useEventListener("keydown", (e) => { switch (e.key) { case "ArrowRight": @@ -81,21 +122,21 @@ export default function Travel() { if (targetId) { setIsSubmitting(true); const { event, hash } = await travel(gameId, targetId); - console.log(event, hash); if (event) { - router.push(`/${gameId}/event/decision?nextId=${targetId}`); - } else { - toast( - `You've traveled to ${getLocationById(targetId).name}`, - Car, - `http://amazing_explorer/${hash}`, - ); - - router.push(`/${gameId}/turn`); + return router.push(`/${gameId}/event/decision?nextId=${targetId}`); } + + toast( + `You've traveled to ${getLocationById(targetId)?.name}`, + Car, + `http://amazing_explorer/${hash}`, + ); + router.push(`/${gameId}/turn`); } }, [targetId, router, gameId, travel, toast]); + if (!playerEntity || !locationPrices) return <>; + return ( { - setTargetId(getLocationByType(selected).id); + setTargetId(getLocationByType(selected)!.id); }} /> ), }} - showMap={true} - showBack={true} + showBack > - <> - - - {locations.map((location, index) => ( - setTargetId(location.id)} - /> - ))} - - + ({isPercentage ? "#" : "%"}) + + + + + + + {targetMarkets.map((drug, index) => { + return ( + + + {getDrugById(drug.id)?.icon({ + boxSize: "24px", + })} + ${drug.price.toFixed(0)} + {drug.diff !== 0 && ( + = 0 ? "neon.200" : "red"} + > + ( + {isPercentage + ? `${drug.percentage.toFixed(0)}%` + : formatCash(drug.diff)} + ) + + )} + + + ); + })} + + - + - - + {getLocationById(targetId)?.name} + + + + + ); } diff --git a/web/src/pages/[gameId]/turn.tsx b/web/src/pages/[gameId]/turn.tsx index a854addf4..39ee6b61e 100644 --- a/web/src/pages/[gameId]/turn.tsx +++ b/web/src/pages/[gameId]/turn.tsx @@ -35,7 +35,7 @@ export default function Turn() { return <>; } - const locationInfo = getLocationById(playerEntity.locationId); + const locationInfo = getLocationById(playerEntity.locationId)!; return ( { const change = trade.direction === TradeDirection.Buy ? "+" : "-"; - const drugInfo = getDrugByType(drug); + const drugInfo = getDrugByType(drug)!; return ( { resetTurn(); - router.push(`/${gameId}/${locationInfo.slug})}`); + router.push(`/${gameId}/${locationInfo.slug}`); }} > Continue diff --git a/web/src/pages/index.tsx b/web/src/pages/index.tsx index 56ea5d805..9f66c1afb 100644 --- a/web/src/pages/index.tsx +++ b/web/src/pages/index.tsx @@ -31,7 +31,7 @@ import { useEffect, useState } from "react"; // hardcode game params for now const START_TIME = 0; const MAX_PLAYERS = 1; -const NUM_TURNS = 14; +const NUM_TURNS = 9; export default function Home() { const router = useRouter(); @@ -50,15 +50,13 @@ export default function Home() { ); const [isTutorialOpen, setIsTutorialOpen] = useState(false); - const isLocal = true; - + const isLocal = true; return ( - {isGated ? ( @@ -69,7 +67,6 @@ export default function Home() { Get ready hustlers... Season II starts in September - ) : ( <>