From 9fbaadeaac81fa77b040a2bc99112d3b4a614ba8 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Tue, 12 Aug 2025 21:22:58 -0400 Subject: [PATCH] Added lander face --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/lander_face.c | 572 +++++++++++++++++++++++++ watch-faces/complication/lander_face.h | 152 +++++++ 4 files changed, 726 insertions(+) create mode 100644 watch-faces/complication/lander_face.c create mode 100644 watch-faces/complication/lander_face.h diff --git a/movement_faces.h b/movement_faces.h index fc43716d..7108c5e0 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,4 +73,5 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "lander_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 22e262cf..f60a827c 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,4 +48,5 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/lander_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/lander_face.c b/watch-faces/complication/lander_face.c new file mode 100644 index 00000000..1cf81039 --- /dev/null +++ b/watch-faces/complication/lander_face.c @@ -0,0 +1,572 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include "lander_face.h" + +#ifndef max +#define max(x, y) ((y) > (x) ? (y) : (x)) +#endif + +#ifndef min +#define min(x, y) ((x) > (y) ? (y) : (x)) +#endif + +#define LANDER_TICK_FREQUENCY 8 +#define MONSTER_DISPLAY_TICKS 9 +#define ENGINE_THRUST 11 +#define MODE_WAITING_TO_START 0 +#define MODE_DISPLAY_SKILL_LEVEL 1 +#define MODE_PLAYING 2 +#define MODE_TOUCHDOWN_BLANK 3 +#define MODE_DISPLAY_FINAL_STATUS 4 +#define MODE_MONSTER 5 +#define MODE_FIND_EARTH_MESSAGE 6 +#define CREWS_COMPLIMENT 13 +// Granularity is divisions per foot - height display +#define GRANUL 40 +// Next lines for repeat heroes only. +#define PROMOTION_INTERVAL 3 +#define LEVEL_ACE 8 +#define LEVEL_STARBUCK 11 +#define HARD_EARTH_INCREMENTS 11 +#define MAX_HARD_EARTH_CHANCE 6 + +// The gory final result calculations: +#define SPEED_FATALITY_ALL 41 +#define SPEED_FATALITY_NONE 26 +#define SPEED_NO_DAMAGE 21 +#define SPEED_LEVEL_INCREMENTS 2 +#define SPEED_MAJOR_CRASH 73 +#define MAJOR_CRASH_INCREMENTS 65 +#define SPEED_INJURY_NONE 20 +#define SPEED_INJURY_FULCRUM 32 +#define INJURY_FULCRUM_PROB 65 +#define FUEL_SCORE_GOOD 145 +#define FUEL_SCORE_GREAT 131 +#define FUEL_SCORE_FANTASTIC 125 + +// Joey Castillo to oversee storage allocation row +#define LANDER_STORAGE_ROW 2 +#define STORAGE_KEY_NUMBER 110 + +#define DIFFICULTY_LEVELS 3 +char lander_difficulty_names[DIFFICULTY_LEVELS][7] = { + "NOrMAL", "HArd ", "HArdEr" +}; +#define MONSTER_TYPES 4 +char lander_monster_names[MONSTER_TYPES][7] = { + "mOnStr", "6Erbil", "HAmStr", "Rabbit" +}; +#define MONSTER_ACTIONS 8 +char lander_monster_actions[MONSTER_ACTIONS][7] = { + "HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y" +}; + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int16_t lower, int16_t upper) { + int range; + int retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) { + float probRange, speedRange; + float ratio, probFloat; + int probInt; + speedRange = upperSpeed - lowerSpeed; + if (speedRange<1.0) speedRange = 1.0; + probRange = upperProb - lowerProb; + ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange; + probFloat = (float) lowerProb + ( ratio * probRange ); + probInt = (int) ( probFloat + 0.5 ); + probInt = min ( probInt, upperProb ); + probInt = max ( probInt, lowerProb ); + return (uint8_t) probInt; +} + +static void write_to_lander_EEPROM(lander_state_t *state) { + uint8_t output_array [ 3 ]; + output_array [ 0 ] = STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->hero_counter; + output_array [ 2 ] = state->legend_counter; + watch_storage_erase ( LANDER_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 ); +} + +// --------------------------- +// Standard watch face methods +// --------------------------- +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(lander_state_t)); + memset(*context_ptr, 0, sizeof(lander_state_t)); + lander_state_t *state = (lander_state_t *)*context_ptr; + state->led_enabled = false; + } + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif +} + +void lander_face_activate(void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 7 ]; + state->mode = MODE_WAITING_TO_START; + state->led_active = false; + state->reset_counter = 0; + watch_clear_all_indicators ( ); + uint32_t offset = 0; + uint32_t size = 3; + uint8_t stored_data [ size ]; + // See if the hero_counter was ever written to EEPROM storage + watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size); + if (stored_data[0] == STORAGE_KEY_NUMBER ) + { + state->hero_counter = stored_data [1]; // There's real data in there. + state->legend_counter = stored_data [2]; + } + else + { + state->hero_counter = 0; // Nope. Nothing there. + state->legend_counter = 0; + write_to_lander_EEPROM(state); // Initial EEPROM tracking data. + } + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Fancy intro + if ( state->legend_counter == 0 ) watch_display_string("LA", 0); + else watch_display_string("LE", 0); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_string ( " ", 2); + else + { + sprintf ( buf, "%2d", state->hero_counter ); + watch_display_string(buf, 2); + } + if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); + else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); + else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" ); + else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good + else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); + else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); + watch_display_string ( buf, 4); + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); +} + +bool lander_face_loop(movement_event_t event, void *context) { + lander_state_t *state = (lander_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + + switch (event.event_type) { + case EVENT_TICK: + state->tick_counter++; + if ( state->mode == MODE_PLAYING ) { + int16_t accel = state->gravity; + bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read(); + if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) { + accel = ENGINE_THRUST + state->gravity; // Gravity is negative + state->fuel_remaining--; // Used 1 fuel unit + watch_set_indicator ( WATCH_INDICATOR_LAP ); + // Low fuel warning indicators + if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + watch_set_indicator ( WATCH_INDICATOR_BELL ); + watch_set_indicator ( WATCH_INDICATOR_PM ); + watch_set_indicator ( WATCH_INDICATOR_24H ); + } + else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty! + watch_clear_all_indicators ( ); + } + } + else { + watch_clear_indicator ( WATCH_INDICATOR_LAP ); + } + state->speed += accel; + state->height += state->speed; + if ( state->height > 971 * 80 ) { // Escape height + watch_clear_all_indicators (); + watch_display_string ( "ESCAPE", 4 ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->height <= 0 ) { // Touchdown + state->tick_counter = 0; + state->mode = MODE_TOUCHDOWN_BLANK; + } + else { + // Update height display + sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) ); + watch_display_string ( buf, 4 ); + } + } + else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { + // Blank display on touchdown + if ( state->tick_counter == 1 ) { + watch_clear_all_indicators (); + watch_display_string ( " ", 4 ); + + // Also calc fuel score now. + float fuel_score_float; + uint16_t fuel_used; + fuel_used = state->fuel_start - state->fuel_remaining; + fuel_score_float = (float) fuel_used / (float) state->fuel_tpl; + state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5); + if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier + // Monitor reset_counter + if ( fuel_used >= 1 ) state->reset_counter = 0; + else state->reset_counter++; + if ( state->reset_counter >= 3 ) { + state->hero_counter = 0; + state->difficulty_level = 0; + if ( state->reset_counter >= 6 ) state->legend_counter = 0; + watch_display_string ( "rESET ", 4 ); + write_to_lander_EEPROM(state); + } + } + // Wait until time for next display + if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_FINAL_STATUS; + } + } + else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) { + bool last_pass = false; + if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true; + + // Show final status + if ( state->tick_counter == 1 ) { + // Calculate many attributes + // 1) Major crash: bug, crater, vaporized (gone). + // 2) Rank ship's health 0 to 8 + // 3) Crew fatalities and injuries + // 4) Special conditions: hero + // 5) Set fuel conservation indicators as appropriate + // 6) Set coffee maker OK indicator as appropriate + // 7) Green light if ship intact + // 8) Set standard display if not preempted. + bool allDone; + int16_t finalSpeed, boostedSpeed, levelsDamage; + int8_t shipsHealth, myRand; + uint8_t fatalities, probFatal, probInjury; + uint8_t i; + + allDone = false; + // Easiest implementation for difficulty_level is to increase touchdown speed above actual. + finalSpeed = abs ( state->speed ) + state->difficulty_level * 4; + // First Earth is a bit easier than all the others + if ( state->legend_counter == 0 ) finalSpeed -= 2; + + // 1) Major crash: bug, crater, vaporized (gone). + if ( finalSpeed >= SPEED_MAJOR_CRASH ) { + allDone = true; + shipsHealth = -1; + if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " ); + else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" ); + else sprintf ( buf, " bU6" ); + } + // 2) Rank ship's health 0 to 8 + if (!allDone) { + boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS ); + shipsHealth = 8 - levelsDamage; + shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8 + shipsHealth = max ( shipsHealth, 0 ); + } + state->ships_health = shipsHealth; // Remember ships health + // 3) Crew fatalities and injuries + if (!allDone) { + // Fatalies + probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed ); + // Injuries + if ( finalSpeed <= SPEED_INJURY_FULCRUM ) { + probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed ); + } else { + probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed ); + } + fatalities = 0; + state->injured = 0; + for ( i = 0; i < CREWS_COMPLIMENT; i++ ) { + myRand = gen_random_int ( 1, 100 ); + if ( myRand <= probFatal ) fatalities++; + else if ( myRand <= probInjury ) state->injured++; + } + state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured; + } + // 4) Special conditions: hero + if (!allDone) { + if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) { + state->hero_counter++; + if ( state->hero_counter==1 ) sprintf ( buf, "HErO " ); + else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " ); + else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" ); + else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter ); + else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case + allDone = true; + // Two rule sets for finding Earth. Alternate between easy and hard. + int8_t my_odds, temp; + if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy + else { + temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1; + my_odds = temp / HARD_EARTH_INCREMENTS; + my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE ); + } + // Display odds in weekday region if positive value + if ( my_odds > 0 ) { + char buff3 [ 5 ]; + sprintf ( buff3, "%2d", my_odds ); + watch_display_string ( buff3, 2 ); + } else watch_display_string ( " ", 2 ); + if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. + sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode + state->hero_counter = 0; + state->legend_counter++; + } + // Recalculate difficulty level base on new hero_counter. + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Write to EEPROM + write_to_lander_EEPROM(state); + } + } + // 5) Set fuel conservation indicators as appropriate + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM ); + // 6) Set coffee maker OK indicator as appropriate + if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){ + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + } + // 7) Green light if ship intact + if ( shipsHealth >= 8 && state->led_enabled) { + watch_set_led_green ( ); + state->led_active = true; + } + // 8) Set standard display if not preempted. + if (!allDone) { + if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) { + sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured ); + } + else { + sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured ); + } + } + // Display final status. + watch_display_string ( buf, 4 ); + } // End if tick_counter == 1 + + // Major crash - ship burning with red LED. + if ( state->ships_health < 0 && state->led_enabled) { + if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass + // Turn on red LED. + watch_set_led_red ( ); + state->led_active = true; + } else { + watch_set_led_off ( ); + } + } + // Wait long enough, then allow waiting for next game. + if ( last_pass ) { + watch_set_led_off ( ); + // No change to display text, allow new game to start. + state->mode = MODE_WAITING_TO_START; + // Unless it's time for monsters + uint8_t survivors = state->injured + state->uninjured; + if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) && + ( gen_random_int ( -1, 3 ) >= state->ships_health ) ) { + state->mode = MODE_MONSTER; + state->tick_counter = 0; + state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 ); + } + } + } // End if MODE_DISPLAY_FINAL_STATUS + else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { + // Display skill level + if ( state->tick_counter == 1 ) { + sprintf ( buf, " %d %d ", state->skill_level, state->skill_level ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then start game. + if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + // Houston, WE ARE LAUNCHING NOW.... + state->mode = MODE_PLAYING; + } + } + else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { + // Display "Find" then "Earth" + if ( state->tick_counter == 1 ) { + sprintf ( buf, " FInd " ); + watch_display_string ( buf, 2 ); + } + if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { + sprintf ( buf, " EArTH " ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then display skill level. + if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + } + else if ( state->mode == MODE_MONSTER ) { + if ( state->tick_counter == 1 ) watch_display_string ( lander_monster_names[state->monster_type], 4 ); + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { + uint8_t my_rand; + my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); + watch_display_string ( lander_monster_actions[my_rand], 4 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character + sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); + buf [1] = 0; + watch_display_string(buf,4); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth + sprintf ( buf, "c%2d%2d", state->uninjured, state->injured ); + watch_display_string ( buf, 5 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_string ( "C", 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { + // Decision to: continue loop, end loop or eat astronaut + uint8_t survivors = state->injured + state->uninjured; + uint8_t myRand = gen_random_int ( 0, 16 ); + if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; + else if ( myRand <= 1 ) { // Leave loop with survivors + sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured ); + watch_display_string ( buf, 4 ); + state->mode = MODE_WAITING_TO_START; + } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues + else { // Eat an astronaut - welcome to the space program! + if ( state->injured > 0 && state->uninjured > 0 ) { + if ( gen_random_int ( 0,1 ) == 0 ) state->injured--; + else state->uninjured--; + } + else if ( state->injured > 0 ) state->injured--; + else state->uninjured--; + state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display + } + } + else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety + } // End if MODE_MONSTER + break; // End case EVENT_TICK + case EVENT_ALARM_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // That was the go signal - start a new game!! + float numerator, denominator, timeSquared; + int16_t gravity, thrust; + float myTime, distToTop, fuel_mult; + uint8_t skill_level; + int32_t tplTop; // Top lander height for TPL calculations + movement_request_tick_frequency(LANDER_TICK_FREQUENCY); + watch_set_led_off ( ); // Safety + watch_clear_all_indicators ( ); + // Randomize starting parameters + state->height = gen_random_int ( 131, 181 ) * 80; + // Per line below; see Mars Orbiter September 23, 1999 + if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80; + state->speed = gen_random_int ( -120, 35 ); // Positive is up + state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value + skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation + // Theoretical Perfect Landing (TPL) calculations start here. + myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is. + distToTop = fabs ( 0.5 * state->gravity * myTime * myTime ); + tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity. + // Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on. + gravity = abs ( state->gravity ); + thrust = ENGINE_THRUST + state->gravity; + numerator = 2.0 * (float) gravity * (float) tplTop; + denominator = thrust * thrust + thrust * gravity; + timeSquared = numerator / denominator; + state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL). + if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300% + else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150% + else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60% + else fuel_mult = 1.3; // TPL + 30% + state->fuel_start = state->fuel_tpl * fuel_mult; + state->fuel_remaining = state->fuel_start; + state->skill_level = skill_level; + state->tick_counter = 0; + if ( gen_random_int ( 1, 109 ) != 37 ) { + // Houston, approaching launch.... + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + else state->mode = MODE_FIND_EARTH_MESSAGE; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // Display difficulty level + watch_display_string ( lander_difficulty_names [state->difficulty_level], 4 ); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if ( state->mode != MODE_WAITING_TO_START ) break; + state->led_enabled = !state->led_enabled; + if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + else watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_LIGHT_LONG_UP: + if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) { + if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); + else sprintf (buf,"EArth%d", state->legend_counter ); + // Display legend counter + watch_display_string ( buf, 4 ); + } + break; + + default: + movement_default_loop_handler(event); + break; + } + if ( !state->led_active ) return true; + else return false; +} + +void lander_face_resign(void *context) { + (void) context; + watch_set_led_off ( ); +} \ No newline at end of file diff --git a/watch-faces/complication/lander_face.h b/watch-faces/complication/lander_face.h new file mode 100644 index 00000000..5e4c344f --- /dev/null +++ b/watch-faces/complication/lander_face.h @@ -0,0 +1,152 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef LANDER_FACE_H_ +#define LANDER_FACE_H_ + +#include "movement.h" + +/* + +My remake of a classic planet landing game. + +Objective: Safely land the Cringeworthy. +Use your limited fuel supply to achieve a soft touch-down. + +End scenarios and ship's health: + +Hero They name this planet after you. +8 Life is very cozy. +7 +6 +5 Life is tolerable, plus some creature comforts +4 +3 Marooned. +2 +1 +0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. ** +Bug As in squished. +Crater They name this crater after you. +Gone As in vapourized. + +Landing display format is: +Ship's health, intact crewmen, injured crewmen. + +Additional data: +Crew's compliment: 13. +Low fuel warning icons: activates when 3 seconds of full thrust remains. +** Yes, hamsters are very cute. However; some eating of astronauts may occur. + +Starting velocity, height and gravity are randomized each scenario. +Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters. + +A safe landing is always possible. + +End of game icons: +LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. ) +24H - Great budgeting of fuel supply +PM - Good budgeting of fuel supply +SIGNAL - The combination coffee and tea maker survived + +Landings get progressively harder with the number of heroic landings made. +Number of heroic landings are remembered. + +Heroic +Landings Status + 0 Normal + 3 Hard ( first difficulty increase ) + 6 Harder ( final difficulty increase ) + 8 Ace + 11 ?????? + +Save yourself. Save the coffee maker. + +END of standard training manual + +*/ + + +/* + +What is really going on here? +The fleet is lost. You are a newbie pilot making a name for yourself. + +Objective: Find Earth. + +After reaching ?????? status, future heroic sorties will have 'some' chance in 200 +of finding Earth. + +Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL). + +Completing HL 12 will give you 1 chance in 200, for that landing. +HL 13 will give you 2 chances in 200, for that landing. +HL 14 will give you 3 chances in 200, for that landing. +HL 20 will give you 9 chances in 200, for that landing, and so on. + +At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing. + +For wannabe pilots only: The HL counter can be reset by crashing three consecutive +missions without touching the thrust button. ( 6 to reset Earth-found counter ) + +Find Earth. Save Humanity. + +*/ + +typedef struct { + int32_t height; + int16_t speed; // Positive is up + uint16_t tick_counter; // For minimum delays + uint16_t fuel_start; + uint16_t fuel_remaining; + uint16_t fuel_tpl; // Fuel required for theoretical perfect landing + uint16_t fuel_score; // 100 is perfect; higher is less perfect + int8_t gravity; // negative downwards value + bool led_enabled; // Can the led be turned on? + bool led_active; // Did we use it this scenario? + uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster + uint8_t skill_level; // 1 thru 4. Dictates fuel alloted + int8_t ships_health; // 0 thru 8. -1 = major crash + uint8_t hero_counter; // Total heroic landings ever + uint8_t legend_counter; // Historic events counter ( Earth ) + uint8_t difficulty_level; // Based on hero_counter + uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios + uint8_t monster_type; // Which monster is hungry? + uint8_t uninjured; // OK survivors + uint8_t injured; // Hurt survivors +} lander_state_t; + +void lander_face_setup(uint8_t watch_face_index, void ** context_ptr); +void lander_face_activate(void *context); +bool lander_face_loop(movement_event_t event, void *context); +void lander_face_resign(void *context); + +#define lander_face ((const watch_face_t){ \ + lander_face_setup, \ + lander_face_activate, \ + lander_face_loop, \ + lander_face_resign, \ + NULL, \ +}) + +#endif // LANDER_FACE_H_