Game logic : timing / turns etc


#1

I’ve made a few little games in Python and Godot, and I’m now considering a 2d top-down turn-based RPG.

This is a step up in complexity for me, and I’m trying to visualise the game flow.

C++ is scary and I can’t really codedive - but I want to shamelessly borrow the way that C:DDA plays, because I like the way it plays.

Could somebody well-versed in the code give me an overview - in code terms - of how the logic for the turns works?

I imagine it’s something like :
Time is “frozen” until player character input.
this input triggers an action
this action has a “time-taken” measure
for the length of this time-taken measure, time unfreezes
all objects in game area check for actions for this duration
monsters move etc
(if character action takes 500 time units - and one monster move takes 450 time units - does the 50 extra time units “carry over” after the monster’s move to their next action or is it lost? )

Thanks in advance for any overview provided.


#2

http://cddawiki.chezzo.com/cdda_wiki/index.php?title=Hidden_stats#Moves


#3

ah hah, thanks. that outlines the turn structure.


#4

It would be really easier for you to browse the code by yourself in some editor which allows jumping between methods. You can start from game::do_turn function (https://github.com/CleverRaven/Cataclysm-DDA/blob/79d9903e17b0ceb9e648f9e6d3fd73a599f3b7fe/src/game.cpp#L1491).

Cataclysm is complex game, so there is a lot of things going on each turn (you can guess from the names of called functions).

Basically player has a number of movement points which are spent on current activity (while there are more than 0 movement points), then monster are moved (and other changes are made to the map), movement points are reset and turn is ended. Number of movement points are determined by speed and every creature/npc/player has some.

// MAIN GAME LOOP
// Returns true if game is over (death, saved, quit, etc)
bool game::do_turn()
{
    if( is_game_over() ) {
        return cleanup_at_end();
    }
    // Actual stuff
    if( new_game ) {
        new_game = false;
    } else {
        gamemode->per_turn();
        calendar::turn.increment();
    }

    // starting a new turn, clear out temperature cache
    temperature_cache.clear();

    if( npcs_dirty ) {
        load_npcs();
    }

    events.process();
    mission::process_all();

    if( calendar::once_every( 1_days ) ) {
        overmap_buffer.process_mongroups();
        if( calendar::turn.day_of_year() == 0 ) {
            lua_callback( "on_year_passed" );
        }
        lua_callback( "on_day_passed" );
    }

    if( calendar::once_every( 1_hours ) ) {
        lua_callback( "on_hour_passed" );
    }

    if( calendar::once_every( 1_minutes ) ) {
        lua_callback( "on_minute_passed" );
    }

    lua_callback( "on_turn_passed" );

    // Move hordes every 2.5 min
    if( calendar::once_every( time_duration::from_minutes( 2.5 ) ) ) {
        overmap_buffer.move_hordes();
        // Hordes that reached the reality bubble need to spawn,
        // make them spawn in invisible areas only.
        m.spawn_monsters( false );
    }

    u.update_body();

    // Auto-save if autosave is enabled
    if( get_option<bool>( "AUTOSAVE" ) &&
        calendar::once_every( 1_turns * get_option<int>( "AUTOSAVE_TURNS" ) ) &&
        !u.is_dead_state() ) {
        autosave();
    }

    update_weather();
    reset_light_level();

    perhaps_add_random_npc();

    process_activity();

    // Process sound events into sound markers for display to the player.
    sounds::process_sound_markers( &u );

    if( u.is_deaf() ) {
        sfx::do_hearing_loss();
    }

    if( !u.has_effect( efftype_id( "sleep" ) ) ) {
        if( u.moves > 0 || uquit == QUIT_WATCH ) {
            while( u.moves > 0 || uquit == QUIT_WATCH ) {
                cleanup_dead();
                // Process any new sounds the player caused during their turn.
                sounds::process_sound_markers( &u );
                if( !u.activity && uquit != QUIT_WATCH ) {
                    draw();
                }

                if( handle_action() ) {
                    ++moves_since_last_save;
                    u.action_taken();
                }

                if( is_game_over() ) {
                    return cleanup_at_end();
                }

                if( uquit == QUIT_WATCH ) {
                    break;
                }
                if( u.activity ) {
                    process_activity();
                }
            }
            // Reset displayed sound markers now that the turn is over.
            // We only want this to happen if the player had a chance to examine the sounds.
            sounds::reset_markers();
        } else {
            handle_key_blocking_activity();
        }
    }

    if( driving_view_offset.x != 0 || driving_view_offset.y != 0 ) {
        // Still have a view offset, but might not be driving anymore,
        // or the option has been deactivated,
        // might also happen when someone dives from a moving car.
        // or when using the handbrake.
        vehicle *veh = veh_pointer_or_null( m.veh_at( u.pos() ) );
        calc_driving_offset( veh );
    }

    // No-scent debug mutation has to be processed here or else it takes time to start working
    if( !u.has_active_bionic( bionic_id( "bio_scent_mask" ) ) &&
        !u.has_trait( trait_id( "DEBUG_NOSCENT" ) ) ) {
        scent.set( u.pos(), u.scent );
        overmap_buffer.set_scent( u.global_omt_location(),  u.scent );
    }
    scent.update( u.pos(), m );

    // We need floor cache before checking falling 'n stuff
    m.build_floor_caches();

    m.process_falling();
    m.vehmove();

    // Process power and fuel consumption for all vehicles, including off-map ones.
    // m.vehmove used to do this, but now it only give them moves instead.
    for( auto &elem : MAPBUFFER ) {
        tripoint sm_loc = elem.first;
        point sm_topleft = sm_to_ms_copy( sm_loc.x, sm_loc.y );
        point in_reality = m.getlocal( sm_topleft );

        submap *sm = elem.second;

        const bool in_bubble_z = m.has_zlevels() || sm_loc.z == get_levz();
        for( auto &veh : sm->vehicles ) {
            veh->power_parts();
            veh->idle( in_bubble_z && m.inbounds( in_reality.x, in_reality.y ) );
        }
    }
    m.process_fields();
    m.process_active_items();
    m.creature_in_field( u );

    // Apply sounds from previous turn to monster and NPC AI.
    sounds::process_sounds();
    // Update vision caches for monsters. If this turns out to be expensive,
    // consider a stripped down cache just for monsters.
    m.build_map_cache( get_levz(), true );
    monmove();
    update_stair_monsters();
    u.process_turn();
    if( u.moves < 0 && get_option<bool>( "FORCE_REDRAW" ) ) {
        draw();
        refresh_display();
    }
    u.process_active_items();

    if( get_levz() >= 0 && !u.is_underwater() ) {
        weather_data( weather ).effect();
    }

    const bool player_is_sleeping = u.has_effect( effect_sleep );

    if( player_is_sleeping ) {
        if( calendar::once_every( 30_minutes ) || !player_was_sleeping ) {
            draw();
            //Putting this in here to save on checking
            if( calendar::once_every( 1_hours ) ) {
                add_artifact_dreams( );
            }
        }

        if( calendar::once_every( 1_minutes ) ) {
            catacurses::window popup = create_wait_popup_window( string_format(
                                           _( "Wait till you wake up..." ) ) );

            wrefresh( popup );

            catacurses::refresh();
            refresh_display();
        }
    }

    player_was_sleeping = player_is_sleeping;

    u.update_bodytemp();
    u.update_body_wetness( *weather_precise );
    u.apply_wetness_morale( temperature );
    u.do_skill_rust();

    if( calendar::once_every( 1_minutes ) ) {
        u.update_morale();
    }

    if( calendar::once_every( 9_turns ) ) {
        u.check_and_recover_morale();
    }

    if( !u.is_deaf() ) {
        sfx::remove_hearing_loss();
    }
    sfx::do_danger_music();
    sfx::do_fatigue();

    return false;
}