diff --git a/src/base/all.h b/src/base/all.h index f0aa58e..eb45fe8 100644 --- a/src/base/all.h +++ b/src/base/all.h @@ -503,6 +503,8 @@ global const u64 MAX_u64 = 0xffffffffffffffffull; global const u32 MAX_u32 = 0xffffffff; global const u16 MAX_u16 = 0xffff; global const u8 MAX_u8 = 0xff; +#define EULERS_E (2.71828) +#define PI (3.14159265358979323846) ///// CUSTOM ENTRY POINT /* TODO? diff --git a/src/client.c b/src/client.c index b994bc2..26edd07 100644 --- a/src/client.c +++ b/src/client.c @@ -30,9 +30,9 @@ ///// TYPES typedef enum Tab { TabDebug, -// TabChat, TabMap, TabShip, + TabAuction, TabMarket, TabPassengers, Tab_Count, @@ -62,6 +62,13 @@ typedef enum PassengersTabStates { PassengersTabStates_Count, } PassengersTabStates; +typedef enum AuctionTabStates { + AuctionTabStateMain, + AuctionTabStateLoading, + AuctionTabStateResult, + AuctionTabStates_Count, +} AuctionTabStates; + typedef enum Screen { ScreenLogin, ScreenCreateCharacter, @@ -165,6 +172,7 @@ typedef struct GameState { MarketTabStates market_tab_state; ShipTabStates ship_tab_states; PassengersTabStates passenger_tab_state; + AuctionTabStates auction_tab_state; bool passenger_job_accepted; TransactionResult tx_result; u64 turn_tick_started_on; @@ -179,7 +187,7 @@ global u8 system_message_index = 0; global GameState state = {0}; global OutgoingMessageQueue* network_send_queue = {0}; global ParsedServerMessageThreadQueue* network_recv_queue = {0}; -global str TAB_STRS[Tab_Count] = {"Debug", /*"Chat",*/ "Map", "Ship", "Market", "Passengers"}; +global str TAB_STRS[Tab_Count] = {"Debug", "Map", "Ship", "Auction", "Market", "Passengers"}; global FieldDescriptor COMPARE_COMMODITY_FIELDS[STAR_SYSTEM_COUNT] = { { "System", FieldTypeString, offsetof(CompareCommodity, system_name), 16 }, { "Bid", FieldTypeU32, offsetof(CompareCommodity, bid), 6 }, @@ -306,6 +314,8 @@ fn void resetTabRow(Tab tab) { state.row.selected_index = 0; state.modal_choice.selected_index = 0; state.modal_choice.len = 2; + } else if (state.menu.selected_index == TabAuction) { + state.auction_tab_state = AuctionTabStateMain; } } @@ -341,6 +351,16 @@ fn void handleIncomingMessage(u8* message, u32 len, SocketAddress sender, i32 so case MessageGameOver: { parsed.byte = message[msg_pos++]; } break; + case MessageAuctionDetails: { + parsed.sys.auction.type = message[msg_pos++]; + parsed.sys.auction.qty = message[msg_pos++]; + parsed.sys.auction.price = readU32FromBufferLE(message + msg_pos); + msg_pos += 4; + parsed.sys.auction.started_at = readU32FromBufferLE(message + msg_pos); + msg_pos += 4; + parsed.sys.auction.finished_at = readU32FromBufferLE(message + msg_pos); + msg_pos += 4; + } break; case MessageJobComplete: { parsed.byte = message[msg_pos++]; } break; @@ -628,6 +648,11 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count } } } break; + case MessageAuctionDetails: { + u32 sys_idx = state->me.system_idx; + StarSystem* sys = &state->map[sys_idx]; + sys->auction = msg.sys.auction; + } break; case MessageSystemCommodities: { u32 sys_idx = msg.id; StarSystem* sys = &state->map[sys_idx]; @@ -878,7 +903,7 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count // draw the tabs box u32 tabs_y = 0; - u32 box_y = tabs_y + 3; + u32 box_y = tabs_y + 2; Box box = { .x = 1, .y = box_y, @@ -903,9 +928,6 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count } renderSystemMessages(tui->frame_buffer, tui->screen_dimensions, box); } break; - /*case TabChat: { - renderStrToBuffer(tui->frame_buffer, box.x+2, box.y+1, "You haven't heard anything interesting lately...", screen_dimensions); - } break;*/ case TabMap: { if (user_pressed_up) { state->pos.y -= 1; @@ -1248,6 +1270,54 @@ fn bool updateAndRender(TuiState* tui, void* s, u8* input_buffer, u64 loop_count renderStrToBuffer(tui->frame_buffer, modal_outline.x+2, y_off, "Loading...", screen_dimensions); } } break; + case TabAuction: { + u32 yoff = box.y+1; + renderStrToBuffer(tui->frame_buffer, box.x+((box.width - 13)/2), yoff++, "Auction House", screen_dimensions); + yoff++; + + if (curr.auction.finished_at > curr.auction.started_at) { + MemoryZero(sbuf, SBUFLEN); + sprintf(sbuf, "Sold! %d containers of %s were sold for $%d", curr.auction.qty, COMMODITY_STRINGS[curr.auction.type], curr.auction.price); + renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); + } else if (state->auction_tab_state == AuctionTabStateMain) { + MemoryZero(sbuf, SBUFLEN); + sprintf(sbuf, "Commodity up for Auction: %s", COMMODITY_STRINGS[curr.auction.type]); + renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); + + MemoryZero(sbuf, SBUFLEN); + sprintf(sbuf, "Offering %d containers", curr.auction.qty); + renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); + yoff++; + + MemoryZero(sbuf, SBUFLEN); + sprintf(sbuf, "Current price: %d", curr.auction.price); + renderStrToBuffer(tui->frame_buffer, box.x+2, yoff++, sbuf, screen_dimensions); + yoff++; + + const char* label = "ACCEPT OFFER [ENTER]"; + Range1u32 range = { + .min = XYToPos(box.x+6, yoff, screen_dimensions.width), + .max = XYToPos(box.x+6+strlen(label), yoff, screen_dimensions.width), + }; + tui->cursor.x = box.x+6; + tui->cursor.y = yoff; + colorizeRange(tui, range, ANSI_WHITE, ANSI_BLACK); + renderStrToBuffer(tui->frame_buffer, box.x+6, yoff++, label, screen_dimensions); + if (user_pressed_enter) { + // send the "buy this auction" message to the server + u32 msg_idx = 0; + UDPMessage msg = {0}; + msg.address = udp->server_address; + msg.bytes[msg_idx++] = CommandBuyAuction; + msg.bytes_len = msg_idx; + outgoingMessageQueuePush(network_send_queue, &msg); + + state->auction_tab_state = AuctionTabStateLoading; + } + } else if (state->auction_tab_state == AuctionTabStateLoading) { + renderStrToBuffer(tui->frame_buffer, box.x+((box.width - 13)/2), yoff++, "Bid submitted...", screen_dimensions); + } + } break; case TabMarket: { if (input_buffer[0] == 'q' || user_pressed_esc) { state->market_tab_state = MarketTabStateTable; diff --git a/src/server.c b/src/server.c index 17337ab..08460de 100644 --- a/src/server.c +++ b/src/server.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "shared.h" #include "base/impl.c" #define NET_OUTGOING_MESSAGE_QUEUE_LEN 64 @@ -286,6 +287,14 @@ fn u32 starSystemPlanetCount(StarSystem* sys) { return planet_count; } +fn bool accountIsEmpty(Account* a) { + return a->id == 0 && a->name.length == 0; +} + +fn bool shipIsNull(PlayerShip* ship) { + return ship->id == 0 && ship->base_cost == 0; +} + fn UDPMessage makeMessageSystemPassengers(StarSystem* sys) { UDPMessage outgoing_message = {0}; u32 msg_i = 0; @@ -562,6 +571,26 @@ fn void* sendNetworkUpdates(void* sock) { sendUDPu8List(socket, &client.address, &sys_msg); // and the passenger offers sendUDPu8List(socket, &client.address, &sys_pass_msg); + + // send the client the current auction details for their current system + Account* account = &state.accounts[client.account_id]; + if (!accountIsEmpty(account)) { + StarSystem curr = state.map[account->ship.system_idx]; + MemoryZero(sbuf, SBUFLEN); + u32 msgidx = 0; + sbuf[msgidx++] = MessageAuctionDetails; + sbuf[msgidx++] = curr.auction.type; + sbuf[msgidx++] = curr.auction.qty; + msgidx += writeU32ToBufferLE((u8*)sbuf + msgidx, curr.auction.price); + msgidx += writeU32ToBufferLE((u8*)sbuf + msgidx, curr.auction.started_at); + msgidx += writeU32ToBufferLE((u8*)sbuf + msgidx, curr.auction.finished_at); + u8List msg = { + .capacity = UDP_MAX_MESSAGE_LEN, + .items = (u8*)sbuf, + .length = msgidx, + }; + sendUDPu8List(socket, &client.address, &msg); + } } // update all the changed systems @@ -621,14 +650,6 @@ fn void* sendNetworkUpdates(void* sock) { return NULL; } -fn bool accountIsEmpty(Account* a) { - return a->id == 0 && a->name.length == 0; -} - -fn bool shipIsNull(PlayerShip* ship) { - return ship->id == 0 && ship->base_cost == 0; -} - fn void* gameLoop(void* params) { LaneCtx* lane_ctx = (LaneCtx*)params; ThreadContext tctx = { @@ -1003,6 +1024,27 @@ fn void* gameLoop(void* params) { } } + // tick all the star systems (auctions) + StarSystem* sys = NULL; + Range1u64 sys_range = LaneRange(STAR_SYSTEM_COUNT); + for (u32 i = sys_range.min; i < sys_range.max; i++) { + sys = &state.map[i]; + + // tick the auction price + u32 grace_period_ends_at = sys->auction.started_at + (GOAL_GAME_LOOPS_PER_S * 3); + bool is_auction_still_running = sys->auction.finished_at == 0; + bool is_auction_grace_period_finished = state.frame > grace_period_ends_at; + if (is_auction_still_running && is_auction_grace_period_finished) { + f32 initial_price = COMMODITIES[sys->auction.type].price * AUCTION_PRICE_START_MULTIPLE; + f32 t_sec = ((f32)state.frame - (f32)grace_period_ends_at) / (f32)GOAL_GAME_LOOPS_PER_S; + // t is in minutes + f32 t = t_sec / 60; + f32 decay = -0.30 * t; + f32 floor_price = COMMODITIES[sys->auction.type].price * 0.9; + sys->auction.price = Max((initial_price * pow(EULERS_E, decay)), floor_price); + } + } + // 2. tick non-user entities if (state.all_accounts_ready) { addSystemMessage((u8*)"NEW TURN: ticking all_accounts_ready"); @@ -1012,7 +1054,7 @@ fn void* gameLoop(void* params) { for (u32 i = sys_range.min; i < sys_range.max; i++) { sys = &state.map[i]; sys->changed = true; - // consume and produce commodities + // 1. consume and produce commodities for (u32 ii = 0; ii < Commodity_Count; ii++) { for (u32 iii = 0; iii < MAX_PLANETS; iii++) { if (sys->planets[iii].type != PlanetTypeNull) { @@ -1025,7 +1067,8 @@ fn void* gameLoop(void* params) { } } } - // potentially create a new passenger job + + // 2. potentially create a new passenger job u32 planet_divisor = ((MAX_PLANETS - starSystemPlanetCount(sys))+1); u32 max_passenger_job_count = 1 + ((MAX_PASSENGER_JOB_OFFERS-1) / planet_divisor); for (u32 ii = 0; ii < max_passenger_job_count; ii++) { @@ -1041,6 +1084,13 @@ fn void* gameLoop(void* params) { sys->offers[ii].offer = (u32)((f32)sys->offers[ii].offer * 1.05); } } + + // 3. reset the auction + MemoryZero(&sys->auction, sizeof(Auction)); + sys->auction.started_at = state.frame; + sys->auction.type = AUCTION_COMMODITY_CUTOFF + (rand() % (Commodity_Count - AUCTION_COMMODITY_CUTOFF)); + sys->auction.price = COMMODITIES[sys->auction.type].price * AUCTION_PRICE_START_MULTIPLE; + sys->auction.qty = COMMODITIES[sys->auction.type].consumption; } Range1u64 ship_range = LaneRange(player_count); @@ -1420,6 +1470,12 @@ i32 main(i32 argc, ptr argv[]) { // now, build out the commodity info and passenger jobs for the systems char sbuf[SBUFLEN] = {0}; for (u32 i = 0; i < STAR_SYSTEM_COUNT; i++) { + // setup the auction + state.map[i].auction.started_at = state.frame; + state.map[i].auction.type = AUCTION_COMMODITY_CUTOFF + (rand() % (Commodity_Count - AUCTION_COMMODITY_CUTOFF)); + state.map[i].auction.price = COMMODITIES[state.map[i].auction.type].price * AUCTION_PRICE_START_MULTIPLE; + state.map[i].auction.qty = COMMODITIES[state.map[i].auction.type].consumption; + // setup the planets u32 planet_count = rand() % MAX_PLANETS + 1; for (u32 ii = 0; ii < planet_count; ii++) { Planet* p = &state.map[i].planets[ii]; diff --git a/src/shared.h b/src/shared.h index 548cb78..a05e6e6 100644 --- a/src/shared.h +++ b/src/shared.h @@ -7,14 +7,16 @@ ///// #define some game-tunable constants #define STARTING_DOWN_PAYMENT (10000) #define SHIP_DETAIL_COUNT (8) -#define STAR_SYSTEM_COUNT (16) -#define MAP_WIDTH (32) +#define STAR_SYSTEM_COUNT (6) +#define MAP_WIDTH (28) #define MAP_HEIGHT (12) #define MAX_PLANETS (3) -#define MAX_PASSENGER_JOB_OFFERS (16) +#define MAX_PASSENGER_JOB_OFFERS (12) #define MAX_PASSENGER_JOB_PEOPLE (4) #define MAX_PASSENGER_JOB_PRICE (10000) #define MAX_PASSENGER_BERTHS (8) +#define AUCTION_COMMODITY_CUTOFF (6) +#define AUCTION_PRICE_START_MULTIPLE (12) typedef enum ShipType { ShipSparrow, @@ -252,8 +254,8 @@ typedef struct PlayerShip { u32 base_cost; u32 remaining_mortgage; f32 interest_rate; - u32 cu_m_fuel; // we are just saying you can buy as much fuel as you want - u32 cu_m_o2; // we are just saying you can buy as much o2 as you want + u32 cu_m_fuel; + u32 cu_m_o2; u32 credits; u64 id; u32 commodities[Commodity_Count]; @@ -276,6 +278,14 @@ typedef struct Planet { u32 production[Commodity_Count]; } Planet; +typedef struct Auction { + CommodityType type; + u8 qty; + u32 price; + u32 started_at; + u32 finished_at; +} Auction; + typedef struct StarSystem { bool changed; u32 x; @@ -285,26 +295,27 @@ typedef struct StarSystem { str name; Planet planets[MAX_PLANETS]; PassengerJobOffer offers[MAX_PASSENGER_JOB_OFFERS]; + Auction auction; } StarSystem; global str STAR_NAMES[STAR_SYSTEM_COUNT] = { "Vega", "Aldebaran", "Mining Colony 17", - "Antares", + "Centauri Prime", "Mintaka", "Barnard's Star", - "Betelgeuse", +/* "Betelgeuse", + "Antares", "Tau Ceti", "Capella", "Castor", - "Centauri Prime", "Deneb", "Epsilon Eridani", "Fomalhaut", "Gliese 581", "Hadar", -/* "Izar", + "Izar", "Achernar", "Kepler-186", "Lacaille 8760", @@ -352,6 +363,7 @@ typedef enum CommandType { CommandSetDestination, CommandPayMortgage, CommandAcceptPassengerJob, + CommandBuyAuction, CommandType_Count, } CommandType; @@ -364,6 +376,7 @@ static const char* command_type_strings[CommandType_Count] = { "SetDestination", "PayMortgage", "AcceptPassengerJob", + "BuyAuction", }; typedef enum Message { @@ -382,6 +395,7 @@ typedef enum Message { MessageJobAcceptResult, MessageJobComplete, MessageNotAlive, + MessageAuctionDetails, Message_Count, } Message; @@ -399,7 +413,8 @@ static const char* MESSAGE_STRINGS[] = { "PayoffResult", "SystemPassengers", "JobAcceptResult", - "MessageNotAlive", + "NotAlive", + "AuctionDetails", }; ///// shared helper functions @@ -445,7 +460,7 @@ fn f32 priceForCommodity(CommodityType type, u32 quantity, bool bid) { } fn f32 calcInterestRate(u32 cost, u32 down_payment) { - return 3.0; + return 4.0; //f32 mortgage_rate = 10.0 *(1.0 - (3.0*(f32)down_payment / ((f32)cost/3.0))); //return mortgage_rate; }