package Blacknote::System; use strict; use warnings FATAL => qw(all); use Blacknote::Logging; use Class::MOP; use Blacknote::Sound; use Blacknote::Interface; use Blacknote::System::State qw( bnsm_get_player bnsm_get_mapdata bnsm_which_tile bnsm_get_state bnsm_set_state bnsm_set_active_keybinding bnsm_get_log bnsm_get_tile_at bnsm_set_player bnsm_get_snd_chunk bnsm_add_snd_chunk bnsm_get_focused_window bnsm_set_focused_window bnsm_run_hooks bnsm_push_turn bnsm_change_state STATE_GAME STATE_BOOT STATE_MENU ); #use Blacknote::Input; use List::Util qw(any); use Curses; use Exporter qw(import); use Carp qw(croak); our @EXPORT_OK = qw(deftile defequipment); defclass Tile => attributes => [ qw(pos x y color type symbol) ]; definterface IDrawable => qw(x y symbol); definterface IKeyInputsRouge => qw( smash action interact examine search move_n move_s move_w move_e move_ne move_nw move_se move_sw quaf drop rest pause character_screen activate enter_confirm run_w run_s run_w run_e run_ne run_nw run_se run_sw look_around exec_binding pickup ); definterface IKeyInputsMenu => qw(anykey enter cancel); definterface IWindow => qw(title keyboard_selection close prepare_items x y w h); sub deftile ($$$;$$@) { my ($tilename, $type, $symbol, $color, $blocking, @classes) = @_; my ($pkg) = caller; my $meta = Class::MOP::Class->create( $pkg ."::"."Tile::".$tilename => ( superclasses => [ $pkg . "::".'Tile', @classes ], attributes => [ Class::MOP::Attribute->new(blocking => ( default => $blocking, accessor => "blocking" ) ), Class::MOP::Attribute->new(x => accessor => "x"), Class::MOP::Attribute->new(y => accessor => "y"), Class::MOP::Attribute->new(symbol => accessor => "symbol", default => $symbol), ], methods => { type => sub { return $type }, color => sub { return $color }, }, ) ); $Blacknote::System::State::TILE_MAPPING{$type} = $meta->name; DEBUG "Created symbol mapping for tile type $type [@{[$meta->name()]}] to symbol `$symbol'"; } definterface IEquipment => qw(wear take_off damage symbol); defrole RDrawable => methods => { draw => sub { my $self = shift; addch($self->y, $self->x, $self->symbol); } }; defrole RDoor => methods => { open => sub { my $self = shift; $self->blocking(0); $self->symbol("'"); }, close => sub { my $self = shift; $self->blocking(1); $self->symbol("+"); # TODO: Make this open/close symbol more dynamic } }; # A role that handles any moving object such as the player defrole RMoving => methods => { tile_at => sub { # NOTE: ->tile_at method uses relative coordinate addressing from player position my $self = shift; my $x = shift; my $y = shift; my $target_x = $self->x + $x; my $target_y = $self->y + $y; my $row = $self->mapdata->[$target_y]; my $tile; if(defined $row){ $tile = $row->[$target_x]; } if($target_y < 0 or $target_x < 0){ return; } return $tile; }, tile_at_current_pos => sub { my $self = shift; return $self->tile_at(0,0); }, can_move => sub { my $self = shift; my ($x,$y) = @_; my $tile = $self->tile_at($x,$y); return 0 if not defined $tile; if(defined $tile){ if($tile->isa("Blacknote::System::RDoor")){ my $canmove = not $tile->blocking; if($self->isa("Blacknote::System::Player")){ bnsm_get_log->push("You opened the door") if $tile->blocking; # if its a door and its blocking = closed }else{ # This role can be used in enemies as well, so we display a different message bnsm_get_log->push("Something opened a door") if $tile->blocking; } $tile->open; return $canmove; } } if( (not defined $tile) or ( defined $tile and $tile->blocking ) ){ if($self->isa("Blacknote::System::Player")){ bnsm_get_log->push("Can't go that way, something is blocking the way!"); } return 0; }else{ return 1; } }, move => sub { my $self = shift; my ($x,$y) = @_; my $target_x = $self->x + $x; my $target_y = $self->y + $y; # This indicates that this action will take a turn bnsm_push_turn { if($self->can_move($x,$y)){ $self->x($target_x); $self->y($target_y); if($self->can('disrupted')){ $self->disrupted(0); } $self->consume_turns(1); }else{ if($self->can("disrupted")){ # If the movable entity can be disrupted by a blocking object or tile, then it will be $self->disrupted(1); } } }; }, consume_turns => sub { my $self = shift; $self->turns(0) if not defined $self->turns; $self->turns($self->turns + shift // 0); return $self->turns; }, mapdata => sub { return bnsm_get_mapdata() }, }; definterface IItem => qw(pickup color symbol x y name desc); sub defequipment ($$$$$$){ my ($name, $type, $ac, $pool, $symbol, $color) = @_; my ($pkg) = caller; Class::MOP::Class->create( "Blacknote::Equipment::".$name => ( superclasses => [ "Blacknote::System::IEquipment", "Blacknote::System::RDrawable", "Blacknote::System::IItem", "Blacknote::System::IDrawable" ], methods => { wear => sub { my $self = shift; DEBUG "Wearing: " . $self->name; }, damage => sub {}, take_off => sub { my $self = shift; DEBUG "No longer wearing" . $self->name; }, symbol => sub { return $symbol }, pickup => sub { my $self = shift; DEBUG "Picked up:".$self->name; bnsm_get_log->push("Picked up: ".$self->name); } }, attributes => [ Class::MOP::Attribute->new('name' => default => $name, accessor => "name"), # name of item Class::MOP::Attribute->new('ac' => default => $ac, accessor => "ac"), # armor class Class::MOP::Attribute->new(desc => (default => 'Undescribed item', accessor => "desc")), # description Class::MOP::Attribute->new("x" => accessor => "x"), Class::MOP::Attribute->new("y" => accessor => "y"), Class::MOP::Attribute->new("color" => accessor => "color", default => 'white'), ], ) ); push @{$Blacknote::System::State::ITEM_POOLS{$pool}}, "Blacknote::Equipment::".$name; } sub defun ($$$) { my ($name, $type, $code) = @_; $Blacknote::System::State::FUNCTION_POOL{$type} = [] if not exists $Blacknote::System::State::FUNCTION_POOL{$type}; push @{$Blacknote::System::State::FUNCTION_POOL{$type}}, $code; } sub defkeymap ($%) { my $name = shift; my %mappings = @_; $Blacknote::System::State::KEYMAP{$name} = \%mappings; } sub defhook (&$){ my ($hooktype, $code) = @_; DEBUG "Added a hook to state hook $hooktype"; push @{$Blacknote::System::State::HOOKS{$hooktype}}, $code; } sub defkeybindings { my $name = shift; my $pkg = (caller)[0]; my %bindings = @_; my $INSTANCE = undef; my $meta = Class::MOP::Class->create( $pkg . "::" . $name => ( superclasses => [ "Blacknote::System::IKeyInputsRouge" ], methods => \%bindings ) ); $meta->add_method( exec_binding => sub { my $self = shift; my $action_name = shift; $self->$action_name(); } ); $meta->add_method( instance => sub { return $INSTANCE } ); #NOTE: Temporary, we should allow the player to chose his own bindings #$Blacknote::Input::KEYBINDING = $meta->new_object(); $meta->make_immutable; $INSTANCE = $meta->new_object; return $INSTANCE; } #NOTE: Items that are stacked inside something like a container should be considered "inside" the container # Define a window defclass WindowMenu => superclasses => [ "Blacknote::System::IWindow" ], #NOTE: Override handlers to setup a simple menu with keycode-dispatch attributes => [ qw(items menu_items x y w h title handlers) ], methods => { prepare_items => sub { # expecting items to just be text for now my $self = shift; $self->{menu_items} = {}; my @order = $self->letter_order; for my $item(@{$self->{items}}){ $self->{menu_items}->{pop @order} = $item; } }, close => sub { my $self = shift; delete $Blacknote::System::State::WINDOWS{$self->title}; bnsm_set_focused_window(undef); bnsm_change_state(STATE_GAME); }, keyboard_selection => sub { my $self = shift; my $keycode = shift; DEBUG "Window " .$self." received " . $keycode . " key"; my $handler = $self->handlers->{$keycode}; WARN "No handler for key: $keycode" and return unless $handler; $handler->(); }, open => sub { my $self = shift; $self->prepare_items; bnsm_set_focused_window($self); bnsm_change_state(STATE_MENU); $Blacknote::System::State::WINDOWS{$self->title} = $self; }, letter_order => sub { return reverse qw(a b c d e f g h i j k l m n o p r s t u v w x y z); }, }; defclass DungeonSelectionMenu => superclasses => ['Blacknote::System::WindowMenu'], methods => { handlers => sub { return { a => sub { DEBUG "DungeonSelectionMenu got the 'a' key"; croak "TODO"; } }; }, }; defclass InventoryMenu => superclasses => [ 'Blacknote::System::WindowMenu' ], attributes => [ qw(items item_data menu_items x y w h title) ], methods => { prepare_items => sub { my $self = shift; $self->{menu_items} = {}; $self->{item_data} = {}; my @order = $self->letter_order; for my $item(@{$self->{items}}){ # Items are inventory items my $letter = pop @order; $self->{menu_items}->{$letter} = $item->name; $self->{item_data}->{$letter} = $item; } }, handlers => sub { return { a => sub { DEBUG "Default dummy inventory handler" } }; }, standard_handler => sub { # Handler used for all key presses return sub { my $keycode = shift; DEBUG "standard_handler recieved key input: " . $keycode; }; }, }; defclass DropMenu => superclasses => [ 'Blacknote::System::InventoryMenu' ], methods => { handlers => sub { } }; definterface IInventory => qw (type contents size); defclass Bag => attributes => [ qw(type contents size) ], superclasses => [ "Blacknote::System::IInventory" ], methods => { add => sub { my $self = shift; push @{$self->{contents}},@_; }, }; # Define player defclass Player => superclasses => [ 'Blacknote::System::IDrawable' ], methods => { symbol => sub { return '@' }, turn => sub { # Turn is different from `update' in that it gets processed BEFORE # an SM cycle. # We put what we need in the TURN_QUEUE there my $self = shift; if(defined $self->autoturn){ bnsm_push_turn { if(not $self->disrupted){ $self->autoturn->(); }else{ $self->clear_autoturn(); } }; } }, update => sub { my $self = shift; my $coords = $self->x . "," . $self->y; my $items = $Blacknote::System::State::MAPITEMS{$coords}; if(defined $items){ bnsm_get_log->push("You see " . join(",",map { $_->name } @$items). " lying on the ground"); } }, autoturn => sub { my $self = shift; $self->{autoturn} = shift // $self->{autoturn}; # autoturn is a private variable holding a subroutine that executes until # interrupted by something return $self->{autoturn}; }, clear_autoturn => sub { my $self = shift; $self->{autoturn} = undef;}, run => sub { my $self = shift; my @dir = @_; $self->disrupted(0); $self->autoturn(sub { DEBUG "Running: " . join ",",@dir; $self->move(@dir); }); }, }, attributes => [ qw( pos x y color inventory equipment disrupted turns eq_wield_slot eq_torso_slot eq_pants_slot eq_boots_slot ) ]; fulfill 'Blacknote::System::RDrawable' => qw(Blacknote::System::Player); fulfill 'Blacknote::System::RMoving' => qw(Blacknote::System::Player); # keymap # ^X is a unix notation for CTRL+X defkeymap Rouge => ( h => 'move_w', 4 => 'move_w', H => 'run_w', j => 'move_s', 2 => 'move_s', J => 'run_s', k => 'move_n', 8 => 'move_n', K => 'run_n', l => 'move_e', 6 => 'move_e', L => 'run_e', u => 'move_ne', 9 => 'move_ne', U => 'run_ne', y => 'move_nw', 7 => 'move_nw', Y => 'run_nw', b => 'move_sw', 1 => 'move_sw', B => 'run_sw', n => 'move_se', 3 => 'move_se', N => 'run_se', KEY_DOWN => 'move_s', KEY_UP => 'move_n', KEY_LEFT => 'move_w', KEY_RIGHT => 'move_e', q => 'quaf', # Quaf (drink) a liquid d => 'drop', e => 'equipment', i => 'inventory', I => 'identify', # Shows a description of an item from inventory w => 'wield', W => 'wear', T => 'take_off', x => 'look_around', s => 'search', S => 'smash', R => 'rest', # From angband, rouge had '.' for rest but here its just pause for 1 turn '.' => 'pause', g => 'pickup', # pick up item beneath player, i.e. 'Get' '^P' => 'all_messages', # Show all previous messages '^O' => 'previous_message', # Repeats last message '^X' => 'save_and_quit', ":" => "command_line", '>' => 'descend', ); #NOTE: These bindings need to have access to most of the program my $rougekeybinds = defkeybindings Rouge => ( move_n => sub { bnsm_get_player->move(0,-1); }, move_s => sub { bnsm_get_player->move(0,1); }, move_w => sub { bnsm_get_player->move(-1,0); }, move_e => sub { bnsm_get_player->move(1,0); }, move_ne => sub { bnsm_get_player->move(1,-1); }, move_nw => sub { bnsm_get_player->move(-1,-1); }, move_sw => sub { bnsm_get_player->move(-1,1); }, move_se => sub { bnsm_get_player->move(1,1); }, run_w => sub { bnsm_get_player->run(-1,0); }, run_e => sub { bnsm_get_player->run(1,0); }, run_s => sub { bnsm_get_player->run(0,1); }, run_n => sub{ bnsm_get_player->run(0,-1); }, run_nw => sub { bnsm_get_player->run(-1,-1); }, run_ne => sub { bnsm_get_player->run(1,-1); }, run_sw => sub{ bnsm_get_player->run(-1,1); }, run_se => sub{ bnsm_get_player->run(1,1); }, all_messages => sub { DEBUG "Ctrl+P pressed"; }, previous_message => sub { DEBUG "Showing previous message in log"; }, inventory => sub { my $newwindow = Blacknote::System::InventoryMenu->new( items => bnsm_get_player->inventory()->contents(), x => 0, y => 0, w => 10, h => 10, title=>'Inventory', ); $newwindow->open; play_chunk(bnsm_get_snd_chunk("mechanical")) }, interact => sub {}, activate => sub {}, quaf => sub {}, drop => sub {}, search => sub {}, rest => sub {}, look_around => sub {}, pause => sub { DEBUG "Resting for 1 turn"; bnsm_push_turn { bnsm_get_log->push("You pause for a bit"); bnsm_get_player->consume_turns(1); }; }, enter_confirm => sub{}, examine => sub{}, smash => sub{}, character_screen => sub{}, action => sub{}, pickup => sub { my $player = bnsm_get_player; my $coords = $player->x . "," . $player->y; my $items = $Blacknote::System::State::MAPITEMS{$coords}; if(defined $items){ $player->inventory->add(@$items); delete $Blacknote::System::State::MAPITEMS{$coords}; bnsm_get_log->push("Picked up: ". join ", ", map { $_->name } @$items); } }, command_line => sub { use Blacknote::System::Shell; bnsh_open; }, descend => sub { my $tile = bnsm_get_player->tile_at_current_pos; if($tile->isa("Blacknote::Tiles::Tile::StairDownSpecial")){ my $dunwin = Blacknote::System::DungeonSelectionMenu->new( items => [qw(Dungeon1 Dungeon2 Dungeon3)], x => 0, y =>0, w => 10, h => 4, title => "Go where?" ); $dunwin->open; }else{ if($tile->isa("Blacknote::Tiles::Tile::StairDown")){ DEBUG "TODO: Unimplemented yet"; }else{ bnsm_get_log->push("You see no stairs you could use to descend"); } } } ); INIT { # Before runtime, we initialize all data we expect to be initialized # Preload common sounds Blacknote::Sound::open_audio; # Since this INIT block runs before any execution, we initialize the audio here instead of blacknote bnsm_add_snd_chunk("mechanical",load_soundfile("res/snd/keyenter.wav")); # Initialize player my $player = Blacknote::System::Player->new( x=>0, y=>0, pos=>[0,0], inventory => Blacknote::System::Bag->new(type => 1, contents => [], size => 10), ); bnsm_set_player($player); $player->x(20); $player->y(7); # Run other init hook code bnsm_run_hooks "init"; # Advance state to GAME bnsm_change_state(STATE_GAME); } bnsm_set_active_keybinding($rougekeybinds); TRACE "System module 'use'd"; 1; .