diff --git a/public/cards/j_8_ball.png b/public/cards/j_8_ball.png new file mode 100644 index 0000000..9a533f3 Binary files /dev/null and b/public/cards/j_8_ball.png differ diff --git a/public/cards/j_abstract.png b/public/cards/j_abstract.png new file mode 100644 index 0000000..3582e16 Binary files /dev/null and b/public/cards/j_abstract.png differ diff --git a/public/cards/j_acrobat.png b/public/cards/j_acrobat.png new file mode 100644 index 0000000..1375607 Binary files /dev/null and b/public/cards/j_acrobat.png differ diff --git a/public/cards/j_ancient.png b/public/cards/j_ancient.png new file mode 100644 index 0000000..8e02f7f Binary files /dev/null and b/public/cards/j_ancient.png differ diff --git a/public/cards/j_arrowhead.png b/public/cards/j_arrowhead.png new file mode 100644 index 0000000..f12165c Binary files /dev/null and b/public/cards/j_arrowhead.png differ diff --git a/public/cards/j_astronomer.png b/public/cards/j_astronomer.png new file mode 100644 index 0000000..d2f236e Binary files /dev/null and b/public/cards/j_astronomer.png differ diff --git a/public/cards/j_banner.png b/public/cards/j_banner.png new file mode 100644 index 0000000..628bee1 Binary files /dev/null and b/public/cards/j_banner.png differ diff --git a/public/cards/j_baron.png b/public/cards/j_baron.png new file mode 100644 index 0000000..370847c Binary files /dev/null and b/public/cards/j_baron.png differ diff --git a/public/cards/j_baseball.png b/public/cards/j_baseball.png new file mode 100644 index 0000000..50ee74d Binary files /dev/null and b/public/cards/j_baseball.png differ diff --git a/public/cards/j_blackboard.png b/public/cards/j_blackboard.png new file mode 100644 index 0000000..38f31a8 Binary files /dev/null and b/public/cards/j_blackboard.png differ diff --git a/public/cards/j_bloodstone.png b/public/cards/j_bloodstone.png new file mode 100644 index 0000000..1d530c5 Binary files /dev/null and b/public/cards/j_bloodstone.png differ diff --git a/public/cards/j_blue_joker.png b/public/cards/j_blue_joker.png new file mode 100644 index 0000000..669e6af Binary files /dev/null and b/public/cards/j_blue_joker.png differ diff --git a/public/cards/j_blueprint.png b/public/cards/j_blueprint.png new file mode 100644 index 0000000..94b130b Binary files /dev/null and b/public/cards/j_blueprint.png differ diff --git a/public/cards/j_bootstraps.png b/public/cards/j_bootstraps.png new file mode 100644 index 0000000..dc55093 Binary files /dev/null and b/public/cards/j_bootstraps.png differ diff --git a/public/cards/j_brainstorm.png b/public/cards/j_brainstorm.png new file mode 100644 index 0000000..59ce5e4 Binary files /dev/null and b/public/cards/j_brainstorm.png differ diff --git a/public/cards/j_bull.png b/public/cards/j_bull.png new file mode 100644 index 0000000..1682587 Binary files /dev/null and b/public/cards/j_bull.png differ diff --git a/public/cards/j_burglar.png b/public/cards/j_burglar.png new file mode 100644 index 0000000..c3d3582 Binary files /dev/null and b/public/cards/j_burglar.png differ diff --git a/public/cards/j_burnt.png b/public/cards/j_burnt.png new file mode 100644 index 0000000..62ad8bb Binary files /dev/null and b/public/cards/j_burnt.png differ diff --git a/public/cards/j_business.png b/public/cards/j_business.png new file mode 100644 index 0000000..721ac9b Binary files /dev/null and b/public/cards/j_business.png differ diff --git a/public/cards/j_caino.png b/public/cards/j_caino.png new file mode 100644 index 0000000..575166c Binary files /dev/null and b/public/cards/j_caino.png differ diff --git a/public/cards/j_campfire.png b/public/cards/j_campfire.png new file mode 100644 index 0000000..e5ea030 Binary files /dev/null and b/public/cards/j_campfire.png differ diff --git a/public/cards/j_card_sharp.png b/public/cards/j_card_sharp.png new file mode 100644 index 0000000..baea074 Binary files /dev/null and b/public/cards/j_card_sharp.png differ diff --git a/public/cards/j_cartomancer.png b/public/cards/j_cartomancer.png new file mode 100644 index 0000000..79502f0 Binary files /dev/null and b/public/cards/j_cartomancer.png differ diff --git a/public/cards/j_castle.png b/public/cards/j_castle.png new file mode 100644 index 0000000..0b2d45f Binary files /dev/null and b/public/cards/j_castle.png differ diff --git a/public/cards/j_cavendish.png b/public/cards/j_cavendish.png new file mode 100644 index 0000000..ab97ec3 Binary files /dev/null and b/public/cards/j_cavendish.png differ diff --git a/public/cards/j_ceremonial.png b/public/cards/j_ceremonial.png new file mode 100644 index 0000000..dc4226b Binary files /dev/null and b/public/cards/j_ceremonial.png differ diff --git a/public/cards/j_certificate.png b/public/cards/j_certificate.png new file mode 100644 index 0000000..654dac3 Binary files /dev/null and b/public/cards/j_certificate.png differ diff --git a/public/cards/j_chaos.png b/public/cards/j_chaos.png new file mode 100644 index 0000000..fb466f1 Binary files /dev/null and b/public/cards/j_chaos.png differ diff --git a/public/cards/j_clever.png b/public/cards/j_clever.png new file mode 100644 index 0000000..9d14cba Binary files /dev/null and b/public/cards/j_clever.png differ diff --git a/public/cards/j_cloud_9.png b/public/cards/j_cloud_9.png new file mode 100644 index 0000000..31294ca Binary files /dev/null and b/public/cards/j_cloud_9.png differ diff --git a/public/cards/j_constellation.png b/public/cards/j_constellation.png new file mode 100644 index 0000000..81618fe Binary files /dev/null and b/public/cards/j_constellation.png differ diff --git a/public/cards/j_crafty.png b/public/cards/j_crafty.png new file mode 100644 index 0000000..61886c5 Binary files /dev/null and b/public/cards/j_crafty.png differ diff --git a/public/cards/j_crazy.png b/public/cards/j_crazy.png new file mode 100644 index 0000000..131b12b Binary files /dev/null and b/public/cards/j_crazy.png differ diff --git a/public/cards/j_credit_card.png b/public/cards/j_credit_card.png new file mode 100644 index 0000000..b43f73a Binary files /dev/null and b/public/cards/j_credit_card.png differ diff --git a/public/cards/j_delayed_grat.png b/public/cards/j_delayed_grat.png new file mode 100644 index 0000000..897bc26 Binary files /dev/null and b/public/cards/j_delayed_grat.png differ diff --git a/public/cards/j_devious.png b/public/cards/j_devious.png new file mode 100644 index 0000000..7657859 Binary files /dev/null and b/public/cards/j_devious.png differ diff --git a/public/cards/j_diet_cola.png b/public/cards/j_diet_cola.png new file mode 100644 index 0000000..57e184f Binary files /dev/null and b/public/cards/j_diet_cola.png differ diff --git a/public/cards/j_dna.png b/public/cards/j_dna.png new file mode 100644 index 0000000..589bfcf Binary files /dev/null and b/public/cards/j_dna.png differ diff --git a/public/cards/j_drivers_license.png b/public/cards/j_drivers_license.png new file mode 100644 index 0000000..5c7d5f8 Binary files /dev/null and b/public/cards/j_drivers_license.png differ diff --git a/public/cards/j_droll.png b/public/cards/j_droll.png new file mode 100644 index 0000000..a56e14c Binary files /dev/null and b/public/cards/j_droll.png differ diff --git a/public/cards/j_drunkard.png b/public/cards/j_drunkard.png new file mode 100644 index 0000000..a73c2b0 Binary files /dev/null and b/public/cards/j_drunkard.png differ diff --git a/public/cards/j_duo.png b/public/cards/j_duo.png new file mode 100644 index 0000000..b90a969 Binary files /dev/null and b/public/cards/j_duo.png differ diff --git a/public/cards/j_dusk.png b/public/cards/j_dusk.png new file mode 100644 index 0000000..b1ac695 Binary files /dev/null and b/public/cards/j_dusk.png differ diff --git a/public/cards/j_egg.png b/public/cards/j_egg.png new file mode 100644 index 0000000..5751d60 Binary files /dev/null and b/public/cards/j_egg.png differ diff --git a/public/cards/j_erosion.png b/public/cards/j_erosion.png new file mode 100644 index 0000000..dfd3d2b Binary files /dev/null and b/public/cards/j_erosion.png differ diff --git a/public/cards/j_even_steven.png b/public/cards/j_even_steven.png new file mode 100644 index 0000000..cd06a74 Binary files /dev/null and b/public/cards/j_even_steven.png differ diff --git a/public/cards/j_faceless.png b/public/cards/j_faceless.png new file mode 100644 index 0000000..28ebbac Binary files /dev/null and b/public/cards/j_faceless.png differ diff --git a/public/cards/j_family.png b/public/cards/j_family.png new file mode 100644 index 0000000..b2b062b Binary files /dev/null and b/public/cards/j_family.png differ diff --git a/public/cards/j_fibonacci.png b/public/cards/j_fibonacci.png new file mode 100644 index 0000000..557a73f Binary files /dev/null and b/public/cards/j_fibonacci.png differ diff --git a/public/cards/j_flash.png b/public/cards/j_flash.png new file mode 100644 index 0000000..0c19216 Binary files /dev/null and b/public/cards/j_flash.png differ diff --git a/public/cards/j_flower_pot.png b/public/cards/j_flower_pot.png new file mode 100644 index 0000000..e9edadb Binary files /dev/null and b/public/cards/j_flower_pot.png differ diff --git a/public/cards/j_fortune_teller.png b/public/cards/j_fortune_teller.png new file mode 100644 index 0000000..f3a6c40 Binary files /dev/null and b/public/cards/j_fortune_teller.png differ diff --git a/public/cards/j_four_fingers.png b/public/cards/j_four_fingers.png new file mode 100644 index 0000000..1a9fcea Binary files /dev/null and b/public/cards/j_four_fingers.png differ diff --git a/public/cards/j_gift.png b/public/cards/j_gift.png new file mode 100644 index 0000000..4423442 Binary files /dev/null and b/public/cards/j_gift.png differ diff --git a/public/cards/j_glass.png b/public/cards/j_glass.png new file mode 100644 index 0000000..3d8bb0a Binary files /dev/null and b/public/cards/j_glass.png differ diff --git a/public/cards/j_gluttenous_joker.png b/public/cards/j_gluttenous_joker.png new file mode 100644 index 0000000..4b998c0 Binary files /dev/null and b/public/cards/j_gluttenous_joker.png differ diff --git a/public/cards/j_golden.png b/public/cards/j_golden.png new file mode 100644 index 0000000..23bd88f Binary files /dev/null and b/public/cards/j_golden.png differ diff --git a/public/cards/j_greedy_joker.png b/public/cards/j_greedy_joker.png new file mode 100644 index 0000000..2e40d36 Binary files /dev/null and b/public/cards/j_greedy_joker.png differ diff --git a/public/cards/j_green_joker.png b/public/cards/j_green_joker.png new file mode 100644 index 0000000..cdad252 Binary files /dev/null and b/public/cards/j_green_joker.png differ diff --git a/public/cards/j_gros_michel.png b/public/cards/j_gros_michel.png new file mode 100644 index 0000000..dda4a35 Binary files /dev/null and b/public/cards/j_gros_michel.png differ diff --git a/public/cards/j_hack.png b/public/cards/j_hack.png new file mode 100644 index 0000000..cd2dabc Binary files /dev/null and b/public/cards/j_hack.png differ diff --git a/public/cards/j_half.png b/public/cards/j_half.png new file mode 100644 index 0000000..c5bd4e2 Binary files /dev/null and b/public/cards/j_half.png differ diff --git a/public/cards/j_hallucination.png b/public/cards/j_hallucination.png new file mode 100644 index 0000000..b4f01b6 Binary files /dev/null and b/public/cards/j_hallucination.png differ diff --git a/public/cards/j_hiker.png b/public/cards/j_hiker.png new file mode 100644 index 0000000..580a428 Binary files /dev/null and b/public/cards/j_hiker.png differ diff --git a/public/cards/j_hit_the_road.png b/public/cards/j_hit_the_road.png new file mode 100644 index 0000000..e200f83 Binary files /dev/null and b/public/cards/j_hit_the_road.png differ diff --git a/public/cards/j_hologram.png b/public/cards/j_hologram.png new file mode 100644 index 0000000..d895b76 Binary files /dev/null and b/public/cards/j_hologram.png differ diff --git a/public/cards/j_ice_cream.png b/public/cards/j_ice_cream.png new file mode 100644 index 0000000..cfd8f42 Binary files /dev/null and b/public/cards/j_ice_cream.png differ diff --git a/public/cards/j_idol.png b/public/cards/j_idol.png new file mode 100644 index 0000000..dde1ac2 Binary files /dev/null and b/public/cards/j_idol.png differ diff --git a/public/cards/j_invisible.png b/public/cards/j_invisible.png new file mode 100644 index 0000000..f70878e Binary files /dev/null and b/public/cards/j_invisible.png differ diff --git a/public/cards/j_joker.png b/public/cards/j_joker.png new file mode 100644 index 0000000..4ca9c05 Binary files /dev/null and b/public/cards/j_joker.png differ diff --git a/public/cards/j_jolly.png b/public/cards/j_jolly.png new file mode 100644 index 0000000..9b07c20 Binary files /dev/null and b/public/cards/j_jolly.png differ diff --git a/public/cards/j_juggler.png b/public/cards/j_juggler.png new file mode 100644 index 0000000..6312ddb Binary files /dev/null and b/public/cards/j_juggler.png differ diff --git a/public/cards/j_locked.png b/public/cards/j_locked.png new file mode 100644 index 0000000..fd00da8 Binary files /dev/null and b/public/cards/j_locked.png differ diff --git a/public/cards/j_loyalty_card.png b/public/cards/j_loyalty_card.png new file mode 100644 index 0000000..c0fbb40 Binary files /dev/null and b/public/cards/j_loyalty_card.png differ diff --git a/public/cards/j_lucky_cat.png b/public/cards/j_lucky_cat.png new file mode 100644 index 0000000..2fe7499 Binary files /dev/null and b/public/cards/j_lucky_cat.png differ diff --git a/public/cards/j_lusty_joker.png b/public/cards/j_lusty_joker.png new file mode 100644 index 0000000..8e764fe Binary files /dev/null and b/public/cards/j_lusty_joker.png differ diff --git a/public/cards/j_mad.png b/public/cards/j_mad.png new file mode 100644 index 0000000..e3a61ea Binary files /dev/null and b/public/cards/j_mad.png differ diff --git a/public/cards/j_madness.png b/public/cards/j_madness.png new file mode 100644 index 0000000..8c4ee2b Binary files /dev/null and b/public/cards/j_madness.png differ diff --git a/public/cards/j_mail.png b/public/cards/j_mail.png new file mode 100644 index 0000000..f9e4048 Binary files /dev/null and b/public/cards/j_mail.png differ diff --git a/public/cards/j_marble.png b/public/cards/j_marble.png new file mode 100644 index 0000000..d1cebdc Binary files /dev/null and b/public/cards/j_marble.png differ diff --git a/public/cards/j_merry_andy.png b/public/cards/j_merry_andy.png new file mode 100644 index 0000000..ae09be8 Binary files /dev/null and b/public/cards/j_merry_andy.png differ diff --git a/public/cards/j_midas_mask.png b/public/cards/j_midas_mask.png new file mode 100644 index 0000000..f036f73 Binary files /dev/null and b/public/cards/j_midas_mask.png differ diff --git a/public/cards/j_mime.png b/public/cards/j_mime.png new file mode 100644 index 0000000..e69a5d8 Binary files /dev/null and b/public/cards/j_mime.png differ diff --git a/public/cards/j_misprint.png b/public/cards/j_misprint.png new file mode 100644 index 0000000..46c7ed5 Binary files /dev/null and b/public/cards/j_misprint.png differ diff --git a/public/cards/j_mp_hanging_chad.png b/public/cards/j_mp_hanging_chad.png new file mode 100644 index 0000000..f3cf504 Binary files /dev/null and b/public/cards/j_mp_hanging_chad.png differ diff --git a/public/cards/j_mystic_summit.png b/public/cards/j_mystic_summit.png new file mode 100644 index 0000000..bb83817 Binary files /dev/null and b/public/cards/j_mystic_summit.png differ diff --git a/public/cards/j_obelisk.png b/public/cards/j_obelisk.png new file mode 100644 index 0000000..8f7b440 Binary files /dev/null and b/public/cards/j_obelisk.png differ diff --git a/public/cards/j_odd_todd.png b/public/cards/j_odd_todd.png new file mode 100644 index 0000000..5c65908 Binary files /dev/null and b/public/cards/j_odd_todd.png differ diff --git a/public/cards/j_onyx_agate.png b/public/cards/j_onyx_agate.png new file mode 100644 index 0000000..28f51e9 Binary files /dev/null and b/public/cards/j_onyx_agate.png differ diff --git a/public/cards/j_oops.png b/public/cards/j_oops.png new file mode 100644 index 0000000..f8dbecc Binary files /dev/null and b/public/cards/j_oops.png differ diff --git a/public/cards/j_order.png b/public/cards/j_order.png new file mode 100644 index 0000000..a9505ff Binary files /dev/null and b/public/cards/j_order.png differ diff --git a/public/cards/j_pareidolia.png b/public/cards/j_pareidolia.png new file mode 100644 index 0000000..a5beb27 Binary files /dev/null and b/public/cards/j_pareidolia.png differ diff --git a/public/cards/j_perkeo.png b/public/cards/j_perkeo.png new file mode 100644 index 0000000..9ce8d7f Binary files /dev/null and b/public/cards/j_perkeo.png differ diff --git a/public/cards/j_photograph.png b/public/cards/j_photograph.png new file mode 100644 index 0000000..1b1921c Binary files /dev/null and b/public/cards/j_photograph.png differ diff --git a/public/cards/j_popcorn.png b/public/cards/j_popcorn.png new file mode 100644 index 0000000..44cd05c Binary files /dev/null and b/public/cards/j_popcorn.png differ diff --git a/public/cards/j_raised_fist.png b/public/cards/j_raised_fist.png new file mode 100644 index 0000000..d056a21 Binary files /dev/null and b/public/cards/j_raised_fist.png differ diff --git a/public/cards/j_ramen.png b/public/cards/j_ramen.png new file mode 100644 index 0000000..1b58afd Binary files /dev/null and b/public/cards/j_ramen.png differ diff --git a/public/cards/j_red_card.png b/public/cards/j_red_card.png new file mode 100644 index 0000000..4a1b579 Binary files /dev/null and b/public/cards/j_red_card.png differ diff --git a/public/cards/j_reserved_parking.png b/public/cards/j_reserved_parking.png new file mode 100644 index 0000000..1b5ac3b Binary files /dev/null and b/public/cards/j_reserved_parking.png differ diff --git a/public/cards/j_ride_the_bus.png b/public/cards/j_ride_the_bus.png new file mode 100644 index 0000000..c09d985 Binary files /dev/null and b/public/cards/j_ride_the_bus.png differ diff --git a/public/cards/j_riff_raff.png b/public/cards/j_riff_raff.png new file mode 100644 index 0000000..781e831 Binary files /dev/null and b/public/cards/j_riff_raff.png differ diff --git a/public/cards/j_ring_master.png b/public/cards/j_ring_master.png new file mode 100644 index 0000000..0658bba Binary files /dev/null and b/public/cards/j_ring_master.png differ diff --git a/public/cards/j_rocket.png b/public/cards/j_rocket.png new file mode 100644 index 0000000..1d1ea55 Binary files /dev/null and b/public/cards/j_rocket.png differ diff --git a/public/cards/j_rough_gem.png b/public/cards/j_rough_gem.png new file mode 100644 index 0000000..2f6590e Binary files /dev/null and b/public/cards/j_rough_gem.png differ diff --git a/public/cards/j_runner.png b/public/cards/j_runner.png new file mode 100644 index 0000000..0d915f5 Binary files /dev/null and b/public/cards/j_runner.png differ diff --git a/public/cards/j_satellite.png b/public/cards/j_satellite.png new file mode 100644 index 0000000..97965f6 Binary files /dev/null and b/public/cards/j_satellite.png differ diff --git a/public/cards/j_scary_face.png b/public/cards/j_scary_face.png new file mode 100644 index 0000000..b7cbdc7 Binary files /dev/null and b/public/cards/j_scary_face.png differ diff --git a/public/cards/j_scholar.png b/public/cards/j_scholar.png new file mode 100644 index 0000000..d5cc0d6 Binary files /dev/null and b/public/cards/j_scholar.png differ diff --git a/public/cards/j_seance.png b/public/cards/j_seance.png new file mode 100644 index 0000000..c4587df Binary files /dev/null and b/public/cards/j_seance.png differ diff --git a/public/cards/j_seeing_double.png b/public/cards/j_seeing_double.png new file mode 100644 index 0000000..a4df5ad Binary files /dev/null and b/public/cards/j_seeing_double.png differ diff --git a/public/cards/j_selzer.png b/public/cards/j_selzer.png new file mode 100644 index 0000000..982a739 Binary files /dev/null and b/public/cards/j_selzer.png differ diff --git a/public/cards/j_shoot_the_moon.png b/public/cards/j_shoot_the_moon.png new file mode 100644 index 0000000..0aaf201 Binary files /dev/null and b/public/cards/j_shoot_the_moon.png differ diff --git a/public/cards/j_shortcut.png b/public/cards/j_shortcut.png new file mode 100644 index 0000000..21991c1 Binary files /dev/null and b/public/cards/j_shortcut.png differ diff --git a/public/cards/j_sixth_sense.png b/public/cards/j_sixth_sense.png new file mode 100644 index 0000000..e9d10fb Binary files /dev/null and b/public/cards/j_sixth_sense.png differ diff --git a/public/cards/j_sly.png b/public/cards/j_sly.png new file mode 100644 index 0000000..abfaccd Binary files /dev/null and b/public/cards/j_sly.png differ diff --git a/public/cards/j_smeared.png b/public/cards/j_smeared.png new file mode 100644 index 0000000..af7e423 Binary files /dev/null and b/public/cards/j_smeared.png differ diff --git a/public/cards/j_smiley.png b/public/cards/j_smiley.png new file mode 100644 index 0000000..1328ca3 Binary files /dev/null and b/public/cards/j_smiley.png differ diff --git a/public/cards/j_sock_and_buskin.png b/public/cards/j_sock_and_buskin.png new file mode 100644 index 0000000..090096f Binary files /dev/null and b/public/cards/j_sock_and_buskin.png differ diff --git a/public/cards/j_space.png b/public/cards/j_space.png new file mode 100644 index 0000000..ea01823 Binary files /dev/null and b/public/cards/j_space.png differ diff --git a/public/cards/j_splash.png b/public/cards/j_splash.png new file mode 100644 index 0000000..61cfada Binary files /dev/null and b/public/cards/j_splash.png differ diff --git a/public/cards/j_square.png b/public/cards/j_square.png new file mode 100644 index 0000000..2069041 Binary files /dev/null and b/public/cards/j_square.png differ diff --git a/public/cards/j_steel_joker.png b/public/cards/j_steel_joker.png new file mode 100644 index 0000000..a8fe6bb Binary files /dev/null and b/public/cards/j_steel_joker.png differ diff --git a/public/cards/j_stencil.png b/public/cards/j_stencil.png new file mode 100644 index 0000000..955c78e Binary files /dev/null and b/public/cards/j_stencil.png differ diff --git a/public/cards/j_stone.png b/public/cards/j_stone.png new file mode 100644 index 0000000..a8ab3c9 Binary files /dev/null and b/public/cards/j_stone.png differ diff --git a/public/cards/j_stuntman.png b/public/cards/j_stuntman.png new file mode 100644 index 0000000..1f59bcd Binary files /dev/null and b/public/cards/j_stuntman.png differ diff --git a/public/cards/j_supernova.png b/public/cards/j_supernova.png new file mode 100644 index 0000000..a29f241 Binary files /dev/null and b/public/cards/j_supernova.png differ diff --git a/public/cards/j_superposition.png b/public/cards/j_superposition.png new file mode 100644 index 0000000..ebf7172 Binary files /dev/null and b/public/cards/j_superposition.png differ diff --git a/public/cards/j_swashbuckler.png b/public/cards/j_swashbuckler.png new file mode 100644 index 0000000..4ee819a Binary files /dev/null and b/public/cards/j_swashbuckler.png differ diff --git a/public/cards/j_throwback.png b/public/cards/j_throwback.png new file mode 100644 index 0000000..eef30c0 Binary files /dev/null and b/public/cards/j_throwback.png differ diff --git a/public/cards/j_ticket.png b/public/cards/j_ticket.png new file mode 100644 index 0000000..d7ab42f Binary files /dev/null and b/public/cards/j_ticket.png differ diff --git a/public/cards/j_to_the_moon.png b/public/cards/j_to_the_moon.png new file mode 100644 index 0000000..90c2f48 Binary files /dev/null and b/public/cards/j_to_the_moon.png differ diff --git a/public/cards/j_todo_list.png b/public/cards/j_todo_list.png new file mode 100644 index 0000000..28d3f0c Binary files /dev/null and b/public/cards/j_todo_list.png differ diff --git a/public/cards/j_trading.png b/public/cards/j_trading.png new file mode 100644 index 0000000..f912941 Binary files /dev/null and b/public/cards/j_trading.png differ diff --git a/public/cards/j_tribe.png b/public/cards/j_tribe.png new file mode 100644 index 0000000..2a4c935 Binary files /dev/null and b/public/cards/j_tribe.png differ diff --git a/public/cards/j_triboulet.png b/public/cards/j_triboulet.png new file mode 100644 index 0000000..2a11444 Binary files /dev/null and b/public/cards/j_triboulet.png differ diff --git a/public/cards/j_trio.png b/public/cards/j_trio.png new file mode 100644 index 0000000..9ddbd68 Binary files /dev/null and b/public/cards/j_trio.png differ diff --git a/public/cards/j_troubadour.png b/public/cards/j_troubadour.png new file mode 100644 index 0000000..b3166fe Binary files /dev/null and b/public/cards/j_troubadour.png differ diff --git a/public/cards/j_trousers.png b/public/cards/j_trousers.png new file mode 100644 index 0000000..5768aa8 Binary files /dev/null and b/public/cards/j_trousers.png differ diff --git a/public/cards/j_turtle_bean.png b/public/cards/j_turtle_bean.png new file mode 100644 index 0000000..98aa076 Binary files /dev/null and b/public/cards/j_turtle_bean.png differ diff --git a/public/cards/j_undiscovered.png b/public/cards/j_undiscovered.png new file mode 100644 index 0000000..de40bc0 Binary files /dev/null and b/public/cards/j_undiscovered.png differ diff --git a/public/cards/j_vagabond.png b/public/cards/j_vagabond.png new file mode 100644 index 0000000..628dbb4 Binary files /dev/null and b/public/cards/j_vagabond.png differ diff --git a/public/cards/j_vampire.png b/public/cards/j_vampire.png new file mode 100644 index 0000000..a687f2a Binary files /dev/null and b/public/cards/j_vampire.png differ diff --git a/public/cards/j_walkie_talkie.png b/public/cards/j_walkie_talkie.png new file mode 100644 index 0000000..b47e196 Binary files /dev/null and b/public/cards/j_walkie_talkie.png differ diff --git a/public/cards/j_wee.png b/public/cards/j_wee.png new file mode 100644 index 0000000..4ca9c05 Binary files /dev/null and b/public/cards/j_wee.png differ diff --git a/public/cards/j_wily.png b/public/cards/j_wily.png new file mode 100644 index 0000000..e4e8ef4 Binary files /dev/null and b/public/cards/j_wily.png differ diff --git a/public/cards/j_wrathful_joker.png b/public/cards/j_wrathful_joker.png new file mode 100644 index 0000000..320efd3 Binary files /dev/null and b/public/cards/j_wrathful_joker.png differ diff --git a/public/cards/j_yorick.png b/public/cards/j_yorick.png new file mode 100644 index 0000000..93b134b Binary files /dev/null and b/public/cards/j_yorick.png differ diff --git a/public/cards/j_zany.png b/public/cards/j_zany.png new file mode 100644 index 0000000..2ecaf5f Binary files /dev/null and b/public/cards/j_zany.png differ diff --git a/src/app/(home)/log-parser/page.tsx b/src/app/(home)/log-parser/page.tsx index bb160cc..6814d2a 100644 --- a/src/app/(home)/log-parser/page.tsx +++ b/src/app/(home)/log-parser/page.tsx @@ -1,5 +1,6 @@ 'use client' +import { OptimizedImage } from '@/components/optimized-image' import { Card, CardContent, @@ -26,6 +27,13 @@ import { TableRow, } from '@/components/ui/table' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip' +import { jokers } from '@/shared/jokers' import { useFormatter } from 'next-intl' import { useState } from 'react' @@ -50,27 +58,30 @@ type GameOptions = { stake?: number | null } -// Refined Game type to hold structured data type Game = { id: number // Simple identifier for keys host: string | null guest: string | null + logOwnerName: string | null // Name of the player whose log this is for this game + opponentName: string | null // Name of the opponent relative to the log owner hostMods: string[] guestMods: string[] - isHost: boolean | null - opponentName: string | null + isHost: boolean | null // Log owner's role in lobby creation deck: string | null seed: string | null options: GameOptions | null - moneyGained: number - moneySpent: number - opponentMoneySpent: number + moneyGained: number // Log owner's gains + moneySpent: number // Log owner's spending + opponentMoneySpent: number // Opponent's reported spending (from got message) startDate: Date endDate: Date | null durationSeconds: number | null - lastLives: number - moneySpentPerShop: (number | null)[] - moneySpentPerShopOpponent: (number | null)[] + opponentLastLives: number // Opponent's last known lives (from enemyInfo) + opponentLastSkips: number // Opponent's last known skip count (from enemyInfo) + moneySpentPerShop: (number | null)[] // Log owner's spending/skips per shop + moneySpentPerShopOpponent: (number | null)[] // Opponent's spending/skips per shop + logOwnerFinalJokers: string[] // Log owner's final jokers + opponentFinalJokers: string[] // Opponent's final jokers events: LogEvent[] } @@ -79,10 +90,11 @@ const initGame = (id: number, startDate: Date): Game => ({ id, host: null, guest: null, + logOwnerName: null, // Initialize + opponentName: null, // Initialize hostMods: [], guestMods: [], isHost: null, - opponentName: null, deck: null, seed: null, options: null, @@ -92,9 +104,12 @@ const initGame = (id: number, startDate: Date): Game => ({ startDate, endDate: null, durationSeconds: null, - lastLives: 4, // Default starting lives, might be overridden by options + opponentLastLives: 4, + opponentLastSkips: 0, moneySpentPerShop: [], moneySpentPerShopOpponent: [], + logOwnerFinalJokers: [], + opponentFinalJokers: [], events: [], }) @@ -122,7 +137,7 @@ function boolStrToText(str: string | boolean | undefined | null): string { const lower = str.toLowerCase() if (lower === 'true') return 'Yes' if (lower === 'false') return 'No' - return str // Return original if not true/false + return str } // Main component @@ -135,7 +150,7 @@ export default function LogParser() { const parseLogFile = async (file: File) => { setIsLoading(true) setError(null) - setParsedGames([]) // Clear previous results + setParsedGames([]) try { const content = await file.text() @@ -146,21 +161,57 @@ export default function LogParser() { let lastSeenLobbyOptions: GameOptions | null = null let gameCounter = 0 - // Pre-process to find lobby info associated with game starts - // This is simplified; a more robust approach might be needed for complex logs const gameStartInfos = extractGameStartInfo(logLines) let gameInfoIndex = 0 - + let lastProcessedTimestamp: Date | null = null for (const line of logLines) { + if (!line.trim()) continue const timeMatch = line.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) - const timestamp = timeMatch?.[1] ? new Date(timeMatch[1]) : new Date() // Use current time as fallback + const timestamp = timeMatch?.[1] ? new Date(timeMatch[1]) : new Date() const lineLower = line.toLowerCase() - + lastProcessedTimestamp = timestamp // --- Game Lifecycle --- - if (lineLower.includes('startgame message')) { - // Finalize previous game if it exists + if (line.includes('Client got receiveEndGameJokers message')) { if (currentGame) { - if (!currentGame.endDate) currentGame.endDate = timestamp // Use current line time if no end signal seen + // Mark end date if not already set + if (!currentGame.endDate) { + currentGame.endDate = timestamp + } + // Extract Opponent Jokers + const keysMatch = line.match(/\(keys: ([^)]+)\)/) + if (keysMatch?.[1]) { + currentGame.opponentFinalJokers = keysMatch[1] + .split(';') + .filter(Boolean) // Remove empty strings if any + } + // Extract Seed (often found here) + const seedMatch = line.match(/seed: ([A-Z0-9]+)/) + if (!currentGame.seed && seedMatch?.[1]) { + currentGame.seed = seedMatch[1] + } + } + continue + } + if (line.includes('Client sent message: action:receiveEndGameJokers')) { + if (currentGame) { + // Mark end date if not already set (might happen slightly before 'got') + if (!currentGame.endDate) { + currentGame.endDate = timestamp + } + // Extract Log Owner Jokers + const keysMatch = line.match(/keys:(.+)$/) // Match from keys: to end of line + if (keysMatch?.[1]) { + currentGame.logOwnerFinalJokers = keysMatch[1] + .split(';') + + .filter(Boolean) // Remove empty strings + } + } + continue + } + if (lineLower.includes('startgame message')) { + if (currentGame) { + if (!currentGame.endDate) currentGame.endDate = timestamp currentGame.durationSeconds = currentGame.endDate ? (currentGame.endDate.getTime() - currentGame.startDate.getTime()) / @@ -169,28 +220,45 @@ export default function LogParser() { games.push(currentGame) } - // Start new game gameCounter++ currentGame = initGame(gameCounter, timestamp) - const currentInfo = gameStartInfos[gameInfoIndex++] ?? {} + const currentInfo = + gameStartInfos[gameInfoIndex++] ?? ({} as GameStartInfo) - // Apply pre-parsed lobby info and options + // Assign host/guest first currentGame.host = currentInfo.lobbyInfo?.host ?? null currentGame.guest = currentInfo.lobbyInfo?.guest ?? null currentGame.hostMods = currentInfo.lobbyInfo?.hostHash ?? [] currentGame.guestMods = currentInfo.lobbyInfo?.guestHash ?? [] - currentGame.isHost = currentInfo.lobbyInfo?.isHost ?? null - currentGame.opponentName = currentGame.isHost - ? currentGame.guest - : currentGame.host - currentGame.options = lastSeenLobbyOptions // Apply last seen options - currentGame.deck = lastSeenLobbyOptions?.back ?? null - currentGame.seed = currentInfo.seed ?? null // Use seed found near startGame - if (currentGame.options?.starting_lives) { - currentGame.lastLives = currentGame.options.starting_lives + currentGame.isHost = currentInfo.lobbyInfo?.isHost ?? null // Log owner's role + + // *** Determine Log Owner and Opponent Names based on isHost *** + if (currentGame.isHost !== null) { + if (currentGame.isHost) { + // Log owner was the host + currentGame.logOwnerName = currentGame.host + currentGame.opponentName = currentGame.guest + } else { + // Log owner was the guest + currentGame.logOwnerName = currentGame.guest + currentGame.opponentName = currentGame.host + } + } + // Fallback if names are missing but role is known + if (!currentGame.logOwnerName && currentGame.isHost !== null) { + currentGame.logOwnerName = currentGame.isHost ? 'Host' : 'Guest' + } + if (!currentGame.opponentName && currentGame.isHost !== null) { + currentGame.opponentName = currentGame.isHost ? 'Guest' : 'Host' + } + + currentGame.options = lastSeenLobbyOptions + currentGame.deck = lastSeenLobbyOptions?.back ?? null + currentGame.seed = currentInfo.seed ?? null + if (currentGame.options?.starting_lives) { + currentGame.opponentLastLives = currentGame.options.starting_lives } - // Add system event for game start currentGame.events.push({ timestamp, text: `Game ${gameCounter} Started`, @@ -201,6 +269,12 @@ export default function LogParser() { text: `Host: ${currentGame.host || 'Unknown'}, Guest: ${currentGame.guest || 'Unknown'}`, type: 'info', }) + // Add event indicating log owner's role + currentGame.events.push({ + timestamp, + text: `Log Owner Role: ${currentGame.isHost === null ? 'Unknown' : currentGame.isHost ? 'Host' : 'Guest'} (${currentGame.logOwnerName || 'Unknown'})`, + type: 'info', + }) currentGame.events.push({ timestamp, text: `Deck: ${currentGame.deck || 'Unknown'}`, @@ -211,14 +285,12 @@ export default function LogParser() { text: `Seed: ${currentGame.seed || 'Unknown'}`, type: 'info', }) - // Add more info events for options if needed - continue // Move to next line + continue } if (line.includes('Client got receiveEndGameJokers')) { if (currentGame && !currentGame.endDate) { currentGame.endDate = timestamp - // Sometimes seed is only available here const seedMatch = line.match(/seed: ([A-Z0-9]+)/) if (!currentGame.seed && seedMatch?.[1]) { currentGame.seed = seedMatch[1] @@ -232,12 +304,12 @@ export default function LogParser() { const optionsStr = line.split(' Client sent message:')[1]?.trim() if (optionsStr) { lastSeenLobbyOptions = parseLobbyOptions(optionsStr) - // If a game is active, update its options (might happen mid-game?) if (currentGame && !currentGame.options) { currentGame.options = lastSeenLobbyOptions currentGame.deck = lastSeenLobbyOptions.back ?? currentGame.deck if (lastSeenLobbyOptions.starting_lives) { - currentGame.lastLives = lastSeenLobbyOptions.starting_lives + currentGame.opponentLastLives = + lastSeenLobbyOptions.starting_lives } } } @@ -245,20 +317,48 @@ export default function LogParser() { } // --- In-Game Event Parsing (requires currentGame) --- - if (!currentGame) continue // Skip lines if no game is active + if (!currentGame) continue + // enemyInfo ALWAYS refers to the opponent from the log owner's perspective if (lineLower.includes('enemyinfo')) { - const match = line.match(/lives:(\d+)/) - if (match?.[1]) { - const newLives = Number.parseInt(match[1], 10) - if (!isNaN(newLives) && newLives < currentGame.lastLives) { + // Parse opponent lives + const livesMatch = line.match(/lives:(\d+)/) + if (livesMatch?.[1]) { + const newLives = Number.parseInt(livesMatch[1], 10) + if ( + !Number.isNaN(newLives) && + newLives < currentGame.opponentLastLives + ) { currentGame.events.push({ timestamp, - text: `Opponent lost a life (${currentGame.lastLives} -> ${newLives})`, + text: `Opponent lost a life (${currentGame.opponentLastLives} -> ${newLives})`, type: 'event', }) } - currentGame.lastLives = newLives + currentGame.opponentLastLives = newLives + } + + // Parse opponent skips + const skipsMatch = line.match(/skips: *(\d+)/) + if (skipsMatch?.[1]) { + const newSkips = Number.parseInt(skipsMatch[1], 10) + if ( + !Number.isNaN(newSkips) && + newSkips > currentGame.opponentLastSkips + ) { + const numSkipsOccurred = newSkips - currentGame.opponentLastSkips + for (let i = 0; i < numSkipsOccurred; i++) { + currentGame.moneySpentPerShopOpponent.push(null) + } + currentGame.events.push({ + timestamp, + text: `Opponent skipped ${numSkipsOccurred} shop${numSkipsOccurred > 1 ? 's' : ''} (Total: ${newSkips})`, + type: 'shop', + }) + currentGame.opponentLastSkips = newSkips + } else if (!Number.isNaN(newSkips)) { + currentGame.opponentLastSkips = newSkips + } } continue } @@ -274,12 +374,12 @@ export default function LogParser() { } continue } - + // This message indicates opponent's spending report if (line.includes(' Client got spentLastShop message')) { const match = line.match(/amount: (\d+)/) if (match?.[1]) { const amount = Number.parseInt(match[1], 10) - if (!isNaN(amount)) { + if (!Number.isNaN(amount)) { currentGame.opponentMoneySpent += amount currentGame.moneySpentPerShopOpponent.push(amount) currentGame.events.push({ @@ -292,13 +392,13 @@ export default function LogParser() { continue } + // This message indicates the log owner reporting their spending if (line.includes('Client sent message: action:spentLastShop')) { const match = line.match(/amount:(\d+)/) if (match?.[1]) { const amount = Number.parseInt(match[1], 10) - if (!isNaN(amount)) { + if (!Number.isNaN(amount)) { currentGame.moneySpentPerShop.push(amount) - // Note: Total money spent is tracked via moneymoved/reroll/buy currentGame.events.push({ timestamp, text: `Reported spending $${amount} last shop`, @@ -309,8 +409,9 @@ export default function LogParser() { continue } + // This message indicates the log owner skipped if (line.includes('Client sent message: action:skip')) { - currentGame.moneySpentPerShop.push(null) // Mark shop as skipped + currentGame.moneySpentPerShop.push(null) currentGame.events.push({ timestamp, text: 'Skipped shop', @@ -319,13 +420,14 @@ export default function LogParser() { continue } - // --- Player Actions/Events --- + // --- Log Owner Actions/Events (Client sent ...) --- if (lineLower.includes('client sent')) { + // Log owner gained/spent money directly if (lineLower.includes('moneymoved')) { const match = line.match(/amount: *(-?\d+)/) if (match?.[1]) { const amount = Number.parseInt(match[1], 10) - if (!isNaN(amount)) { + if (!Number.isNaN(amount)) { if (amount >= 0) { currentGame.moneyGained += amount currentGame.events.push({ @@ -335,7 +437,7 @@ export default function LogParser() { }) } else { const spent = Math.abs(amount) - currentGame.moneySpent += spent // Track spending here + currentGame.moneySpent += spent currentGame.events.push({ timestamp, text: `Spent $${spent}`, @@ -345,23 +447,25 @@ export default function LogParser() { } } } else if (line.includes('boughtCardFromShop')) { + // Log owner bought card const cardMatch = line.match(/card:([^,\n]+)/i) - const costMatch = line.match(/cost: *(\d+)/i) // Assuming cost is logged + const costMatch = line.match(/cost: *(\d+)/i) const cardRaw = cardMatch?.[1]?.trim() ?? 'Unknown Card' const cardClean = cardRaw.replace(/^(c_mp_|j_mp_)/, '') const cost = costMatch?.[1] ? Number.parseInt(costMatch[1], 10) : 0 - if (cost > 0) currentGame.moneySpent += cost // Add purchase cost + if (cost > 0) currentGame.moneySpent += cost currentGame.events.push({ timestamp, text: `Bought ${cardClean}${cost > 0 ? ` for $${cost}` : ''}`, type: 'shop', }) } else if (line.includes('rerollShop')) { + // Log owner rerolled const costMatch = line.match(/cost: *(\d+)/i) if (costMatch?.[1]) { const cost = Number.parseInt(costMatch[1], 10) - if (!isNaN(cost)) { - currentGame.moneySpent += cost // Add reroll cost + if (!Number.isNaN(cost)) { + currentGame.moneySpent += cost currentGame.events.push({ timestamp, text: `Rerolled shop for $${cost}`, @@ -370,6 +474,7 @@ export default function LogParser() { } } } else if (lineLower.includes('usedcard')) { + // Log owner used card const match = line.match(/card:([^,\n]+)/i) if (match?.[1]) { const raw = match[1].trim() @@ -388,6 +493,7 @@ export default function LogParser() { }) } } else if (lineLower.includes('setlocation')) { + // Log owner changed location const locMatch = line.match(/location:([a-zA-Z0-9_-]+)/) if (locMatch?.[1]) { const locCode = locMatch[1] @@ -403,17 +509,14 @@ export default function LogParser() { } } // End of line processing loop - // Add the last game if it exists if (currentGame) { if (!currentGame.endDate) { - // Find the timestamp of the last event or the last line processed const lastEventTime = currentGame.events.length > 0 - ? currentGame.events[currentGame.events.length - 1].timestamp + ? currentGame.events[currentGame.events.length - 1]?.timestamp : null - const lastLineTime = timeMatch?.[1] ? new Date(timeMatch[1]) : null currentGame.endDate = - lastEventTime ?? lastLineTime ?? currentGame.startDate // Fallback chain + lastEventTime ?? lastProcessedTimestamp ?? currentGame.startDate // Fallback chain } currentGame.durationSeconds = currentGame.endDate ? (currentGame.endDate.getTime() - currentGame.startDate.getTime()) / @@ -438,222 +541,349 @@ export default function LogParser() { } } + // Generate a default tab value using determined names or fallbacks const defaultTabValue = parsedGames.length > 0 - ? `game-${parsedGames[0].id}-${parsedGames[0].opponentName || 'Unknown'}` + ? `game-${parsedGames![0]!.id}-${parsedGames![0]!.logOwnerName || 'LogOwner'}-vs-${parsedGames![0]!.opponentName || 'Opponent'}` : '' return ( -
- { - const file = files[0] - if (file instanceof File) { - parseLogFile(file) - } - }} - disabled={isLoading} + +
- - - - - - Drop log file here or click - - Upload your Balatro log.txt file. - + { + const file = files[0] + if (file instanceof File) { + parseLogFile(file) + } + }} + disabled={isLoading} + > + + + + + + Drop log file here or click + + Upload your Balatro log.txt file. + + - - - + + - {isLoading &&

Loading and parsing log...

} - {error &&

{error}

} + {isLoading &&

Loading and parsing log...

} + {error &&

{error}

} - {parsedGames.length > 0 && ( - - - {parsedGames.map((game) => ( - - Game {game.id}: {game.opponentName || 'Unknown'} - - ))} - + {parsedGames.length > 0 && ( + + + {parsedGames.map((game) => { + // Determine labels for the tab trigger, handling potential name conflicts + const useGenericLabels = + game.logOwnerName && + game.opponentName && + game.logOwnerName === game.opponentName + const opponentLabel = useGenericLabels + ? 'Opponent' + : game.opponentName || 'P2' - {parsedGames.map((game) => ( - - - - - Game {game.id} vs {game.opponentName || 'Unknown'} - - - Started:{' '} - {formatter.dateTime(game.startDate, { - dateStyle: 'short', - timeStyle: 'short', - })}{' '} - | Ended:{' '} - {game.endDate - ? formatter.dateTime(game.endDate, { + return ( + + Game {game.id} vs {opponentLabel} + + ) + })} + + + {parsedGames.map((game) => { + // Determine labels for the content, handling potential name conflicts + const useGenericLabels = + game.logOwnerName && + game.opponentName && + game.logOwnerName === game.opponentName + const ownerLabel = useGenericLabels + ? 'Log Owner' + : game.logOwnerName || 'Log Owner' // Fallback for display + const opponentLabel = useGenericLabels + ? 'Opponent' + : game.opponentName || 'Opponent' // Fallback for display + + return ( + + + + + Game {game.id}: {ownerLabel} vs {opponentLabel} + + + Started:{' '} + {formatter.dateTime(game.startDate, { dateStyle: 'short', timeStyle: 'short', - }) - : 'N/A'}{' '} - | Duration: {formatDuration(game.durationSeconds)} - - - - {/* Column 1: Game Info & Events */} -
- - - Game Details - - -

- You Were:{' '} - {game.isHost ? 'Host' : 'Guest'} ( - {game.isHost ? game.host : game.guest}) -

-

- Deck: {game.deck || 'Unknown'} -

-

- Seed: {game.seed || 'Unknown'} -

-

- Ruleset:{' '} - {game.options?.ruleset || 'Default'} -

-

- Stake:{' '} - {game.options?.stake ?? 'Unknown'} -

- {/* Add more options as needed */} -

- Different Decks:{' '} - {boolStrToText(game.options?.different_decks)} -

-

- Different Seeds:{' '} - {boolStrToText(game.options?.different_seeds)} -

-

- Death on Round Loss:{' '} - {boolStrToText(game.options?.death_on_round_loss)} -

-

- Gold on Life Loss:{' '} - {boolStrToText(game.options?.gold_on_life_loss)} -

-

- No Gold on Round Loss:{' '} - {boolStrToText(game.options?.no_gold_on_round_loss)} -

-
-
- - - Events - - - -
- {game.events.map((event, index) => ( -
- - {formatter.dateTime(event.timestamp, { - timeStyle: 'medium', - })} - - {event.text} + })}{' '} + | Ended:{' '} + {game.endDate + ? formatter.dateTime(game.endDate, { + dateStyle: 'short', + timeStyle: 'short', + }) + : 'N/A'}{' '} + | Duration: {formatDuration(game.durationSeconds)} + + + + {/* Column 1: Game Info & Events */} +
+ + + + Game Details + + + + {/* Show Log Owner's Role */} +

+ Log Owner's Role:{' '} + {game.isHost === null + ? 'Unknown' + : game.isHost + ? 'Host' + : 'Guest'}{' '} + ({ownerLabel}) +

+

+ Deck: {game.deck || 'Unknown'} +

+

+ Seed: {game.seed || 'Unknown'} +

+

+ Ruleset:{' '} + {game.options?.ruleset || 'Default'} +

+

+ Stake:{' '} + {game.options?.stake ?? 'Unknown'} +

+

+ Different Decks:{' '} + {boolStrToText(game.options?.different_decks)} +

+

+ Different Seeds:{' '} + {boolStrToText(game.options?.different_seeds)} +

+

+ Death on Round Loss:{' '} + {boolStrToText(game.options?.death_on_round_loss)} +

+

+ Gold on Life Loss:{' '} + {boolStrToText(game.options?.gold_on_life_loss)} +

+

+ No Gold on Round Loss:{' '} + {boolStrToText( + game.options?.no_gold_on_round_loss + )} +

+
+
+ + + Events + + + +
+ {game.events.map((event, index) => ( +
+ + {formatter.dateTime(event.timestamp, { + timeStyle: 'medium', + })} + + {event.text} +
+ ))}
- ))} -
- -
- -
+ + + +
- {/* Column 2: Money & Mods */} -
- - - Shop Spending - - - - - - - - Mods - - -
- Host ({game.host || 'Unknown'}) Mods: - {game.hostMods.length > 0 ? ( -
    - {game.hostMods.map((mod, i) => ( -
  • {mod}
  • - ))} -
- ) : ( -

- None detected -

- )} -
-
- - Guest ({game.guest || 'Unknown'}) Mods: - - {game.guestMods.length > 0 ? ( -
    - {game.guestMods.map((mod, i) => ( -
  • {mod}
  • - ))} -
- ) : ( -

- None detected -

- )} -
-
-
-
-
-
- - ))} - - )} -
+ {/* Column 2: Money & Mods */} +
+ + + + Shop Spending + + + + {/* Pass game and determined labels to the table */} + + + + + + + Final Jokers + + + +
+ {ownerLabel}: + {game.logOwnerFinalJokers.length > 0 ? ( +
    + {game.logOwnerFinalJokers.map((joker, i) => { + const cleanName = + jokers[joker]?.name ?? + cleanJokerKey(joker) + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: Simple list +
  • +
    + + {cleanName} +
    +
  • + ) + })} +
+ ) : ( +

+ No data found. +

+ )} +
+
+ {opponentLabel}: + {game.opponentFinalJokers.length > 0 ? ( +
    + {game.opponentFinalJokers.map((joker, i) => { + const cleanName = + jokers[joker]?.name ?? + cleanJokerKey(joker) + return ( + // biome-ignore lint/suspicious/noArrayIndexKey: Simple list +
  • +
    + + {cleanName} +
    +
  • + ) + })} +
+ ) : ( +

+ No data found. +

+ )} +
+
+
+ + + Mods + + +
+ + Host Mods ({game.host || 'Unknown'}): + + {game.hostMods.length > 0 ? ( +
    + {game.hostMods.map((mod, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: Simple list +
  • {mod}
  • + ))} +
+ ) : ( +

+ None detected +

+ )} +
+
+ + Guest Mods ({game.guest || 'Unknown'}): + + {game.guestMods.length > 0 ? ( +
    + {game.guestMods.map((mod, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: Simple list +
  • {mod}
  • + ))} +
+ ) : ( +

+ None detected +

+ )} +
+
+
+
+
+
+
+ ) + })} +
+ )} +
+
) } // --- Helper Functions --- -// Simple component for the shop spending table -function ShopSpendingTable({ game }: { game: Game }) { +function ShopSpendingTable({ + game, + ownerLabel, + opponentLabel, +}: { + game: Game + ownerLabel: string + opponentLabel: string +}) { const maxShops = Math.max( game.moneySpentPerShop.length, game.moneySpentPerShopOpponent.length @@ -676,8 +906,10 @@ function ShopSpendingTable({ game }: { game: Game }) { Shop - You - Opponent + {ownerLabel} + + {opponentLabel} + @@ -685,6 +917,7 @@ function ShopSpendingTable({ game }: { game: Game }) { // biome-ignore lint/suspicious/noArrayIndexKey: Simple table rendering {j + 1} + {/* Log Owner Data */} {game.moneySpentPerShop[j] === null ? 'Skipped' @@ -692,6 +925,7 @@ function ShopSpendingTable({ game }: { game: Game }) { ? `$${game.moneySpentPerShop[j]}` : '-'} + {/* Opponent Data */} {game.moneySpentPerShopOpponent[j] === null ? 'Skipped' @@ -719,10 +953,26 @@ function ShopSpendingTable({ game }: { game: Game }) { Total Actual - ${game.moneySpent} + + + ${game.moneySpent} + + + Sum of money {ownerLabel} spent via buy/reroll actions detected + in this log. + + - ${game.opponentMoneySpent} + + + ${game.opponentMoneySpent} + + + Sum of money {opponentLabel} reported spending via network + messages received by {ownerLabel}. + + @@ -730,7 +980,7 @@ function ShopSpendingTable({ game }: { game: Game }) { ) } -// Helper to parse lobby options string +// Helper to parse lobby options string (no changes needed) function parseLobbyOptions(optionsStr: string): GameOptions { const options: GameOptions = {} const params = optionsStr.split(',') @@ -760,7 +1010,7 @@ function parseLobbyOptions(optionsStr: string): GameOptions { case 'starting_lives': case 'stake': const numValue = Number.parseInt(trimmedValue, 10) - if (!isNaN(numValue)) { + if (!Number.isNaN(numValue)) { options[trimmedKey] = numValue } break @@ -769,7 +1019,7 @@ function parseLobbyOptions(optionsStr: string): GameOptions { return options } -// Helper to format location codes +// Helper to format location codes (no changes needed) function formatLocation(locCode: string): string { if (locCode === 'loc_shop') { return 'Shop' @@ -804,7 +1054,6 @@ function formatLocation(locCode: string): string { ) } -// Helper to get color class based on event type function getEventColor(type: LogEvent['type']): string { switch (type) { case 'event': @@ -826,9 +1075,6 @@ function getEventColor(type: LogEvent['type']): string { } } -// --- Log Pre-processing Helpers --- - -// Simplified structure for lobby info parsing result type ParsedLobbyInfo = { timestamp: Date host: string | null @@ -836,58 +1082,42 @@ type ParsedLobbyInfo = { hostHash: string[] guestHash: string[] isHost: boolean | null - // Add other fields if needed from parseLobbyInfo } -// Structure to hold info related to a game start type GameStartInfo = { lobbyInfo: ParsedLobbyInfo | null seed: string | null } -// Function to extract lobby info and seeds associated with game starts function extractGameStartInfo(lines: string[]): GameStartInfo[] { const gameInfos: GameStartInfo[] = [] let latestLobbyInfo: ParsedLobbyInfo | null = null - let nextGameSeed: string | null = null // Seed often appears *after* start + let nextGameSeed: string | null = null for (let i = 0; i < lines.length; i++) { const line = lines[i] + if (!line) { + continue + } const lineLower = line.toLowerCase() - // Capture the latest lobby info seen if (line.includes('Client got lobbyInfo message')) { try { - latestLobbyInfo = parseLobbyInfoLine(line) // Use a dedicated parser + latestLobbyInfo = parseLobbyInfoLine(line) } catch (e) { console.warn('Could not parse lobbyInfo line:', line, e) - latestLobbyInfo = null // Reset if parsing fails + latestLobbyInfo = null } } - // Capture seed from endgame message (often relates to the *next* game's seed if custom) - // Or seed from the startGame message itself - if (line.includes('Client got receiveEndGameJokers message')) { - const seedMatch = line.match(/seed: ([A-Z0-9]+)/) - if (seedMatch?.[1]) { - // This seed might belong to the game that just ended, - // or potentially the *next* one if using custom seeds? - // Let's tentatively store it for the *next* game start. - // A more robust logic might be needed depending on exact log behavior. - // nextGameSeed = seedMatch[1]; // Let's disable this for now, seed on start is more reliable - } - } - - // When a game starts, associate the latest lobby info and potentially the seed if (lineLower.includes('startgame message')) { const seedMatch = line.match(/seed:\s*([^) ]+)/) const startGameSeed = seedMatch?.[1] || null gameInfos.push({ lobbyInfo: latestLobbyInfo, - seed: startGameSeed ?? nextGameSeed, // Prefer seed from start message + seed: startGameSeed ?? nextGameSeed, }) - // Reset for the next game latestLobbyInfo = null nextGameSeed = null } @@ -895,19 +1125,16 @@ function extractGameStartInfo(lines: string[]): GameStartInfo[] { return gameInfos } -// Parses a single lobbyInfo log line (adapt your original parseLobbyInfo) function parseLobbyInfoLine(line: string): ParsedLobbyInfo | null { - // Basic parsing, adjust regex/logic based on your exact log format const timeMatch = line.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) const timestamp = timeMatch?.[1] ? new Date(timeMatch[1]) : new Date() const hostMatch = line.match(/host: ([^ )]+)/) const guestMatch = line.match(/guest: ([^ )]+)/) - const hostHashMatch = line.match(/hostHash: ([^)]+)/) // Capture content within parenthesis potentially + const hostHashMatch = line.match(/hostHash: ([^)]+)/) const guestHashMatch = line.match(/guestHash: ([^)]+)/) const isHostMatch = line.includes('isHost: true') - // Clean up hash strings (remove parenthesis if captured, split by ';') const cleanHash = (hashStr: string | null | undefined) => { if (!hashStr) return [] return hashStr @@ -926,23 +1153,14 @@ function parseLobbyInfoLine(line: string): ParsedLobbyInfo | null { isHost: isHostMatch, } } - -// Original boolStrToText - kept for reference if needed elsewhere -// function boolStrToText(str: string | undefined | null) { -// if (!str) { -// return 'Unknown' -// } -// if (str === 'true') { -// return 'Yes' -// } -// if (str === 'false') { -// return 'No' -// } -// return str -// } - -// Original getGamesConfigs - replaced by extractGameStartInfo -// function getGamesConfigs(lines: string[]) { ... } - -// Original parseLobbyInfo - replaced by parseLobbyInfoLine -// function parseLobbyInfo(line: string) { ... } +function cleanJokerKey(key: string): string { + if (!key) return '' + return key + .trim() + .replace(/^j_mp_|^j_/, '') // Remove prefixes j_mp_ or j_ + .replace(/_/g, ' ') // Replace underscores with spaces + .replace( + /\w\S*/g, // Capitalize each word (Title Case) + (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ) +} diff --git a/src/shared/jokers.ts b/src/shared/jokers.ts new file mode 100644 index 0000000..1040fe8 --- /dev/null +++ b/src/shared/jokers.ts @@ -0,0 +1,153 @@ +export interface JokerInfo { + name: string + file: string +} + +export const jokers: { [key: string]: JokerInfo } = { + j_8_ball: { name: '8 Ball', file: 'j_8_ball.png' }, + j_abstract: { name: 'Abstract Joker', file: 'j_abstract.png' }, + j_acrobat: { name: 'Acrobat', file: 'j_acrobat.png' }, + j_ancient: { name: 'Ancient Joker', file: 'j_ancient.png' }, + j_arrowhead: { name: 'Arrowhead', file: 'j_arrowhead.png' }, + j_astronomer: { name: 'Astronomer', file: 'j_astronomer.png' }, + j_banner: { name: 'Banner', file: 'j_banner.png' }, + j_baron: { name: 'Baron', file: 'j_baron.png' }, + j_baseball: { name: 'Baseball Card', file: 'j_baseball.png' }, + j_bloodstone: { name: 'Bloodstone', file: 'j_bloodstone.png' }, + j_blueprint: { name: 'Blueprint', file: 'j_blueprint.png' }, + j_bootstraps: { name: 'Bootstraps', file: 'j_bootstraps.png' }, + j_brainstorm: { name: 'Brainstorm', file: 'j_brainstorm.png' }, + j_bull: { name: 'Bull', file: 'j_bull.png' }, + j_burnt: { name: 'Burnt Joker', file: 'j_burnt.png' }, + j_business: { name: 'Business Card', file: 'j_business.png' }, + j_caino: { name: 'Caino', file: 'j_caino.png' }, + j_campfire: { name: 'Campfire', file: 'j_campfire.png' }, + j_card_sharp: { name: 'Card Sharp', file: 'j_card_sharp.png' }, + j_cartomancer: { name: 'Cartomancer', file: 'j_cartomancer.png' }, + j_castle: { name: 'Castle', file: 'j_castle.png' }, + j_ceremonial: { name: 'Ceremonial Dagger', file: 'j_ceremonial.png' }, + j_certificate: { name: 'Certificate', file: 'j_certificate.png' }, + j_chaos: { name: 'Chaos the Clown', file: 'j_chaos.png' }, + j_chicot: { name: 'Chicot', file: 'j_chicot.png' }, + j_clever: { name: 'Clever Joker', file: 'j_clever.png' }, + j_cloud_9: { name: 'Cloud 9', file: 'j_cloud_9.png' }, + j_crafty: { name: 'Crafty Joker', file: 'j_crafty.png' }, + j_crazy: { name: 'Crazy Joker', file: 'j_crazy.png' }, + j_credit_card: { name: 'Credit Card', file: 'j_credit_card.png' }, + j_delayed_grat: { name: 'Delayed Gratification', file: 'j_delayed_grat.png' }, + j_devious: { name: 'Devious Joker', file: 'j_devious.png' }, + j_diet_cola: { name: 'Diet Cola', file: 'j_diet_cola.png' }, + j_drivers_license: { + name: "Driver's License", + file: 'j_drivers_license.png', + }, + j_droll: { name: 'Droll Joker', file: 'j_droll.png' }, + j_drunkard: { name: 'Drunkard', file: 'j_drunkard.png' }, + j_duo: { name: 'The Duo', file: 'j_duo.png' }, + j_dusk: { name: 'Dusk', file: 'j_dusk.png' }, + j_egg: { name: 'Cavendish', file: 'j_egg.png' }, + j_erosion: { name: 'Erosion', file: 'j_erosion.png' }, + j_even_steven: { name: 'Even Steven', file: 'j_even_steven.png' }, + j_family: { name: 'The Family', file: 'j_family.png' }, + j_fibonacci: { name: 'Fibonacci', file: 'j_fibonacci.png' }, + j_flash: { name: 'Flash Card', file: 'j_flash.png' }, + j_flower_pot: { name: 'Flower Pot', file: 'j_flower_pot.png' }, + j_fortune_teller: { name: 'Fortune Teller', file: 'j_fortune_teller.png' }, + j_four_fingers: { name: 'Four Fingers', file: 'j_four_fingers.png' }, + j_gift: { name: 'Gift Card', file: 'j_gift.png' }, + j_glass: { name: 'Glass Joker', file: 'j_glass.png' }, + j_gluttenous_joker: { + name: 'Gluttonous Joker', + file: 'j_gluttenous_joker.png', + }, + j_golden: { name: 'Golden Joker', file: 'j_golden.png' }, + j_greedy_joker: { name: 'Greedy Joker', file: 'j_greedy_joker.png' }, + j_gros_michel: { name: 'Gros Michel', file: 'j_gros_michel.png' }, + j_hack: { name: 'Hack', file: 'j_hack.png' }, + j_half: { name: 'Half Joker', file: 'j_half.png' }, + j_hallucination: { name: 'Hallucination', file: 'j_hallucination.png' }, + j_hanging_chad: { name: 'Hanging Chad', file: 'j_hanging_chad.png' }, + j_hit_the_road: { name: 'Hit the Road', file: 'j_hit_the_road.png' }, + j_hologram: { name: 'Hologram', file: 'j_hologram.png' }, + j_idol: { name: 'The Idol', file: 'j_idol.png' }, + j_invisible: { name: 'Invisible Joker', file: 'j_invisible.png' }, + j_joker: { name: 'Joker', file: 'j_joker.png' }, + j_jolly: { name: 'Jolly Joker', file: 'j_jolly.png' }, + j_juggler: { name: 'Juggler', file: 'j_juggler.png' }, + j_locked: { name: 'Locked', file: 'j_locked.png' }, + j_loyalty_card: { name: 'Loyalty Card', file: 'j_loyalty_card.png' }, + j_luchador: { name: 'Luchador', file: 'j_luchador.png' }, + j_lucky_cat: { name: 'Lucky Cat', file: 'j_lucky_cat.png' }, + j_lusty_joker: { name: 'Lusty Joker', file: 'j_lusty_joker.png' }, + j_mad: { name: 'Mad Joker', file: 'j_mad.png' }, + j_madness: { name: 'Madness', file: 'j_madness.png' }, + j_mail: { name: 'Mail-In Rebate', file: 'j_mail.png' }, + j_marble: { name: 'Marble Joker', file: 'j_marble.png' }, + j_matador: { name: 'Matador', file: 'j_matador.png' }, + j_merry_andy: { name: 'Merry Andy', file: 'j_merry_andy.png' }, + j_midas_mask: { name: 'Midas Mask', file: 'j_midas_mask.png' }, + j_mime: { name: 'Mime', file: 'j_mime.png' }, + j_misprint: { name: 'Misprint', file: 'j_misprint.png' }, + j_mr_bones: { name: 'Mr. Bones', file: 'j_mr_bones.png' }, + j_mystic_summit: { name: 'Mystic Summit', file: 'j_mystic_summit.png' }, + j_obelisk: { name: 'Obelisk', file: 'j_obelisk.png' }, + j_odd_todd: { name: 'Odd Todd', file: 'j_odd_todd.png' }, + j_onyx_agate: { name: 'Onyx Agate', file: 'j_onyx_agate.png' }, + j_oops: { name: 'Oops! All 6s', file: 'j_oops.png' }, + j_order: { name: 'The Order', file: 'j_order.png' }, + j_pareidolia: { name: 'Pareidolia', file: 'j_pareidolia.png' }, + j_perkeo: { name: 'Perkeo', file: 'j_perkeo.png' }, + j_photograph: { name: 'Photograph', file: 'j_photograph.png' }, + j_popcorn: { name: 'Popcorn', file: 'j_popcorn.png' }, + j_raised_fist: { name: 'Raised Fist', file: 'j_raised_fist.png' }, + j_ramen: { name: 'Ramen', file: 'j_ramen.png' }, + j_red_card: { name: 'Red Card', file: 'j_red_card.png' }, + j_reserved_parking: { + name: 'Reserved Parking', + file: 'j_reserved_parking.png', + }, + j_ride_the_bus: { name: 'Ride the Bus', file: 'j_ride_the_bus.png' }, + j_riff_raff: { name: 'Riff-raff', file: 'j_riff_raff.png' }, + j_ring_master: { name: 'Showman', file: 'j_ring_master.png' }, + j_rocket: { name: 'Rocket', file: 'j_rocket.png' }, + j_rough_gem: { name: 'Rough Gem', file: 'j_rough_gem.png' }, + j_satellite: { name: 'Satellite', file: 'j_satellite.png' }, + j_scary_face: { name: 'Scary Face', file: 'j_scary_face.png' }, + j_scholar: { name: 'Scholar', file: 'j_scholar.png' }, + j_seance: { name: 'Seance', file: 'j_seance.png' }, + j_seeing_double: { name: 'Seeing Double', file: 'j_seeing_double.png' }, + j_selzer: { name: 'Seltzer', file: 'j_selzer.png' }, + j_shoot_the_moon: { name: 'Shoot the Moon', file: 'j_shoot_the_moon.png' }, + j_shortcut: { name: 'Shortcut', file: 'j_shortcut.png' }, + j_sly: { name: 'Sly Joker', file: 'j_sly.png' }, + j_smeared: { name: 'Smeared Joker', file: 'j_smeared.png' }, + j_smiley: { name: 'Smiley Face', file: 'j_smiley.png' }, + j_sock_and_buskin: { name: 'Sock and Buskin', file: 'j_sock_and_buskin.png' }, + j_space: { name: 'Space Joker', file: 'j_space.png' }, + j_square: { name: 'Square Joker', file: 'j_square.png' }, + j_steel_joker: { name: 'Steel Joker', file: 'j_steel_joker.png' }, + j_stencil: { name: 'Joker Stencil', file: 'j_stencil.png' }, + j_stone: { name: 'Stone Joker', file: 'j_stone.png' }, + j_stuntman: { name: 'Stuntman', file: 'j_stuntman.png' }, + j_supernova: { name: 'Supernova', file: 'j_supernova.png' }, + j_swashbuckler: { name: 'Swashbuckler', file: 'j_swashbuckler.png' }, + j_throwback: { name: 'Throwback', file: 'j_throwback.png' }, + j_ticket: { name: 'Golden Ticket', file: 'j_ticket.png' }, + j_to_the_moon: { name: 'To the Moon', file: 'j_to_the_moon.png' }, + j_trading: { name: 'Trading Card', file: 'j_trading.png' }, + j_tribe: { name: 'The Tribe', file: 'j_tribe.png' }, + j_triboulet: { name: 'Triboulet', file: 'j_triboulet.png' }, + j_trio: { name: 'The Trio', file: 'j_trio.png' }, + j_troubadour: { name: 'Troubadour', file: 'j_troubadour.png' }, + j_trousers: { name: 'Spare Trousers', file: 'j_trousers.png' }, + j_turtle_bean: { name: 'Turtle Bean', file: 'j_turtle_bean.png' }, + j_undiscovered: { name: 'Locked', file: 'j_undiscovered.png' }, + j_vagabond: { name: 'Vagabond', file: 'j_vagabond.png' }, + j_vampire: { name: 'Vampire', file: 'j_vampire.png' }, + j_walkie_talkie: { name: 'Walkie Talkie', file: 'j_walkie_talkie.png' }, + j_wee: { name: 'Wee Joker', file: 'j_wee.png' }, + j_wily: { name: 'Wily Joker', file: 'j_wily.png' }, + j_wrathful_joker: { name: 'Wrathful Joker', file: 'j_wrathful_joker.png' }, + j_yorick: { name: 'Yorick', file: 'j_yorick.png' }, + j_zany: { name: 'Zany Joker', file: 'j_zany.png' }, +}