The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# Games::Construder - A 3D Game written in Perl with an infinite and modifiable world.
# Copyright (C) 2011  Robin Redeker
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
package Games::Construder::Server::Player;
#d#use Devel::FindRef;
use common::sense;
use AnyEvent;
use Games::Construder::Server::World;
use Games::Construder::Server::UI;
use Games::Construder::Server::Objects;
use Games::Construder::Server::PatStorHandle;
use Games::Construder::Logging;
use Games::Construder::Vector;
use Time::HiRes qw/time/;
use base qw/Object::Event/;
use Scalar::Util qw/weaken/;
use Compress::LZF;

=head1 NAME

Games::Construder::Server::Player - Implementation of Player central Game Mechanics (the heart of the game)

=over 4

=cut

my $PL_VIS_RAD = 3;
my $PL_MAX_INV = 24;
my $PL_MAX_QUEUE_SIZE = 100; # max 100 chunks

sub new {
   my $this  = shift;
   my $class = ref ($this) || $this;
   my $self  = { @_ };
   bless $self, $class;

   $self->init_object_events;

   return $self
}

sub _check_file {
   my ($self) = @_;
   my $pld = $Games::Construder::Server::Resources::PLAYERDIR;
   my $file = "$pld/$self->{name}.json";
   return unless -e "$file";

   if (open my $plf, "<", $file) {
      binmode $plf, ":raw";
      my $cont = do { local $/; <$plf> };
      my $data = eval { JSON->new->relaxed->utf8->decode ($cont) };
      if ($@) {
         ctr_log (error => "Couldn't parse player data from file '%s': %s", $file, $!);
         return;
      }

      return $data

   } else {
      ctr_log (error => "Couldn't open player file '%s': %s", $file, $!);
      return;
   }
}

sub _initialize_player {
   my ($self) = @_;
   my $inv = $Games::Construder::Server::RES->get_initial_inventory;
   my $data = {
      name      => $self->{name},
      happyness => 100,
      bio       => 100,
      score     => 0,
 #     pos       => [1.1 * 60, -29 * 60, 1.1 * 60],
      pos       => [
         map { 60 * $_ } $Games::Construder::Server::RES->get_initial_position
      ],
      time      => 0,
      inv       => $inv,
      next_encounter => 15 * 60, # 15 minutes newbie safety
      slots => {
         selection => [],
         selected  => 0
      },
   };

   $data
}

sub load {
   my ($self) = @_;

   my $data = $self->_check_file;
   unless (defined $data) {
      $data = $self->_initialize_player;
   }

   $self->{data} = $data;
}

sub save {
   my ($self) = @_;
   my $cont = JSON->new->pretty->utf8->encode ($self->{data});
   my $pld = $Games::Construder::Server::Resources::PLAYERDIR;
   my $file = "$pld/$self->{name}.json";

   if (open my $plf, ">", "$file~") {
      binmode $plf, ":raw";
      print $plf $cont;
      close $plf;

      if (-s "$file~" != length ($cont)) {
         ctr_log (error => "Couldn't write out player file completely to '%s': %s", $file, $!);
         return;
      }

      unless (rename "$file~", "$file") {
         ctr_log (error => "Couldn't rename $file~ to $file: $!");;
         return;
      }

      ctr_log (info => "Saved player %s to %s", $self->{name}, $file);

   } else {
      ctr_log (error => "Couldn't open player file $file~ for writing: $!");
      return;
   }
}

sub init {
   my ($self) = @_;
   $self->load;
   $self->save;
   $self->set_vis_rad;

   my $wself = $self;
   weaken $wself;
   my $tick_time = time;
   my $msg_beacon_upd = 0;
   my $save_tmr = 0;

   $self->{tick_timer} = AE::timer 0.25, 0.25, sub {
      my $cur = time;
      my $dt = $cur - $tick_time;

      $msg_beacon_upd += $dt;
      if ($msg_beacon_upd > 2)
         {
            $msg_beacon_upd = 0;
            $self->check_message_beacons;

            $self->check_assignment_offers (2);

            $self->check_signal_jamming;
         }

      $wself->player_tick ($dt);
      $tick_time = $cur;
      $save_tmr       += $dt;
      $self->{data}->{time} += $dt;

      if (defined $self->{data}->{next_encounter}) {
         $self->{data}->{next_encounter} -= $dt;
         #d# warn "next encounter in $self->{data}->{next_encounter} seconds!\n";
         if ($self->{data}->{next_encounter} <= 0) {
            $self->create_encounter;
         }
      }

      if ($save_tmr >= 30)
         {
            $save_tmr = 0;
            $self->save;
         }
   };

   $self->new_ui (bio_warning   => "Games::Construder::Server::UI::BioWarning");
   $self->new_ui (msgbox        => "Games::Construder::Server::UI::MsgBox");
   $self->new_ui (score         => "Games::Construder::Server::UI::Score");
   $self->new_ui (slots         => "Games::Construder::Server::UI::Slots");
   $self->new_ui (status        => "Games::Construder::Server::UI::Status");
   $self->new_ui (server_info   => "Games::Construder::Server::UI::ServerInfo");
   $self->new_ui (material_view => "Games::Construder::Server::UI::MaterialView");
   $self->new_ui (inventory     => "Games::Construder::Server::UI::Inventory");
   $self->new_ui (cheat         => "Games::Construder::Server::UI::Cheat");
   $self->new_ui (sector_finder => "Games::Construder::Server::UI::SectorFinder");
   $self->new_ui (navigator     => "Games::Construder::Server::UI::Navigator");
   $self->new_ui (navigation_programmer
                                => "Games::Construder::Server::UI::NavigationProgrammer");
   $self->new_ui (assignment      => "Games::Construder::Server::UI::Assignment");
   $self->new_ui (assignment_time => "Games::Construder::Server::UI::AssignmentTime");
   $self->new_ui (pattern_storage => "Games::Construder::Server::UI::PatternStorage");
   $self->new_ui (material_handbook => "Games::Construder::Server::UI::MaterialHandbook");
   $self->new_ui (notebook      => "Games::Construder::Server::UI::Notebook");
   $self->new_ui (msg_beacon    => "Games::Construder::Server::UI::MessageBeacon");
   $self->new_ui (msg_beacon_list => "Games::Construder::Server::UI::MessageBeaconList");
   $self->new_ui (teleporter    => "Games::Construder::Server::UI::Teleporter");
   $self->new_ui (color_select  => "Games::Construder::Server::UI::ColorSelector");
   $self->new_ui (ship_transmission => "Games::Construder::Server::UI::ShipTransmission");
   $self->new_ui (prox_warn     => "Games::Construder::Server::UI::ProximityWarning");
   $self->new_ui (text_script   => "Games::Construder::Server::UI::TextScript");
   $self->new_ui (trophies      => "Games::Construder::Server::UI::Trophies");
   $self->new_ui (help          => "Games::Construder::Server::UI::Help");
   $self->new_ui (pcb_prog      => "Games::Construder::Server::UI::PCBProg");
   $self->new_ui (jumper        => "Games::Construder::Server::UI::Jumper");

   $self->{inv} =
      Games::Construder::Server::PatStorHandle->new (data => $self->{data}, slot_cnt => $PL_MAX_INV);

   $self->{inv}->reg_cb (changed => sub {
      if ($wself->{uis}->{inventory}->{shown}) {
         $wself->{uis}->{inventory}->show;
      }

      $wself->{uis}->{slots}->show;
   });

   $self->update_score;
   $self->{uis}->{slots}->show;
   $self->teleport ();
   $self->check_assignment;
}

sub now { $_[0]->{data}->{time} }

sub push_tick_change {
   my ($self, $key, $amt) = @_;
   push @{$self->{tick_changes}}, [$key, $amt];
}

sub player_tick {
   my ($self, $dt) = @_;

   my $player_values = $Games::Construder::Server::RES->player_values ();

   while (@{$self->{tick_changes}}) {
      my ($k, $a) = @{shift @{$self->{tick_changes}}};

      if ($k eq 'happyness' || $k eq 'bio') {
         $self->{data}->{$k} += $a;

         if ($self->{data}->{$k} > 100) {
            $self->{data}->{$k} = 100;
         }

         if ($k eq 'bio' && -$a) {
            $self->{uis}->{status}->show (-$a);
         }

      } elsif ($k eq 'score') {
         my $happy = $Games::Construder::Server::RES->score2happyness ($a);
         $self->{data}->{happyness} += int ($happy + 0.5);

         if ($self->{data}->{happyness} < 90) {
            $a = 0;
         } elsif ($self->{data}->{happyness} > 100) {
            $self->{data}->{happyness} = 100;
         }

         if ($a) {
            $self->update_score ($a);
            my $old = $self->{data}->{score};
            $self->{data}->{score} += $a;
            $self->{data}->{score} = int $self->{data}->{score};

            $self->add_trophies ($old);
         }
      }  elsif ($k eq 'score_punishment') {
         $self->update_score (-$a);
         $self->{data}->{score} -= $a;
         $self->{data}->{score} = 0 if $self->{data}->{score} < 0;
      }
   }

   my $bio_rate;

   $self->{data}->{happyness} -= $dt * $player_values->{unhappy_rate};
   if ($self->{data}->{happyness} < 0) {
      $self->{data}->{happyness} = 0;
      $bio_rate = $player_values->{bio_unhappy};

   } elsif ($self->{data}->{happyness} > 0) {
      $bio_rate = $player_values->{bio_happy};
   }

   $self->{data}->{bio} -= $dt * $bio_rate;
   if ($self->{data}->{bio} <= 0) {
      $self->{data}->{bio} = 0;

      if (!$self->try_eat_something) { # danger: this maybe recurses into player_tick :)
         $self->starvation (1);
      }
   } else {
      $self->starvation (0);
   }

   my $hunger = 100 - $self->{data}->{bio};
   $self->try_eat_something ($hunger);
}

sub starvation {
   my ($self, $starves) = @_;

   my $bio_ui = $self->{uis}->{bio_warning};

   if ($starves) {
      unless ($self->{death_timer}) {
         my $cnt = 30;
         $self->{death_timer} = AE::timer 0, 1, sub {
            if ($cnt-- <= 0) {
               $self->kill_player ("starvation");
               delete $self->{death_timer};

               $bio_ui->hide;
            } else {
               $bio_ui->show ($cnt);
            }
         };
      }

   } else {
      if (delete $self->{death_timer}) {
         $bio_ui->hide;
      }
   }
}

sub try_eat_something {
   my ($self, $amount) = @_;

   my $max_e = $self->{inv}->max_bio_energy_material;

   return 0 unless $max_e;

   my $item = $max_e;
   if ($amount) {
      if ($item->[1] <= $amount) {
         my ($ov) = $self->{inv}->remove ($item->[0]);
         if ($ov) {
            $self->{uis}->{status}->show ([@$item]);
            $self->refill_bio ($item->[1]);
            return 1;
         }
      }

   } else {
      my ($ov) = $self->{inv}->remove ($item->[0]);
      if ($ov) {
         $self->{uis}->{status}->show ([@$item]);
         $self->refill_bio ($item->[1]);
         return 1;
      }
   }

   return 0;
}

sub refill_bio {
   my ($self, $amount) = @_;

   $self->{data}->{bio} += $amount;
   $self->{data}->{bio} = 100
      if $self->{data}->{bio} > 100;

   if ($self->{data}->{bio} > 0) {
      $self->starvation (0); # make sure we don't starve anymore
   }
}

sub kill_player {
   my ($self, $reason) = @_;
   my $pp = $self->{data}->{pos};
   my ($new_pos, $dist, $secdist) = world_find_random_teleport_destination_at_dist ($pp, 780);
   $self->teleport ($new_pos);
   $self->msg (1, "You died of $reason, your stats and inventory were reset and you have been teleported $secdist sectors away!");
   $self->{inv}->remove ('all');
   my $inv = $Games::Construder::Server::RES->get_initial_inventory;
   $self->{data}->{inv}->{$_} = $inv->{$_} for keys %$inv;
   $self->{data}->{happyness} = 100;
   $self->{data}->{bio}       = 100;
   $self->{data}->{score}     = 0;
   $self->update_score;
}

sub logout {
   my ($self) = @_;
   $self->save;
   delete $self->{uis};
   delete $self->{upd_score_hl_tmout};
   delete $self->{death_timer};
   delete $self->{tick_timer};

   ctr_log (info => "Player %s logged out", $self->{name});
   #d# print Devel::FindRef::track $self;
}

my $world_c = 0;

sub update_pos {
   my ($self, $pos, $lv) = @_;

   if ($self->{freeze_update_pos} ne '') {
      ctr_log (debug => "update_pos thrown away, awaiting teleport confirmation!");
      return;
   }

   my $opos = $self->{data}->{pos};
   $self->{data}->{pos} = $pos;
   my $olv = $self->{data}->{look_vec} || [0,0,0];
   $self->{data}->{look_vec} = vnorm ($lv);

   my $oblk = vfloor ($opos);
   my $nblk = vfloor ($pos);

   my $new_pos = vlength (vsub ($oblk, $nblk)) > 0;
   my $new_lv  = vlength (vsub ($olv, $lv)) > 0.05;
   my $dnew_lv = vlength (vsub ($olv, $lv));

   if ($new_pos || $new_lv) {
      if ($self->{uis}->{navigator}->{shown}) {
         $self->{uis}->{navigator}->show;
      }
   }

   return unless $new_pos;

   $self->upd_visible_chunks;
}

sub upd_visible_chunks {
   my ($self) = @_;
   $self->calc_visible_sectors;
   world_load_at_player ($self, sub { });
}

sub get_pos_normalized {
   my ($self) = @_;
   vfloor ($self->{data}->{pos})
}

sub get_pos_chnk {
   my ($self) = @_;
   world_pos2chnkpos ($self->{data}->{pos})
}

sub get_pos_sector {
   my ($self) = @_;
   world_chnkpos2secpos (world_pos2chnkpos ($self->{data}->{pos}))
}

sub set_vis_rad {
   my ($self, $rad) = @_;
   $self->{vis_rad} = $rad || $PL_VIS_RAD;
}

sub set_visible_chunks {
   my ($self, $new, $old, $req) = @_;

   for (@$old) {
      my $id = world_pos2id ($_);
      delete $self->{visible_chunk_ids}->{$id};
   }

   for (@$new) {
      my $id = world_pos2id ($_);
      $self->{visible_chunk_ids}->{$id} = $_;
   }

   for (@{$req || []}) {
      $self->{to_send_chunks}->{world_pos2id ($_)} = $_;
   }

}

sub calc_visible_sectors {
   my ($self) = @_;

   my $pos = $self->{data}->{pos};

   delete $self->{visible_sectors};

   # only load at max the 8 adjacent sectors! this means, visible_chunk*
   # overdraws and just says that those chunks are visible...
   my $plchnk = world_pos2chnkpos ($pos);
   for my $x (-2, 0, 2) {
      for my $y (-2, 0, 2) {
         for my $z (-2, 0, 2) {
            my $id = world_pos2id (world_chnkpos2secpos (vaddd ($plchnk, $x, $y, $z)));
            $self->{visible_sectors}->{$id} = 1;
         }
      }
   }
}

sub chunk_updated {
   my ($self, $chnk) = @_;
   my $id = world_pos2id ($chnk);

   #d#warn "TEST[$id] vs [" . join (", ", keys %{$self->{visible_chunk_ids}}) . "]\n";

   if ($self->{visible_chunk_ids}->{$id}) {
      $self->{to_send_chunks}->{$id} = $chnk;

   } else {
      if ($self->{sent_chunks}->{$id}) {
         $self->send_client ({ cmd => "dirty_chunks", chnks => [$chnk] });
         delete $self->{sent_chunks}->{$id};
      }
   }
   #delete $self->{chunk_uptodate}->{world_pos2id ($chnk)};
}

sub push_chunk_to_network {
   my ($self) = @_;

   my (@upds) = values %{$self->{to_send_chunks}};

   my $plpos = $self->{data}->{pos};
   my $plchnk = world_pos2chnkpos ($self->{data}->{pos});
   (@upds) = grep {
      $self->{visible_chunk_ids}->{world_pos2id ($_)}
   } @upds;
   (@upds) = sort {
      vlength (vsub ($plchnk, $a))
      <=>
      vlength (vsub ($plchnk, $b))
   } @upds;

   splice @upds, $PL_MAX_QUEUE_SIZE;

   my $cnt = scalar @upds;
   ctr_log (debug => "player %s: %d chunk updates in queue", $self->{name}, $cnt)
      if $cnt;
   for (my $i = 0; $i < 5; $i++) {
      my $q = shift @upds
         or return;
      $self->send_chunk ($q);
   }
}

sub send_chunk {
   my ($self, $chnk) = @_;

   # only send chunk when allcoated, in all other cases the chunk will
   # be sent by the chunk_changed-callback by the server (when it checks
   # whether any player might be interested in that chunk).
   my $id = world_pos2id ($chnk);
   my $data = Games::Construder::World::get_chunk_data (@$chnk);
   unless (defined $data) {
      #d# warn "send_chunk: @$chnk was not yet allocated!\n";
      delete $self->{to_send_chunks}->{$id};
      return;
   }

   my $buf = compress ($data);

   $self->send_client ({ cmd => "chunk", pos => $chnk }, \$buf);
   $self->{sent_chunks}->{$id} = $chnk;
   delete $self->{to_send_chunks}->{$id};
}

sub msg {
   my ($self, $error, $msg) = @_;
   $self->{uis}->{msgbox}->show ($error, $msg);
}

sub update_score {
   my ($self, $hl) = @_;
   $self->{uis}->{score}->show ($hl);
}

sub query {
   my ($self, $pos) = @_;
   return unless @$pos;

   world_mutate_at ($pos, sub {
      my ($data) = @_;
      if ($data->[0]) {
         $self->{uis}->{material_view}->show ($data->[0], $data->[5]);
      }
      return 0;
   }, need_entity => 1);
}

sub interact {
   my ($self, $pos) = @_;

   world_at ($pos, sub {
      my ($pos, $cell) = @_;
      Games::Construder::Server::Objects::interact ($self, $pos, $cell->[0], $cell->[5]);
   });
}

sub highlight {
   my ($self, $pos, $time, $color) = @_;
   $self->send_client ({
      cmd   => "highlight",
      pos   => $pos,
      color => $color,
      fade  => -$time
   });
}

sub debug_at {
   my ($self, $pos) = @_;
   $self->send_client ({
      cmd => "model_highlight",
      pos => $pos,
      model => [
         map {
            my $x = $_;
            map {
               my $y = $_;
               map { [[$x, $y, $_], [1, 0, rand (100) / 100, 0.2]] } 0..10
            } 0..10
         } 0..10
      ],
      id => "debug"
   });
   world_mutate_at ($pos, sub {
      my ($data) = @_;
      ctr_log (debug => "player %s: debug position @$pos: @$data", $self->{name});
      if ($data->[0] == 1) {
         $data->[0] = 0;
         return 1;
      }
      return 0;
   });
}

sub get_selected_slot {
   my ($self) = @_;
   my $id = $self->{data}->{slots}->{selection}->[$self->{data}->{slots}->{selected}];
   my ($type, $entid) = split /:/, $id, 2;
   if ($entid ne '') {
      return ($type, $id)
   } else {
      return ($id, $id);
   }
}

sub start_materialize {
   my ($self, $pos) = @_;

   my ($invid)
      = $self->{data}->{slots}->{selection}->[$self->{data}->{slots}->{selected}];
   my ($type, $invid) = $self->{inv}->split_invid ($invid);

   world_mutate_at ($pos, sub {
      my ($data) = @_;

      return 0 unless $data->[0] == 0;

      my $obj = $Games::Construder::Server::RES->get_object_by_type ($type);
      my ($time, $energy, $score) =
         $Games::Construder::Server::RES->get_type_materialize_values (
            $type, $self->has_matter_transformer_upgrade);
      unless ($self->{data}->{bio} >= $energy) {
         $self->msg (1, "You don't have enough energy to materialize the $obj->{name}!");
         return;
      }

      my ($cnt, $ent) = $self->{inv}->remove ($invid);

      return 0 unless $cnt;

      $data->[0] = 1;
      $data->[5] =
         Games::Construder::Server::Objects::instance (
            1, $time, materialize =>
                        m_type     => $type,
                        m_type_ent => $ent,
                        color      => $self->{colorifyer},
                        player     => $self->{name},
                        score      => $score,
         );

      $self->highlight ($pos, $time, [0, 1, 0]);
      $self->push_tick_change (bio => -$energy);
      return 1;
   }, no_light => 1);
}

sub has_matter_transformer_upgrade {
   my ($self) = @_;
   $self->{inv}->has (70);
}

sub start_dematerialize {
   my ($self, $pos) = @_;

   world_mutate_at ($pos, sub {
      my ($data) = @_;
      my $type = $data->[0];
      my $obj = $Games::Construder::Server::RES->get_object_by_type ($type);
      if ($obj->{untransformable}) {
         return;
      }

      unless ($self->{inv}->has_space_for ($type)) {
         $self->msg (1, "Inventory full, no space for $obj->{name} available!");
         return;
      }

      my ($time, $energy) =
         $Games::Construder::Server::RES->get_type_dematerialize_values (
            $type, $self->has_matter_transformer_upgrade);

      unless ($obj->{bio_energy} || $self->{data}->{bio} >= $energy) {
         $self->msg (1, "You don't have enough energy to dematerialize the $obj->{name}!");
         return;
      }

      $data->[0] = 1; # materialization!
      my $ent = $data->[5];
      $data->[5] =
         Games::Construder::Server::Objects::instance (
            1, $time, dematerialize =>
                        m_type     => $type,
                        m_type_ent => $ent,
                        player     => $self->{name},
         );

      $self->highlight ($pos, $time, [1, 0, 0]);
      $self->push_tick_change (bio => -$energy);
      return 1;
   }, no_light => 1, need_entity => 1);
}

sub _map_value_to_material {
   my ($map, $val) = @_;
   for (@$map) {
      my ($a, $b, $t) = @$_;
      return $t if $val >= $a && $val < $b;
   }
   undef
}

sub check_message_beacons {
   my ($self) = @_;

   my $msgboxes =
      Games::Construder::World::get_types_in_cube (
         @{vsubd ($self->get_pos_normalized, 15, 15, 15)}, 30, 34);

   my $cur_beacons = {};
   while (@$msgboxes) {
      my ($pos, $type) = (
         [shift @$msgboxes, shift @$msgboxes, shift @$msgboxes], shift @$msgboxes
      );
      my $e = world_entity_at ($pos);
      my $id = world_pos2id ($pos);
      $cur_beacons->{$id} = $self->{data}->{beacons}->{$id} = [
         $pos, $e->{msg}, $self->now
      ];
   }

   my $now = $self->now;

   for (keys %{$self->{data}->{beacons}}) {
      if (($now - $self->{data}->{beacons}->{$_}->[2]) > 30 * 60) {
         delete $self->{data}->{beacons}->{$_};
      }
   }

   $self->{uis}->{msg_beacon_list}->show ($cur_beacons);
}

our @OFFER_TIMES = (
   5  * 60,
   7  * 60,
   10 * 60,
   15 * 60,
   20 * 60,
);

our @DIFF_PUNSH_FACT = (
   0.05,
   0.1,
   0.2,
   0.5,
   0.7,
   0.9
);

sub check_assignment_offers {
   my ($self, $dt) = @_;
;
   for (my $d = 0; $d < 5; $d++) {
      my $offer = $self->{data}->{offers}->[$d];
      if ($offer) {
         $offer->{offer_time} -= $dt;
         if ($offer->{offer_time} <= 0) {
            undef $offer;
         }
      }

      unless ($offer) {
         my ($desc, $size, $material_map, $distance, $time, $score) =
            $Games::Construder::Server::RES->get_assignment_for_score (
               $self->{data}->{score}, $d * 2);
         $offer = {
            diff         => $d,
            cmds         => $desc,
            size         => $size,
            material_map => $material_map,
            distance     => $distance,
            time         => $time,
            score        => $score,
            offer_time   => $OFFER_TIMES[$d],
            punishment   => int ($DIFF_PUNSH_FACT[$d] * $score),
         };
      }

      $self->{data}->{offers}->[$d] = $offer;
   }

   if ($self->{uis}->{assignment}->{shown}) {
      $self->{uis}->{assignment}->show;
   }
}

sub take_assignment {
   my ($self, $nr) = @_;

   my $offer = $self->{data}->{offers}->[$nr];
   $self->{data}->{offers}->[$nr] = undef;

   #my ($desc, $size, $material_map, $distance, $time, $score) =
   #   $Games::Construder::Server::RES->get_assignment_for_score ($self->{data}->{score});

   #print "ASSIGNMENT BASE VALUES: " . JSON->new->pretty->encode ([
   #   $desc, $size, $material_map, $distance, $time, $score
   #]) . "\n";

   my $vec  = vsmul (vnorm (vrand ()), $offer->{distance});
   my $wpos = vfloor (vadd ($vec, $self->get_pos_normalized));

   ctr_log (debug => "took assignment at @$vec => @$wpos");

   my $size = $offer->{size};
   Games::Construder::VolDraw::alloc ($size);

   Games::Construder::VolDraw::draw_commands (
     $offer->{cmds},
     { size => $size, seed => $offer->{score}, param => 1 }
   );

   my $cube = Games::Construder::VolDraw::to_perl ();
   shift @$cube;

   my $material_map = $offer->{material_map};
   my $materials = {};
   my $positions = {};

   for (my $x = 0; $x < $size; $x++) {
      for (my $y = 0; $y < $size; $y++) {
         for (my $z = 0; $z < $size; $z++) {
            my $val = shift @$cube;
            my $type = _map_value_to_material ($material_map, $val);
            next unless defined $type;
            my $model = ($materials->{$type} ||= []);

            my $pos = [$x, $y, $z];
            push @$model, [$pos, [1, 0, 1, 0.2]];

            my $id = join ",", @{vadd ($pos, $wpos)};
            $positions->{$id} = $type;
         }
      }
   }

   my $cal = $self->{data}->{assignment} = $offer;
   $cal->{pos}        = $wpos;
   $cal->{materials}  = [sort keys %$materials];
   $cal->{sel_mat}    = $cal->{materials}->[0];
   $cal->{pos_types}  = $positions;
   $cal->{mat_models} = $materials;

   $self->{uis}->{assignment}->show;

   delete $self->{assign_ment_hl};
   $self->check_assignment;
}

sub check_assignment_positions {
   my ($self) = @_;

   my $t      = time;
   my $assign = $self->{data}->{assignment};
   my $lpos   = { %{$assign->{pos_types}} };
   my $typ    =
      Games::Construder::World::get_types_in_cube (@{$assign->{pos}}, $assign->{size});

   #d#printf "CHECK TIME 1 %f\n", time - $t;

   for (my $x = 0; $x < $assign->{size}; $x++) {
      for (my $y = 0; $y < $assign->{size}; $y++) {
         for (my $z = 0; $z < $assign->{size}; $z++) {
            my $t = shift @$typ;
            my $pid = join (",", @{vaddd ($assign->{pos}, $x, $y, $z)});

            if ($assign->{pos_types}->{$pid}) {
               if ($assign->{pos_types}->{$pid} == $t) {
                  delete $lpos->{$pid};
               }
            }
         }
      }
   }

   my %tleft;
   for (keys %$lpos) {
      $tleft{$lpos->{$_}}++;
   }
   $assign->{left} = \%tleft;

   #d#printf "CHECK TIME 2 %f\n", time - $t;

   $self->update_assignment_highlight;

   unless (grep { $_ != 0 } values %tleft) {
      $self->finished_assignmenet;
   }
}

sub assignment_select_next {
   my ($self) = @_;
   my $assign = $self->{data}->{assignment};
   my @left = grep {
      $assign->{left}->{$_} > 0
   } keys %{$assign->{left}};

   push @left, @left;
   for (my $i = 0; $i < (@left / 2); $i++) {
      if ($left[$i] == $assign->{sel_mat}) {
         $assign->{sel_mat} = $left[$i + 1];
         last;
      }
   }

   delete $self->{assign_ment_hl};
   $self->update_assignment_highlight;
   $self->{uis}->{assignment_time}->show;
}

sub update_assignment_highlight {
   my ($self) = @_;

   my $assign = $self->{data}->{assignment};
   my $selected = $assign->{sel_mat};
   if ($assign->{left}->{$selected} <= 0) {
      ($assign->{sel_mat}) = grep {
         $assign->{left}->{$_} > 0
      } keys %{$assign->{left}};
      delete $self->{assign_ment_hl};
   }

   unless ($self->{assign_ment_hl}) {
      $self->{assign_ment_hl} = 1;
      my $mat = $assign->{sel_mat};

      $self->send_client ({
         cmd   => "model_highlight",
         pos   => $assign->{pos},
         model => $assign->{mat_models}->{$mat},
         id    => "assignment",
      });
   }
}

sub check_assignment {
   my ($self) = @_;

   my $assign = $self->{data}->{assignment};
   unless ($assign) {
      $self->{uis}->{assignment_time}->hide;
      $self->send_client ({
         cmd => "model_highlight",
         id => "assignment"
      });
      return;
   }

   $self->check_assignment_positions;
   # was it finished?!
   return unless $self->{data}->{assignment};

   $self->{uis}->{assignment_time}->show;
   my $wself = $self;
   weaken $wself;
   $self->{assign_timer} = AE::timer 1, 1, sub {
      $wself->check_assignment_positions;
      # was it finished?!
      return unless $self->{data}->{assignment};

      $wself->{data}->{assignment}->{time} -= 1;
      $wself->{uis}->{assignment_time}->show;
      if ($wself->{data}->{assignment}->{time} <= 0) {
         $wself->cancel_assignment;
      }
   };
}

sub finished_assignmenet {
   my ($self) = @_;
   my $score = $self->{data}->{assignment}->{score};
   $self->push_tick_change (score => $score);
   $self->msg (0, "Congratulations! You finished the assignment and got $score score.");
   $self->{data}->{assignment} = undef;
   delete $self->{assign_timer};
   $self->check_assignment;
}

sub cancel_assignment {
   my ($self) = @_;
   my $ass = $self->{data}->{assignment};
   $self->push_tick_change (score_punishment => $ass->{punishment});
   $self->msg (1, "Sorry, you failed to finish the assignment. You lose $ass->{punishment} score.");
   $self->{data}->{assignment} = undef;
   delete $self->{assign_timer};
   $self->check_assignment;
}

sub create_encounter {
   my ($self) = @_;
   my $dir = [0,0,0];
   while (vlength ($dir) < 1) {
      $dir = vnorm (vrand ());
   }
   my $dist = 10 + rand (40); # hardcoded, if farther than 60, drone will not detect player
   my $pos = vsmul ($dir, $dist);
   viadd ($pos, $self->{data}->{pos});
   my $new_pos = world_find_free_spot ($pos, 0);

   my ($teledist, $nxttime, $lifetime) =
      $Games::Construder::Server::RES->encounter_values ();

   ctr_log (debug => "next encounter for player %s is %d (%d, %d)", $self->{name}, $nxttime, $teledist, $lifetime);
   $self->{data}->{next_encounter} = $nxttime;

   world_mutate_at ($new_pos, sub {
      my ($data) = @_;
      $data->[0] = 50;
      $data->[5] =
         Games::Construder::Server::Objects::instance (
            50, int ($dist * 1.5 + $lifetime), $teledist);
      return 1;
   });
}

sub check_signal_jamming {
   my ($self) = @_;
   my $jammers =
      Games::Construder::World::get_types_in_cube (
         @{vsubd ($self->get_pos_normalized, 15, 15, 15)}, 30, 33);

   my $pre = $self->{data}->{signal_jammed};
   $self->{data}->{signal_jammed} = @$jammers ? 1 : 0;
}

sub add_trophies {
   my ($self, $old_score) = @_;
   my $new_score = $self->{data}->{score};
   my $time      = $self->{data}->{time};

   my @t =
      $Games::Construder::Server::RES->generate_trophies_for_score_change (
         $old_score, $new_score, $time);

   my $new;
   for (@t) {
      next if exists $self->{data}->{trophies}->{$_->[0]};
      $self->{data}->{trophies}->{$_->[0]} = $_;
      $new++;
   }
   if ($new) {
      $self->msg (0, "Congratulations, you gained $new new trophies!");
   }
}


sub send_client : event_cb {
   my ($self, $hdr, $body) = @_;
}

sub teleport {
   my ($self, $pos, $in_air) = @_;

   $pos ||= $self->{data}->{pos};
   $self->msg (0, "Teleport in progress, please wait...");
   world_load_around_at ($pos, sub {
      my $new_pos = world_find_free_spot ($pos, $in_air ? 0 : 1);
      unless (@$new_pos) {
         $new_pos = world_find_free_spot ($pos, 0); # without floor on second try
      }

      unless (@$new_pos) {
         ctr_log (error => "new position for player at @$pos had no free spot! moving him up!");
         viaddd ($pos, 0, 10, 0);
         my $t; $t = AE::timer 0, 0, sub {
            $self->teleport ($pos);
            undef $t;
         };
      }

      $new_pos = vaddd ($new_pos, 0.5, 0.5, 0.5);
      my $fid = "$new_pos";
      $self->{data}->{pos} = $new_pos;
      $self->{freeze_update_pos} = $fid;
      $self->upd_visible_chunks;
      $self->send_client ({ cmd => "place_player", pos => $new_pos, id => $fid });
   });
}

sub unfreeze_update_pos {
   my ($self, $id) = @_;
   if ($self->{freeze_update_pos} eq $id) {
      delete $self->{freeze_update_pos};
   }
}

sub new_ui {
   my ($self, $id, $class, %arg) = @_;
   my $o = $class->new (ui_name => $id, pl => $self, %arg);
   $self->{uis}->{$id} = $o;
}

sub delete_ui {
   my ($self, $id) = @_;
   delete $self->{uis}->{$id};
}

sub display_ui {
   my ($self, $id, $dest) = @_;

   my $o = $self->{uis}->{$id};

   unless ($dest) {
      $self->send_client ({ cmd => deactivate_ui => ui => $id });
      delete $o->{shown};
      return;
   }

   $self->send_client ({ cmd => activate_ui => ui => $id, desc => $dest });
   $o->{shown} = 1;
}

sub ui_res : event_cb {
   my ($self, $ui, $cmd, $arg, $pos) = @_;

   ctr_log (debug => "player %s: ui response %s: %s (%s) (@$pos)", $ui, $cmd, $arg);

   if (my $o = $self->{uis}->{$ui}) {
      $o->react ($cmd, $arg, $pos);

      delete $o->{shown}
         if $cmd eq 'cancel';
   }
}

sub DESTROY {
   my ($self) = @_;
   ctr_log (debug => "player %s [%s] destroyed", $self->{name}, $self);
}

=back

=head1 AUTHOR

Robin Redeker, C<< <elmex@ta-sa.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2011 Robin Redeker, all rights reserved.

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License.

=cut

1;