#include "play.h" #include "datatypes.h" #include "data.h" #include "doors.h" #include "terminals.h" #include "mazes.h" #include "chapters.h" #include "nodes.h" #include "input.h" #include "gui.h" #include "audio.h" #include "loader.h" #include "walkthrough.h" #include "prefs.h" /* Copyright (c) 2013-2021 Devine Lu Linvega 2018 web port by rezmason 2021 C89 port by rezmason Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE. */ #ifdef DEBUG static char *orientationNames = "NESW"; #endif static GameState *_state = NULL; static Node *currentNode = NULL; static Subject *currentSubject = NULL; static int begun = 0; static double _totalTime = 0; static double resetTime = -1; static double walkthroughStartTime = -1; static double walkthroughTime = -1; #ifdef DEBUG static double walkthroughDelay = 0.1; #else static double walkthroughDelay = 0.25; #endif static void setupAction(void) { int hasAction = 1; GUI_EnableFuseCursor(0); switch(currentSubject->type) { case SUBJECTTYPE_DOOR: Doors_Setup(¤tSubject->data.door, _state); break; case SUBJECTTYPE_TERMINAL: Terminals_Setup(¤tSubject->data.terminal, _state); break; case SUBJECTTYPE_MAZE: Mazes_Setup(¤tSubject->data.maze, _state); break; default: hasAction = 0; break; } GUI_SetActionTrigger(hasAction); } static void moveTo(NodeSubject *node) { if(node->node == NULL) { printf("Play error: Cannot move to nonexistent node. What were you doing?\n"); return; } currentNode = node->node; strcpy(_state->nodeName, currentNode->name); if(node->orientation != ORIENTATION_NONE) { _state->orientation = node->orientation; } } static void refreshNode(int nudgeX, int nudgeY) { currentSubject = ¤tNode->subjects[_state->orientation]; GUI_Reset(); GUI_ShowNode(_state->nodeName, _state->orientation, MODIFIER_NONE, 0, 0); GUI_ShowMovement(nudgeX, nudgeY); setupAction(); Audio_SetAmbience(currentNode->zone); Audio_SetMusic(_state->chapter); #ifdef DEBUG printf("%s %c\n", _state->nodeName, orientationNames[_state->orientation]); #endif } static void updateChapter(void) { if(Chapters_Update(currentSubject, _state)) { GUI_ShowSave(); Prefs_Save(); } } static void action(int autoDoor) { switch(currentSubject->type) { case SUBJECTTYPE_DOOR: { NodeSubject *nodeSubject = Doors_Action(¤tSubject->data.door, _state, autoDoor); if(nodeSubject != NULL) { updateChapter(); moveTo(nodeSubject); refreshNode(0, 1); } } break; case SUBJECTTYPE_TERMINAL: if(Terminals_Action(¤tSubject->data.terminal, _state)) { updateChapter(); Audio_SetMusic(_state->chapter); } break; case SUBJECTTYPE_MAZE: moveTo(Mazes_Action(¤tSubject->data.maze, _state)); refreshNode(0, 1); break; default: break; } if(_state->completed) { int showSecret = _state->energy > 0; #ifndef DEBUG if(_state->usedWalkthrough) { showSecret = 0; } #endif GUI_ShowCredits(showSecret); } } static void turn(int direction) { Audio_PlayEffect(EFFECT_FOOTSTEP_TURN); _state->orientation = (_state->orientation + NumberOfOrientations + direction) % NumberOfOrientations; refreshNode(direction, 0); GUI_ShowFootstep(direction == -1 ? FOOTSTEP_LEFT : FOOTSTEP_RIGHT); } static void stepForward(void) { if(currentSubject->type == SUBJECTTYPE_NODE) { Audio_PlayFootstep(); moveTo(¤tSubject->data.node); refreshNode(0, 1); } else { Audio_PlayEffect(EFFECT_FOOTSTEP_COLLIDE); } GUI_ShowFootstep(FOOTSTEP_STRAIGHT); } static void stepBackward(void) { /* A step backwards is a "180° rotation", followed by a step forwards or action, followed by another "180° rotation". This is partly due to the fact that nodes in Hiversaires don't necessarily agree on which direction is north. */ int succeeded = 1; Audio_PlayEffect(EFFECT_FOOTSTEP_TURN); _state->orientation = (_state->orientation + 2) % NumberOfOrientations; refreshNode(0, 0); if(currentSubject->type == SUBJECTTYPE_NODE) { moveTo(¤tSubject->data.node); } else if(currentSubject->type == SUBJECTTYPE_DOOR || currentSubject->type == SUBJECTTYPE_MAZE) { action(1); } else { succeeded = 0; } _state->orientation = (_state->orientation + 2) % NumberOfOrientations; refreshNode(0, succeeded ? -1 : 0); GUI_ShowFootstep(FOOTSTEP_STRAIGHT); } static void reset(void) { Data_ResetState(_state); _state->loaded = 1; currentNode = Nodes_Get(_state->nodeName); refreshNode(0, 0); GUI_ShowHomeMenu(); GUI_ShowSave(); Prefs_Save(); } static void enterGame(void) { begun = 1; GUI_ShowHomeMenu(); } void Play_Init(GameState *state) { _state = state; if(!_state->loaded || _state->completed) { printf("\nWelcome, newcomer, to\n"); Data_ResetState(_state); _state->loaded = 1; } else { printf("\nWelcome back, friend, to\n"); } Audio_SetActive(_state->audio); currentNode = Nodes_Get(_state->nodeName); refreshNode(0, 0); GUI_RunSplash(enterGame); printf( " \n\n" " HH HH HH\n" " HH HH HH\n" " HH HH HH\n" " HHHHHHHHHHHH\n" " HH HH HH\n" " HH HH HH\n" " HH HH HH\n" " \n" " HHIVERSAIRES\n\n"); } void Play_Quit(void) { _state = NULL; } void Play_ProcessInput(InputType type, int down) { if(!begun) { GUI_SkipSplash(); return; } if(_state->completed) { if(down && (type == INPUTTYPE_ACTION || type == INPUTTYPE_CENTER || type == INPUTTYPE_FORWARD)) { GUI_LaunchCreditURL(); } return; } /* Any incoming input during a walkthrough cancels it part-way. */ walkthroughTime = -1; if(down) { switch(type) { #ifdef DEBUG case DEBUGINPUTTYPE_ENERGY: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->energy = (_state->energy + 1) % 4; printf("DEBUG: energy = %d\n", _state->energy); GUI_ShowEnergy(_state->energy); break; case DEBUGINPUTTYPE_CLOCK: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->clock = (_state->clock + 1) % 3; printf("DEBUG: clock = %d\n", _state->clock); GUI_ShowClock(_state->clock); break; case DEBUGINPUTTYPE_STUDIO: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->studio = !_state->studio; printf("DEBUG: studio = %d\n", _state->studio); break; case DEBUGINPUTTYPE_SEAL_1: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->seals[0] = (_state->seals[0] + 1) % NumberOfZones; printf("DEBUG: seal 1 = %d\n", _state->seals[0]); GUI_ShowSeals(_state->seals[0], _state->seals[1]); break; case DEBUGINPUTTYPE_SEAL_2: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->seals[1] = (_state->seals[1] + 1) % NumberOfZones; printf("DEBUG: seal 2 = %d\n", _state->seals[1]); GUI_ShowSeals(_state->seals[0], _state->seals[1]); break; case DEBUGINPUTTYPE_SECRET: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->secret = !_state->secret; printf("DEBUG: secret = %d\n", _state->secret); break; case DEBUGINPUTTYPE_TIME: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->time = !_state->time; printf("DEBUG: time = %d\n", _state->time); break; case DEBUGINPUTTYPE_CHAPTER: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); _state->chapter = (_state->chapter + 1) % NumberOfChapters; Audio_SetMusic(_state->chapter); printf("DEBUG: chapter = %d\n", _state->chapter); break; case DEBUGINPUTTYPE_UNLOCK_DOORS: Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); Doors_unlockAll = !Doors_unlockAll; printf("DEBUG: unlock all doors set to = %d\n", Doors_unlockAll); break; #endif case INPUTTYPE_LEFT: turn(-1); break; case INPUTTYPE_RIGHT: turn(1); break; case INPUTTYPE_FORWARD: stepForward(); break; case INPUTTYPE_BACK: stepBackward(); break; case INPUTTYPE_ACTION: action(0); break; case INPUTTYPE_CENTER: /* Depending on the subject, a CENTER input is either a step forward or an action. */ if(currentSubject->type == SUBJECTTYPE_NODE) { stepForward(); } else { action(0); } break; case INPUTTYPE_RESET: /* Unlike the other inputs above, RESET starts a countdown as a way to confirm the player's intention. */ resetTime = _totalTime; GUI_TintRed(); Audio_PlayEffect(EFFECT_ACTION_SEAL_INACTIVE); break; case INPUTTYPE_WALKTHROUGH: /* WALKTHROUGH, like RESET, starts a countdown. */ walkthroughStartTime = _totalTime; GUI_TintCyan(); Audio_PlayEffect(EFFECT_ACTION_SEAL_INIT); break; } } else { switch(type) { case INPUTTYPE_RESET: /* Cancel the RESET countdown */ resetTime = -1; Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); GUI_StopTint(); break; case INPUTTYPE_WALKTHROUGH: /* Cancel the WALKTHROUGH countdown */ walkthroughStartTime = -1; Audio_PlayEffect(EFFECT_ACTION_ENERGY_ACTIVE); GUI_StopTint(); break; default: break; } } } void Play_Update(double totalTime, double deltaTime) { (void)deltaTime; _totalTime = totalTime; /* If the RESET countdown ticks to zero, we reset the game state. */ if(resetTime != -1 && _totalTime - resetTime > 2) { resetTime = -1; walkthroughStartTime = -1; walkthroughTime = -1; GUI_StopTint(); Audio_PlayEffect(EFFECT_ACTION_DOOR_ACTIVE); reset(); } /* If the WALKTHROUGH countdown ticks to zero, we reset the game state, and initialize the walkthrough. */ if(walkthroughStartTime != -1 && _totalTime - walkthroughStartTime > 2) { resetTime = -1; walkthroughStartTime = -1; GUI_StopTint(); Audio_PlayEffect(EFFECT_ACTION_DOOR_ACTIVE); reset(); Walkthrough_Reset(); walkthroughTime = _totalTime; } /* We run the walkthrough if it is active. */ if(!_state->completed && walkthroughTime != -1 && _totalTime - walkthroughTime > walkthroughDelay) { _state->usedWalkthrough = 1; Play_ProcessInput(Walkthrough_Next(), 1); walkthroughTime = _totalTime; } }