#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;
static void
setupAction()
{
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()
{
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) {
GUI_ShowCredits(_state->energy > 0 && !Walkthrough_PlayerUsedWalkthrough());
}
}
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()
{
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()
{
/*
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()
{
Data_ResetState(_state);
_state->loaded = 1;
currentNode = Nodes_Get(_state->nodeName);
refreshNode(0, 0);
GUI_ShowHomeMenu();
GUI_ShowSave();
Prefs_Save();
}
static void
enterGame()
{
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()
{
_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 > 0.25) {
Play_ProcessInput(Walkthrough_Next(), 1);
walkthroughTime = _totalTime;
}
}