diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/src/server/player.lisp b/src/server/player.lisp new file mode 100644 index 0000000..712738b --- /dev/null +++ b/src/server/player.lisp @@ -0,0 +1,91 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for managing player instances. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct player + (name "") + (password "") + (online NIL) + (human NIL)) + +(defclass human (animal) + ;; A human player + ;;XXX add age? + ((experience :accessor .xp :initform 0) + (level :accessor .level :initform 0) + (dexterity :accessor .dex :initarg :dex :initform 1) + (intelligence :accessor .int :initarg :int :initform 1) + (hunger :accessor .hunger :initarg :hunger :initform 10) + (tool :accessor .tool :initarg :tool :initform NIL) + (inventory :accessor .inventory :initarg :inventory + :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) + (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) + ;;XXX change habitats to (available-biomes)? -> requires load order change + (:default-initargs :habitat T)) + +(defmethod update ((h human)) + ;;TODO + ) + +;; INVENTORY HANDLING FUNCTIONS + +;;TODO convert to methods + +(defun stock-size (resource player) + "How many items of this resource type is the player carrying?" + (dolist (i (player-inventory player) 0) + (when (eq (item-name (first i)) resource) + (return-from stock-size (second i))))) + +(defun weight-carried (player) + "Sum up the total weight of all items carried" + (+ (item-weight (player-equipment player)) + (reduce #'+ (mapcar #'(lambda (i) + (* (second i) (item-weight (first i))))) + (player-inventory player)))) + +(defun pickup (item player) + "Add the item object to the inventory" + ;;Can the item be picked up at all? + (unless (item-movable item) + (notify "This item cannot be picked up.") + (return-from pickup)) + ;;Is the player strong enough to pick this up? + (unless (<= (+ (item-weight item) (weight-carried player)) + (* (player-strength player) 20)) ;XXX magic number + (notify "You are too burdened to pick this up.") + (return-from pickup)) + (dolist (inv (player-inventory player)) + ;;Resources may be stacked + (when (and (item-resource item) + (eq (item-name item) (item-name (first inv)))) + (incf (second inv)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup)) + ;;Deposit the item in an empty slot + (when (and (null (first inv)) (item-resource item) ;normal pickup + ;;XXX replace with find-if + (zerop (count-instances (item-name item) + (mapcar #'(lambda (i) (item-name (car i))))))) + (setf inv (list item 1)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup))) + ;;If nothing has worked, the inventory is full + (notify "Your inventory is full.")) + +(defun drop (player inv-nr) + "Drop an item from the given inventory index" + ;;TODO add item back to patch + (let* ((item-entry (nth inv-nr (player-inventory player))) + (item (when item-entry (first item-entry)))) + (if (and item (> (stock-size (item-name item) 1))) + (decf (second item-entry)) + (setf item NIL (second item-entry) 0)))) + diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/src/server/player.lisp b/src/server/player.lisp new file mode 100644 index 0000000..712738b --- /dev/null +++ b/src/server/player.lisp @@ -0,0 +1,91 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for managing player instances. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct player + (name "") + (password "") + (online NIL) + (human NIL)) + +(defclass human (animal) + ;; A human player + ;;XXX add age? + ((experience :accessor .xp :initform 0) + (level :accessor .level :initform 0) + (dexterity :accessor .dex :initarg :dex :initform 1) + (intelligence :accessor .int :initarg :int :initform 1) + (hunger :accessor .hunger :initarg :hunger :initform 10) + (tool :accessor .tool :initarg :tool :initform NIL) + (inventory :accessor .inventory :initarg :inventory + :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) + (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) + ;;XXX change habitats to (available-biomes)? -> requires load order change + (:default-initargs :habitat T)) + +(defmethod update ((h human)) + ;;TODO + ) + +;; INVENTORY HANDLING FUNCTIONS + +;;TODO convert to methods + +(defun stock-size (resource player) + "How many items of this resource type is the player carrying?" + (dolist (i (player-inventory player) 0) + (when (eq (item-name (first i)) resource) + (return-from stock-size (second i))))) + +(defun weight-carried (player) + "Sum up the total weight of all items carried" + (+ (item-weight (player-equipment player)) + (reduce #'+ (mapcar #'(lambda (i) + (* (second i) (item-weight (first i))))) + (player-inventory player)))) + +(defun pickup (item player) + "Add the item object to the inventory" + ;;Can the item be picked up at all? + (unless (item-movable item) + (notify "This item cannot be picked up.") + (return-from pickup)) + ;;Is the player strong enough to pick this up? + (unless (<= (+ (item-weight item) (weight-carried player)) + (* (player-strength player) 20)) ;XXX magic number + (notify "You are too burdened to pick this up.") + (return-from pickup)) + (dolist (inv (player-inventory player)) + ;;Resources may be stacked + (when (and (item-resource item) + (eq (item-name item) (item-name (first inv)))) + (incf (second inv)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup)) + ;;Deposit the item in an empty slot + (when (and (null (first inv)) (item-resource item) ;normal pickup + ;;XXX replace with find-if + (zerop (count-instances (item-name item) + (mapcar #'(lambda (i) (item-name (car i))))))) + (setf inv (list item 1)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup))) + ;;If nothing has worked, the inventory is full + (notify "Your inventory is full.")) + +(defun drop (player inv-nr) + "Drop an item from the given inventory index" + ;;TODO add item back to patch + (let* ((item-entry (nth inv-nr (player-inventory player))) + (item (when item-entry (first item-entry)))) + (if (and item (> (stock-size (item-name item) 1))) + (decf (second item-entry)) + (setf item NIL (second item-entry) 0)))) + diff --git a/src/server/server.lisp b/src/server/server.lisp new file mode 100644 index 0000000..ae45bad --- /dev/null +++ b/src/server/server.lisp @@ -0,0 +1,284 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file stores all game data and handles the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; TODO save and load functions +;; XXX Will probably require `make-load-form-saving-slots' + +;;; PLAYER LIST + +(let ((players NIL)) + (defun reset-players () (setf players NIL)) ;;TODO logout first + + (defun get-player (name) + (first (member name players + :test #'(lambda (s p) (equalp (player-name p) s))))) + + (defun add-player (name passwd) + (let ((np (make-player :name name :password passwd :online NIL + :human (make-instance 'human + :x (random *world-size*) + :y (random *world-size*))))) + (setf players (cons np players)))) + + (defun online-players () + "Return a list of names of players who are online" + (loop for p in players + if (player-online p) + collect (player-name p))) + + (defun save-players (file-name) + ;;TODO + )) + +(defun online-p (name) + (let ((p (get-player name))) + (when p (player-online p)))) + +;;; WORLD OBJECT + +(let ((world NIL)) + (defun set-world (w) (setf world w)) + + (defun world-size () (length world)) + + (defun save-topography (file-name) ;XXX (re)move this? + "Save the world topography as a text file" + (debugging "~&Saving world to file ~A" file-name) ;debug + (with-open-file (tf file-name :direction :output) + (dolist (row world) + (format stream "~&~A~%" + (string-from-list + (mapcar #'(lambda (p) + (biome-char (patch-biome p))) + row) ""))))) + + (defun save-world (&optional (file-name "naledi.save")) + ;;TODO + (save-players file-name)) + + (defun coord (x y) + "Return the patch at the given coordinates or NIL if out of bounds" + (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) + (nth x (nth y world))))) + +;;; WORLD THREADS + +(let ((uptime 0) (world-thread NIL) (server-thread NIL) + (player-threads NIL) (running NIL)) + + (defun start-server (&optional (force T)) + "Start the game server" + ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR + ;; -> comes from not closing connections properly? + ;;XXX change force back to NIL? + (when force + (reset-server-threads) + (reset-world-age)) + (unless (or world-thread server-thread) + (init-world) + (setf running T) + (setf world-thread + (bt:make-thread #'update-loop :name "world-thread")) + (setf server-thread + (bt:make-thread #'run-server :name "server-thread")) + (logging "SERVER: initialized"))) + + (defun terminate () + ;;TODO do some error catching if the threads no longer exist + (logging "SERVER: terminating the world.") + (setf running NIL) + ;;XXX have to use destroy-thread because the server mostly idles, + ;; waiting for connections - only checks 'running' when connecting + (bt:destroy-thread server-thread) + (bt:join-thread world-thread) + (dolist (pt player-threads) + (bt:join-thread (first pt))) + (reset-server-threads) + (save-world)) ;XXX not yet implemented + + (defun age-of-the-world () uptime) + + (defun reset-world-age () (setf uptime 0)) + + (defun runningp () running) + + (defun reset-server-threads () + (set-list NIL server-thread world-thread player-threads)) + + (defun thread-player (&optional (th (bt:current-thread))) + "Return the player name associated with this thread" + (cassoc th player-threads)) + + (defun set-thread-player (name &optional (th (bt:current-thread))) + "Set the player name associated with this thread" + ;;XXX somewhat ugly... + (setf (cassoc th player-threads) name)) + + (defun cleanup-player-threads () + "Remove threads of disconnected players" + (dotimes (i (length player-threads)) + (let ((pt (nth i player-threads))) + (when (equalp (second pt) "terminated") + (bt:join-thread (first pt)) + (setf (cdr (nth (1- i) player-threads)) + (nth (1+ i) player-threads)))))) + + (defun update-loop () + "The main loop, updating the world in the background" + ;;XXX split this up into two or more functions, to be run by + ;; different threads? (e.g. player- & world-update threads) + (logging "UPDATE ~S" uptime) + ;;Update all items and occupants in each patch + (dotimes (y (world-size)) + (dotimes (x (world-size)) + (unless running (return-from update-loop)) + (when (patch-occupant (coord x y)) + (update (patch-occupant (coord x y)))) + (dolist (i (patch-items (coord x y))) + (update i)))) + ;;Update all items each player has + ;;TODO + ;;Do cleanup work + (when (zerop (rem uptime 20)) (cleanup-player-threads)) + ;;Save the world and start over + (save-world) ;XXX not yet implemented + (incf uptime) + (sleep (/ *framerate* 1000)) + (when running (update-loop))) ;;requires Tail-Call Optimization + + (defun run-server () + "Start a server, listening for connections" + (usocket:with-socket-listener (socket "127.0.0.1" *port*) + (while running + (usocket:wait-for-input socket) + (when (eq (usocket:socket-state socket) ':read) + ;;XXX give player threads unique names again? + (let ((thread (bt:make-thread + #'(lambda () + (handle-connection socket)) + :name "player-thread"))) + (setf player-threads + (cons (list thread "anon") player-threads)) + (sleep 1)))))) ;;give the socket a chance to connect + + (defun handle-connection (socket) + "Answer requests until the player disconnects" + (usocket:with-connected-socket (conn (usocket:socket-accept socket)) + (logging "~&SERVER: received a connection") ; cf. `get-peer-name' + (do* ((sockstr (usocket:socket-stream conn)) + (request (read-line sockstr NIL) (read-line sockstr NIL))) + ((or (not running) (null request)) (logout)) + (format sockstr "~S~%" (to-string (answer request))) + (finish-output sockstr))))) + +(defun answer (request) + "Dispatch on the functions registerd in `*API*'" + ;;XXX potential problem with reading all user-received symbols into memory + (let* ((reqelts (split-string request #\space)) + (cmd (car reqelts)) + (args (cdr reqelts))) + (logging "SERVER: received request ~S" reqelts) + (if (member cmd (keys *API*) :test #'equalp) + ;;XXX Surely there must be a way to simplify the next few lines?! + (if (and (equalp (thread-player) "anon") + (not (or (equalp cmd "login") (equalp cmd "signup")))) + "ERROR: not logged in" + (apply (cassoc cmd *API* :test #'equalp) args)) + "ERROR: unknown command"))) + +;;; COMMUNICATION FUNCTIONS + +;;NOTE: all following functions receive string arguments and must convert +;; them as needed + +(defun login (name passwd) + "Log this player in" + ;;XXX name and passwd are converted to symbols by `answer'! - not currently + (logging "SERVER: ~A is trying to log in" name) + (let ((p (get-player name))) + (cond ((not p) "ERROR: nonexistent player") + ((not (equalp passwd (player-password p))) "ERROR: bad password") + (T (setf (player-online p) T) + (setf (patch-occupant ;;TODO check for previous occupants + (coord (.x (player-human p)) (.y (player-human p)))) + (player-human p)) + (set-thread-player name) + (logging "SERVER: player ~A logged in" name) + name)))) + +(defun logout () + "Log the player out again" + (let ((p (get-player (thread-player)))) + (when p + (setf (player-online p) NIL) + (setf (patch-occupant + (coord (.x (player-human p)) (.y (player-human p)))) + NIL) ;;TODO set to "statue of " + (logging "SERVER: player ~A logged out" (thread-player)) + (set-thread-player "terminated")))) + +(defun create-player (name passwd) + "Create and log in a new player" + (if (get-player name) "ERROR: player exists" + (progn + (add-player name passwd) + (login name passwd)))) + +(defun get-map (swidth sheight) + "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" + (let* ((plr (player-human (get-player (thread-player)))) + (width (read-from-string swidth)) + (height (read-from-string sheight)) + (x0 (- (.x plr) (halve width))) + (y0 (- (.y plr) (halve height))) + (submap (make-array (list width height 2)))) + (dotimes (h height) + (dotimes (w width) + (let ((p (coord (+ w x0 1) (+ h y0 1))) + (next-char #\space) (next-col ':black)) + (if (and p (patch-occupant p)) + (setf next-char (.char (patch-occupant p)) + next-col (.color (patch-occupant p))) + (when p (setf next-char (biome-char (patch-biome p)) + next-col (biome-col (patch-biome p))))) + (setf (aref submap w h 0) next-char + (aref submap w h 1) next-col)))) + ;;FIXME arrays are pretty-printed with linebreaks, this causes a + ;; client-side error (only expecting one line) + submap)) + +(defun describe-patch (coords) + "Return a list of lines describing the patch at these coordinates." + (let ((p (coord (first coords) (second coords)))) + (list (string-upcase (biome-name (patch-biome p))) "" + (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" + (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) + (when (patch-occupant p) ;TODO players -> "$name is here." + (format NIL "There is ~A here." + (leading-vowel (.name (patch-occupant p))))) + (when (patch-items p) + (format NIL "The following items are here:~A *~A" #\newline + (string-from-list (mapcar #'.name (patch-items p))) + (format NIL "~% *")))))) + +(defun move-player (dir) + "Move a player in the given direction" + ;;TODO + ) + +(defparameter *API* + ;; An alist of API commands and their corresponding functions + ;;XXX move all listed functions to another file? + (list (list "login" #'login) + (list "signup" #'create-player) + (list "map" #'get-map) + (list "describe-patch" #'describe-patch) + (list "move" #'move-player))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/src/server/player.lisp b/src/server/player.lisp new file mode 100644 index 0000000..712738b --- /dev/null +++ b/src/server/player.lisp @@ -0,0 +1,91 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for managing player instances. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct player + (name "") + (password "") + (online NIL) + (human NIL)) + +(defclass human (animal) + ;; A human player + ;;XXX add age? + ((experience :accessor .xp :initform 0) + (level :accessor .level :initform 0) + (dexterity :accessor .dex :initarg :dex :initform 1) + (intelligence :accessor .int :initarg :int :initform 1) + (hunger :accessor .hunger :initarg :hunger :initform 10) + (tool :accessor .tool :initarg :tool :initform NIL) + (inventory :accessor .inventory :initarg :inventory + :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) + (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) + ;;XXX change habitats to (available-biomes)? -> requires load order change + (:default-initargs :habitat T)) + +(defmethod update ((h human)) + ;;TODO + ) + +;; INVENTORY HANDLING FUNCTIONS + +;;TODO convert to methods + +(defun stock-size (resource player) + "How many items of this resource type is the player carrying?" + (dolist (i (player-inventory player) 0) + (when (eq (item-name (first i)) resource) + (return-from stock-size (second i))))) + +(defun weight-carried (player) + "Sum up the total weight of all items carried" + (+ (item-weight (player-equipment player)) + (reduce #'+ (mapcar #'(lambda (i) + (* (second i) (item-weight (first i))))) + (player-inventory player)))) + +(defun pickup (item player) + "Add the item object to the inventory" + ;;Can the item be picked up at all? + (unless (item-movable item) + (notify "This item cannot be picked up.") + (return-from pickup)) + ;;Is the player strong enough to pick this up? + (unless (<= (+ (item-weight item) (weight-carried player)) + (* (player-strength player) 20)) ;XXX magic number + (notify "You are too burdened to pick this up.") + (return-from pickup)) + (dolist (inv (player-inventory player)) + ;;Resources may be stacked + (when (and (item-resource item) + (eq (item-name item) (item-name (first inv)))) + (incf (second inv)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup)) + ;;Deposit the item in an empty slot + (when (and (null (first inv)) (item-resource item) ;normal pickup + ;;XXX replace with find-if + (zerop (count-instances (item-name item) + (mapcar #'(lambda (i) (item-name (car i))))))) + (setf inv (list item 1)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup))) + ;;If nothing has worked, the inventory is full + (notify "Your inventory is full.")) + +(defun drop (player inv-nr) + "Drop an item from the given inventory index" + ;;TODO add item back to patch + (let* ((item-entry (nth inv-nr (player-inventory player))) + (item (when item-entry (first item-entry)))) + (if (and item (> (stock-size (item-name item) 1))) + (decf (second item-entry)) + (setf item NIL (second item-entry) 0)))) + diff --git a/src/server/server.lisp b/src/server/server.lisp new file mode 100644 index 0000000..ae45bad --- /dev/null +++ b/src/server/server.lisp @@ -0,0 +1,284 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file stores all game data and handles the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; TODO save and load functions +;; XXX Will probably require `make-load-form-saving-slots' + +;;; PLAYER LIST + +(let ((players NIL)) + (defun reset-players () (setf players NIL)) ;;TODO logout first + + (defun get-player (name) + (first (member name players + :test #'(lambda (s p) (equalp (player-name p) s))))) + + (defun add-player (name passwd) + (let ((np (make-player :name name :password passwd :online NIL + :human (make-instance 'human + :x (random *world-size*) + :y (random *world-size*))))) + (setf players (cons np players)))) + + (defun online-players () + "Return a list of names of players who are online" + (loop for p in players + if (player-online p) + collect (player-name p))) + + (defun save-players (file-name) + ;;TODO + )) + +(defun online-p (name) + (let ((p (get-player name))) + (when p (player-online p)))) + +;;; WORLD OBJECT + +(let ((world NIL)) + (defun set-world (w) (setf world w)) + + (defun world-size () (length world)) + + (defun save-topography (file-name) ;XXX (re)move this? + "Save the world topography as a text file" + (debugging "~&Saving world to file ~A" file-name) ;debug + (with-open-file (tf file-name :direction :output) + (dolist (row world) + (format stream "~&~A~%" + (string-from-list + (mapcar #'(lambda (p) + (biome-char (patch-biome p))) + row) ""))))) + + (defun save-world (&optional (file-name "naledi.save")) + ;;TODO + (save-players file-name)) + + (defun coord (x y) + "Return the patch at the given coordinates or NIL if out of bounds" + (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) + (nth x (nth y world))))) + +;;; WORLD THREADS + +(let ((uptime 0) (world-thread NIL) (server-thread NIL) + (player-threads NIL) (running NIL)) + + (defun start-server (&optional (force T)) + "Start the game server" + ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR + ;; -> comes from not closing connections properly? + ;;XXX change force back to NIL? + (when force + (reset-server-threads) + (reset-world-age)) + (unless (or world-thread server-thread) + (init-world) + (setf running T) + (setf world-thread + (bt:make-thread #'update-loop :name "world-thread")) + (setf server-thread + (bt:make-thread #'run-server :name "server-thread")) + (logging "SERVER: initialized"))) + + (defun terminate () + ;;TODO do some error catching if the threads no longer exist + (logging "SERVER: terminating the world.") + (setf running NIL) + ;;XXX have to use destroy-thread because the server mostly idles, + ;; waiting for connections - only checks 'running' when connecting + (bt:destroy-thread server-thread) + (bt:join-thread world-thread) + (dolist (pt player-threads) + (bt:join-thread (first pt))) + (reset-server-threads) + (save-world)) ;XXX not yet implemented + + (defun age-of-the-world () uptime) + + (defun reset-world-age () (setf uptime 0)) + + (defun runningp () running) + + (defun reset-server-threads () + (set-list NIL server-thread world-thread player-threads)) + + (defun thread-player (&optional (th (bt:current-thread))) + "Return the player name associated with this thread" + (cassoc th player-threads)) + + (defun set-thread-player (name &optional (th (bt:current-thread))) + "Set the player name associated with this thread" + ;;XXX somewhat ugly... + (setf (cassoc th player-threads) name)) + + (defun cleanup-player-threads () + "Remove threads of disconnected players" + (dotimes (i (length player-threads)) + (let ((pt (nth i player-threads))) + (when (equalp (second pt) "terminated") + (bt:join-thread (first pt)) + (setf (cdr (nth (1- i) player-threads)) + (nth (1+ i) player-threads)))))) + + (defun update-loop () + "The main loop, updating the world in the background" + ;;XXX split this up into two or more functions, to be run by + ;; different threads? (e.g. player- & world-update threads) + (logging "UPDATE ~S" uptime) + ;;Update all items and occupants in each patch + (dotimes (y (world-size)) + (dotimes (x (world-size)) + (unless running (return-from update-loop)) + (when (patch-occupant (coord x y)) + (update (patch-occupant (coord x y)))) + (dolist (i (patch-items (coord x y))) + (update i)))) + ;;Update all items each player has + ;;TODO + ;;Do cleanup work + (when (zerop (rem uptime 20)) (cleanup-player-threads)) + ;;Save the world and start over + (save-world) ;XXX not yet implemented + (incf uptime) + (sleep (/ *framerate* 1000)) + (when running (update-loop))) ;;requires Tail-Call Optimization + + (defun run-server () + "Start a server, listening for connections" + (usocket:with-socket-listener (socket "127.0.0.1" *port*) + (while running + (usocket:wait-for-input socket) + (when (eq (usocket:socket-state socket) ':read) + ;;XXX give player threads unique names again? + (let ((thread (bt:make-thread + #'(lambda () + (handle-connection socket)) + :name "player-thread"))) + (setf player-threads + (cons (list thread "anon") player-threads)) + (sleep 1)))))) ;;give the socket a chance to connect + + (defun handle-connection (socket) + "Answer requests until the player disconnects" + (usocket:with-connected-socket (conn (usocket:socket-accept socket)) + (logging "~&SERVER: received a connection") ; cf. `get-peer-name' + (do* ((sockstr (usocket:socket-stream conn)) + (request (read-line sockstr NIL) (read-line sockstr NIL))) + ((or (not running) (null request)) (logout)) + (format sockstr "~S~%" (to-string (answer request))) + (finish-output sockstr))))) + +(defun answer (request) + "Dispatch on the functions registerd in `*API*'" + ;;XXX potential problem with reading all user-received symbols into memory + (let* ((reqelts (split-string request #\space)) + (cmd (car reqelts)) + (args (cdr reqelts))) + (logging "SERVER: received request ~S" reqelts) + (if (member cmd (keys *API*) :test #'equalp) + ;;XXX Surely there must be a way to simplify the next few lines?! + (if (and (equalp (thread-player) "anon") + (not (or (equalp cmd "login") (equalp cmd "signup")))) + "ERROR: not logged in" + (apply (cassoc cmd *API* :test #'equalp) args)) + "ERROR: unknown command"))) + +;;; COMMUNICATION FUNCTIONS + +;;NOTE: all following functions receive string arguments and must convert +;; them as needed + +(defun login (name passwd) + "Log this player in" + ;;XXX name and passwd are converted to symbols by `answer'! - not currently + (logging "SERVER: ~A is trying to log in" name) + (let ((p (get-player name))) + (cond ((not p) "ERROR: nonexistent player") + ((not (equalp passwd (player-password p))) "ERROR: bad password") + (T (setf (player-online p) T) + (setf (patch-occupant ;;TODO check for previous occupants + (coord (.x (player-human p)) (.y (player-human p)))) + (player-human p)) + (set-thread-player name) + (logging "SERVER: player ~A logged in" name) + name)))) + +(defun logout () + "Log the player out again" + (let ((p (get-player (thread-player)))) + (when p + (setf (player-online p) NIL) + (setf (patch-occupant + (coord (.x (player-human p)) (.y (player-human p)))) + NIL) ;;TODO set to "statue of " + (logging "SERVER: player ~A logged out" (thread-player)) + (set-thread-player "terminated")))) + +(defun create-player (name passwd) + "Create and log in a new player" + (if (get-player name) "ERROR: player exists" + (progn + (add-player name passwd) + (login name passwd)))) + +(defun get-map (swidth sheight) + "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" + (let* ((plr (player-human (get-player (thread-player)))) + (width (read-from-string swidth)) + (height (read-from-string sheight)) + (x0 (- (.x plr) (halve width))) + (y0 (- (.y plr) (halve height))) + (submap (make-array (list width height 2)))) + (dotimes (h height) + (dotimes (w width) + (let ((p (coord (+ w x0 1) (+ h y0 1))) + (next-char #\space) (next-col ':black)) + (if (and p (patch-occupant p)) + (setf next-char (.char (patch-occupant p)) + next-col (.color (patch-occupant p))) + (when p (setf next-char (biome-char (patch-biome p)) + next-col (biome-col (patch-biome p))))) + (setf (aref submap w h 0) next-char + (aref submap w h 1) next-col)))) + ;;FIXME arrays are pretty-printed with linebreaks, this causes a + ;; client-side error (only expecting one line) + submap)) + +(defun describe-patch (coords) + "Return a list of lines describing the patch at these coordinates." + (let ((p (coord (first coords) (second coords)))) + (list (string-upcase (biome-name (patch-biome p))) "" + (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" + (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) + (when (patch-occupant p) ;TODO players -> "$name is here." + (format NIL "There is ~A here." + (leading-vowel (.name (patch-occupant p))))) + (when (patch-items p) + (format NIL "The following items are here:~A *~A" #\newline + (string-from-list (mapcar #'.name (patch-items p))) + (format NIL "~% *")))))) + +(defun move-player (dir) + "Move a player in the given direction" + ;;TODO + ) + +(defparameter *API* + ;; An alist of API commands and their corresponding functions + ;;XXX move all listed functions to another file? + (list (list "login" #'login) + (list "signup" #'create-player) + (list "map" #'get-map) + (list "describe-patch" #'describe-patch) + (list "move" #'move-player))) diff --git a/src/server/world.lisp b/src/server/world.lisp new file mode 100644 index 0000000..5365554 --- /dev/null +++ b/src/server/world.lisp @@ -0,0 +1,187 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines patches and administrates the world object. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct patch + (pos '(0 0)) ;position + (biome NIL) + (items '()) + (occupant NIL)) + +(defstruct biome + (name "") + (ground "") + (features '()) ;an alist of possible features and their 1/p probabilities + (char #\.) ;default map display character + (col ':white)) ;default map display colour + +;; BIOME LIST + +(let ((biome-list NIL)) + (defun register-biome (symbol-name biome-object) + (setf biome-list (cons (list symbol-name biome-object) biome-list))) + + (defun available-biomes () + (keys biome-list)) + + (defun get-biome (symbol-name) + (cassoc symbol-name biome-list))) + +(defmacro new-biome (name &body body) + `(register-biome ',name + (make-biome + :name ,(symbol-to-string name) + ,@body))) + +;; MATRIX FUNCTIONS + +(defun init-matrix (size) + "Create a square matrix of empty patches" + ;;TODO change this to arrays for performance + (debugging "~&Creating a ~S/~S matrix." size size) + (do ((y 0 (1+ y)) (world NIL) (row NIL NIL)) + ((= y size) world) + (dotimes (x size) + (setf row (append row (list (make-patch :pos (list x y)))))) + (setf world (append world (list row))))) + +(defun distance (x1 y1 x2 y2 &optional (pythag NIL)) + "Find the distance between two sets of coordinates" + (if pythag (round (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2)))) + (min (abs (- x1 x2)) (abs (- y1 y2))))) + +(defun closest-coords (here coord-list &optional (abs-dist NIL)) + "Find the closest position to 'here' from a list of coordinates" + (do* ((clist coord-list (cdr clist)) (c (car clist) (car clist)) + (dist (when c (distance (first here) (second here) + (first c) (second c) abs-dist)) + (when c (distance (first here) (second here) + (first c) (second c) abs-dist))) + (mindist dist) (closest c)) + ((null clist) closest) + (when (< dist mindist) + (setf mindist dist closest c)))) + +(defun opposite-dir (dir) + "Return the direction opposite the input" + (let ((pos (position dir *directions*))) + (when pos (nth (rem (+ 4 pos) 8) *directions*)))) + +(defun next-dir (dir &optional (cw T)) + "Get the neighbouring direction (clockwise or anticlockwise)" + (let ((pos (position dir *directions*)) + (diff (if cw 1 -1))) + (when pos (nth (rem (+ diff pos 8) 8) *directions*)))) + +(defun orth-dir (dir &optional (cw T)) + "Get the direction orthogonal (at right angles) to the given one." + (next-dir (next-dir dir cw) cw)) + +(defun diagonalp (dir) + "Is dir a diagonal direction?" + (member dir '(NE SE SW NW) :test #'eq)) + +(defun dir2patch (herex herey therex therey) + "Calculate the direction to a patch" + (cond ((> herex therex) + (cond ((> herey therey) 'NW) + ((< herey therey) 'SW) + (T 'W))) + ((< herex therex) + (cond ((> herey therey) 'NE) + ((< herey therey) 'SE) + (T 'E))) + (T (cond ((> herey therey) 'N) + ((< herey therey) 'S) + (T NIL))))) + +(defun coordsindir (x y dir) + "Return the coordinates in the given direction" + (cond ((eq dir 'N) (list x (1- y))) + ((eq dir 'NE) (list (1+ x) (1- y))) + ((eq dir 'E) (list (1+ x) y)) + ((eq dir 'SE) (list (1+ x) (1+ y))) + ((eq dir 'S) (list x (1+ y))) + ((eq dir 'SW) (list (1- x) (1+ y))) + ((eq dir 'W) (list (1- x) y)) + ((eq dir 'NW) (list (1- x) (1- y))) + ((null dir) (list x y)) + (T (error "~&Invalid direction ~S")))) + +(defun patchindir (x y dir) + "Return the patch in the given direction" + (let* ((coords (coordsindir x y dir)) + (nextx (first coords)) (nexty (second coords))) + (coord nextx nexty))) + +(defun neighbour (p dir) + "Return the neighbouring patch in this direction" + (patchindir (first (patch-pos p)) (second (patch-pos p)) dir)) + + +;; WORLD CREATION FUNCTIONS + +(defun get-patch-feature (patch) + "Find a random feature (or none) to occupy this patch." + (let ((flist (biome-features (patch-biome patch)))) + (dolist (f flist NIL) + (when (chancep (second f)) + (return-from get-patch-feature + (make-instance (first f) :x (first (patch-pos patch)) + :y (second (patch-pos patch)))))))) + +(defun generate-biomes (size-factor) + ;;XXX The maps this produces don't look quite as expected, but for + ;; current purposes they are good enough + (debugging "~&Generating biomes") ;debug + (let* ((wsize (world-size)) (seeds NIL) + (nseeds (round (/ wsize size-factor))) + (biomes (remove-first-if + #'(lambda (e) (eq e 'stream)) + (available-biomes)))) + ;;Initialize a set of biome 'seed' coordinates + (dotimes (n nseeds) + (setf seeds + (cons (list (random wsize) + (random wsize) + (random-elt biomes)) + seeds))) + (debugging "~&~S" seeds) + ;;For each patch, calculate the closest seed and set to that biome + (dotimes (x wsize seeds) + (dotimes (y wsize) + (let ((p (coord x y)) + (b (third (closest-coords (list x y) seeds T)))) + (setf (patch-biome p) (get-biome b)) + (setf (patch-occupant p) (get-patch-feature p))))))) + +(defun generate-stream (x0 y0) + (debugging "~&Generating a stream, starting at ~S/~S" x0 y0) ;debug + (do* ((dir (random-elt *directions*) + (if (probabilityp 75) dir (next-dir dir (random-elt '(T NIL))))) + (patch (coord x0 y0) (neighbour patch dir))) + ((or (null patch) (eq (patch-biome patch) (get-biome 'stream)))) + (setf (patch-biome patch) (get-biome 'stream)) + (setf (patch-occupant patch) NIL))) + +(defun create-world (size) + "Create a world of the specified size (square)" + (set-world (init-matrix size)) + ;;XXX magic numbers + (generate-biomes 10) + (dotimes (s (round (/ (expt size 2) 2000))) + (generate-stream (random size) (random size)))) + +(defun init-world () + "Initialize the log, RNG, and world." + (write-to-file "NALEDI ya AFRICA" *logfile*) + (logging (time-stamp)) + (setf *random-state* (make-random-state t)) + (create-world *world-size*)) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/src/server/player.lisp b/src/server/player.lisp new file mode 100644 index 0000000..712738b --- /dev/null +++ b/src/server/player.lisp @@ -0,0 +1,91 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for managing player instances. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct player + (name "") + (password "") + (online NIL) + (human NIL)) + +(defclass human (animal) + ;; A human player + ;;XXX add age? + ((experience :accessor .xp :initform 0) + (level :accessor .level :initform 0) + (dexterity :accessor .dex :initarg :dex :initform 1) + (intelligence :accessor .int :initarg :int :initform 1) + (hunger :accessor .hunger :initarg :hunger :initform 10) + (tool :accessor .tool :initarg :tool :initform NIL) + (inventory :accessor .inventory :initarg :inventory + :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) + (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) + ;;XXX change habitats to (available-biomes)? -> requires load order change + (:default-initargs :habitat T)) + +(defmethod update ((h human)) + ;;TODO + ) + +;; INVENTORY HANDLING FUNCTIONS + +;;TODO convert to methods + +(defun stock-size (resource player) + "How many items of this resource type is the player carrying?" + (dolist (i (player-inventory player) 0) + (when (eq (item-name (first i)) resource) + (return-from stock-size (second i))))) + +(defun weight-carried (player) + "Sum up the total weight of all items carried" + (+ (item-weight (player-equipment player)) + (reduce #'+ (mapcar #'(lambda (i) + (* (second i) (item-weight (first i))))) + (player-inventory player)))) + +(defun pickup (item player) + "Add the item object to the inventory" + ;;Can the item be picked up at all? + (unless (item-movable item) + (notify "This item cannot be picked up.") + (return-from pickup)) + ;;Is the player strong enough to pick this up? + (unless (<= (+ (item-weight item) (weight-carried player)) + (* (player-strength player) 20)) ;XXX magic number + (notify "You are too burdened to pick this up.") + (return-from pickup)) + (dolist (inv (player-inventory player)) + ;;Resources may be stacked + (when (and (item-resource item) + (eq (item-name item) (item-name (first inv)))) + (incf (second inv)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup)) + ;;Deposit the item in an empty slot + (when (and (null (first inv)) (item-resource item) ;normal pickup + ;;XXX replace with find-if + (zerop (count-instances (item-name item) + (mapcar #'(lambda (i) (item-name (car i))))))) + (setf inv (list item 1)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup))) + ;;If nothing has worked, the inventory is full + (notify "Your inventory is full.")) + +(defun drop (player inv-nr) + "Drop an item from the given inventory index" + ;;TODO add item back to patch + (let* ((item-entry (nth inv-nr (player-inventory player))) + (item (when item-entry (first item-entry)))) + (if (and item (> (stock-size (item-name item) 1))) + (decf (second item-entry)) + (setf item NIL (second item-entry) 0)))) + diff --git a/src/server/server.lisp b/src/server/server.lisp new file mode 100644 index 0000000..ae45bad --- /dev/null +++ b/src/server/server.lisp @@ -0,0 +1,284 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file stores all game data and handles the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; TODO save and load functions +;; XXX Will probably require `make-load-form-saving-slots' + +;;; PLAYER LIST + +(let ((players NIL)) + (defun reset-players () (setf players NIL)) ;;TODO logout first + + (defun get-player (name) + (first (member name players + :test #'(lambda (s p) (equalp (player-name p) s))))) + + (defun add-player (name passwd) + (let ((np (make-player :name name :password passwd :online NIL + :human (make-instance 'human + :x (random *world-size*) + :y (random *world-size*))))) + (setf players (cons np players)))) + + (defun online-players () + "Return a list of names of players who are online" + (loop for p in players + if (player-online p) + collect (player-name p))) + + (defun save-players (file-name) + ;;TODO + )) + +(defun online-p (name) + (let ((p (get-player name))) + (when p (player-online p)))) + +;;; WORLD OBJECT + +(let ((world NIL)) + (defun set-world (w) (setf world w)) + + (defun world-size () (length world)) + + (defun save-topography (file-name) ;XXX (re)move this? + "Save the world topography as a text file" + (debugging "~&Saving world to file ~A" file-name) ;debug + (with-open-file (tf file-name :direction :output) + (dolist (row world) + (format stream "~&~A~%" + (string-from-list + (mapcar #'(lambda (p) + (biome-char (patch-biome p))) + row) ""))))) + + (defun save-world (&optional (file-name "naledi.save")) + ;;TODO + (save-players file-name)) + + (defun coord (x y) + "Return the patch at the given coordinates or NIL if out of bounds" + (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) + (nth x (nth y world))))) + +;;; WORLD THREADS + +(let ((uptime 0) (world-thread NIL) (server-thread NIL) + (player-threads NIL) (running NIL)) + + (defun start-server (&optional (force T)) + "Start the game server" + ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR + ;; -> comes from not closing connections properly? + ;;XXX change force back to NIL? + (when force + (reset-server-threads) + (reset-world-age)) + (unless (or world-thread server-thread) + (init-world) + (setf running T) + (setf world-thread + (bt:make-thread #'update-loop :name "world-thread")) + (setf server-thread + (bt:make-thread #'run-server :name "server-thread")) + (logging "SERVER: initialized"))) + + (defun terminate () + ;;TODO do some error catching if the threads no longer exist + (logging "SERVER: terminating the world.") + (setf running NIL) + ;;XXX have to use destroy-thread because the server mostly idles, + ;; waiting for connections - only checks 'running' when connecting + (bt:destroy-thread server-thread) + (bt:join-thread world-thread) + (dolist (pt player-threads) + (bt:join-thread (first pt))) + (reset-server-threads) + (save-world)) ;XXX not yet implemented + + (defun age-of-the-world () uptime) + + (defun reset-world-age () (setf uptime 0)) + + (defun runningp () running) + + (defun reset-server-threads () + (set-list NIL server-thread world-thread player-threads)) + + (defun thread-player (&optional (th (bt:current-thread))) + "Return the player name associated with this thread" + (cassoc th player-threads)) + + (defun set-thread-player (name &optional (th (bt:current-thread))) + "Set the player name associated with this thread" + ;;XXX somewhat ugly... + (setf (cassoc th player-threads) name)) + + (defun cleanup-player-threads () + "Remove threads of disconnected players" + (dotimes (i (length player-threads)) + (let ((pt (nth i player-threads))) + (when (equalp (second pt) "terminated") + (bt:join-thread (first pt)) + (setf (cdr (nth (1- i) player-threads)) + (nth (1+ i) player-threads)))))) + + (defun update-loop () + "The main loop, updating the world in the background" + ;;XXX split this up into two or more functions, to be run by + ;; different threads? (e.g. player- & world-update threads) + (logging "UPDATE ~S" uptime) + ;;Update all items and occupants in each patch + (dotimes (y (world-size)) + (dotimes (x (world-size)) + (unless running (return-from update-loop)) + (when (patch-occupant (coord x y)) + (update (patch-occupant (coord x y)))) + (dolist (i (patch-items (coord x y))) + (update i)))) + ;;Update all items each player has + ;;TODO + ;;Do cleanup work + (when (zerop (rem uptime 20)) (cleanup-player-threads)) + ;;Save the world and start over + (save-world) ;XXX not yet implemented + (incf uptime) + (sleep (/ *framerate* 1000)) + (when running (update-loop))) ;;requires Tail-Call Optimization + + (defun run-server () + "Start a server, listening for connections" + (usocket:with-socket-listener (socket "127.0.0.1" *port*) + (while running + (usocket:wait-for-input socket) + (when (eq (usocket:socket-state socket) ':read) + ;;XXX give player threads unique names again? + (let ((thread (bt:make-thread + #'(lambda () + (handle-connection socket)) + :name "player-thread"))) + (setf player-threads + (cons (list thread "anon") player-threads)) + (sleep 1)))))) ;;give the socket a chance to connect + + (defun handle-connection (socket) + "Answer requests until the player disconnects" + (usocket:with-connected-socket (conn (usocket:socket-accept socket)) + (logging "~&SERVER: received a connection") ; cf. `get-peer-name' + (do* ((sockstr (usocket:socket-stream conn)) + (request (read-line sockstr NIL) (read-line sockstr NIL))) + ((or (not running) (null request)) (logout)) + (format sockstr "~S~%" (to-string (answer request))) + (finish-output sockstr))))) + +(defun answer (request) + "Dispatch on the functions registerd in `*API*'" + ;;XXX potential problem with reading all user-received symbols into memory + (let* ((reqelts (split-string request #\space)) + (cmd (car reqelts)) + (args (cdr reqelts))) + (logging "SERVER: received request ~S" reqelts) + (if (member cmd (keys *API*) :test #'equalp) + ;;XXX Surely there must be a way to simplify the next few lines?! + (if (and (equalp (thread-player) "anon") + (not (or (equalp cmd "login") (equalp cmd "signup")))) + "ERROR: not logged in" + (apply (cassoc cmd *API* :test #'equalp) args)) + "ERROR: unknown command"))) + +;;; COMMUNICATION FUNCTIONS + +;;NOTE: all following functions receive string arguments and must convert +;; them as needed + +(defun login (name passwd) + "Log this player in" + ;;XXX name and passwd are converted to symbols by `answer'! - not currently + (logging "SERVER: ~A is trying to log in" name) + (let ((p (get-player name))) + (cond ((not p) "ERROR: nonexistent player") + ((not (equalp passwd (player-password p))) "ERROR: bad password") + (T (setf (player-online p) T) + (setf (patch-occupant ;;TODO check for previous occupants + (coord (.x (player-human p)) (.y (player-human p)))) + (player-human p)) + (set-thread-player name) + (logging "SERVER: player ~A logged in" name) + name)))) + +(defun logout () + "Log the player out again" + (let ((p (get-player (thread-player)))) + (when p + (setf (player-online p) NIL) + (setf (patch-occupant + (coord (.x (player-human p)) (.y (player-human p)))) + NIL) ;;TODO set to "statue of " + (logging "SERVER: player ~A logged out" (thread-player)) + (set-thread-player "terminated")))) + +(defun create-player (name passwd) + "Create and log in a new player" + (if (get-player name) "ERROR: player exists" + (progn + (add-player name passwd) + (login name passwd)))) + +(defun get-map (swidth sheight) + "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" + (let* ((plr (player-human (get-player (thread-player)))) + (width (read-from-string swidth)) + (height (read-from-string sheight)) + (x0 (- (.x plr) (halve width))) + (y0 (- (.y plr) (halve height))) + (submap (make-array (list width height 2)))) + (dotimes (h height) + (dotimes (w width) + (let ((p (coord (+ w x0 1) (+ h y0 1))) + (next-char #\space) (next-col ':black)) + (if (and p (patch-occupant p)) + (setf next-char (.char (patch-occupant p)) + next-col (.color (patch-occupant p))) + (when p (setf next-char (biome-char (patch-biome p)) + next-col (biome-col (patch-biome p))))) + (setf (aref submap w h 0) next-char + (aref submap w h 1) next-col)))) + ;;FIXME arrays are pretty-printed with linebreaks, this causes a + ;; client-side error (only expecting one line) + submap)) + +(defun describe-patch (coords) + "Return a list of lines describing the patch at these coordinates." + (let ((p (coord (first coords) (second coords)))) + (list (string-upcase (biome-name (patch-biome p))) "" + (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" + (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) + (when (patch-occupant p) ;TODO players -> "$name is here." + (format NIL "There is ~A here." + (leading-vowel (.name (patch-occupant p))))) + (when (patch-items p) + (format NIL "The following items are here:~A *~A" #\newline + (string-from-list (mapcar #'.name (patch-items p))) + (format NIL "~% *")))))) + +(defun move-player (dir) + "Move a player in the given direction" + ;;TODO + ) + +(defparameter *API* + ;; An alist of API commands and their corresponding functions + ;;XXX move all listed functions to another file? + (list (list "login" #'login) + (list "signup" #'create-player) + (list "map" #'get-map) + (list "describe-patch" #'describe-patch) + (list "move" #'move-player))) diff --git a/src/server/world.lisp b/src/server/world.lisp new file mode 100644 index 0000000..5365554 --- /dev/null +++ b/src/server/world.lisp @@ -0,0 +1,187 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines patches and administrates the world object. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct patch + (pos '(0 0)) ;position + (biome NIL) + (items '()) + (occupant NIL)) + +(defstruct biome + (name "") + (ground "") + (features '()) ;an alist of possible features and their 1/p probabilities + (char #\.) ;default map display character + (col ':white)) ;default map display colour + +;; BIOME LIST + +(let ((biome-list NIL)) + (defun register-biome (symbol-name biome-object) + (setf biome-list (cons (list symbol-name biome-object) biome-list))) + + (defun available-biomes () + (keys biome-list)) + + (defun get-biome (symbol-name) + (cassoc symbol-name biome-list))) + +(defmacro new-biome (name &body body) + `(register-biome ',name + (make-biome + :name ,(symbol-to-string name) + ,@body))) + +;; MATRIX FUNCTIONS + +(defun init-matrix (size) + "Create a square matrix of empty patches" + ;;TODO change this to arrays for performance + (debugging "~&Creating a ~S/~S matrix." size size) + (do ((y 0 (1+ y)) (world NIL) (row NIL NIL)) + ((= y size) world) + (dotimes (x size) + (setf row (append row (list (make-patch :pos (list x y)))))) + (setf world (append world (list row))))) + +(defun distance (x1 y1 x2 y2 &optional (pythag NIL)) + "Find the distance between two sets of coordinates" + (if pythag (round (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2)))) + (min (abs (- x1 x2)) (abs (- y1 y2))))) + +(defun closest-coords (here coord-list &optional (abs-dist NIL)) + "Find the closest position to 'here' from a list of coordinates" + (do* ((clist coord-list (cdr clist)) (c (car clist) (car clist)) + (dist (when c (distance (first here) (second here) + (first c) (second c) abs-dist)) + (when c (distance (first here) (second here) + (first c) (second c) abs-dist))) + (mindist dist) (closest c)) + ((null clist) closest) + (when (< dist mindist) + (setf mindist dist closest c)))) + +(defun opposite-dir (dir) + "Return the direction opposite the input" + (let ((pos (position dir *directions*))) + (when pos (nth (rem (+ 4 pos) 8) *directions*)))) + +(defun next-dir (dir &optional (cw T)) + "Get the neighbouring direction (clockwise or anticlockwise)" + (let ((pos (position dir *directions*)) + (diff (if cw 1 -1))) + (when pos (nth (rem (+ diff pos 8) 8) *directions*)))) + +(defun orth-dir (dir &optional (cw T)) + "Get the direction orthogonal (at right angles) to the given one." + (next-dir (next-dir dir cw) cw)) + +(defun diagonalp (dir) + "Is dir a diagonal direction?" + (member dir '(NE SE SW NW) :test #'eq)) + +(defun dir2patch (herex herey therex therey) + "Calculate the direction to a patch" + (cond ((> herex therex) + (cond ((> herey therey) 'NW) + ((< herey therey) 'SW) + (T 'W))) + ((< herex therex) + (cond ((> herey therey) 'NE) + ((< herey therey) 'SE) + (T 'E))) + (T (cond ((> herey therey) 'N) + ((< herey therey) 'S) + (T NIL))))) + +(defun coordsindir (x y dir) + "Return the coordinates in the given direction" + (cond ((eq dir 'N) (list x (1- y))) + ((eq dir 'NE) (list (1+ x) (1- y))) + ((eq dir 'E) (list (1+ x) y)) + ((eq dir 'SE) (list (1+ x) (1+ y))) + ((eq dir 'S) (list x (1+ y))) + ((eq dir 'SW) (list (1- x) (1+ y))) + ((eq dir 'W) (list (1- x) y)) + ((eq dir 'NW) (list (1- x) (1- y))) + ((null dir) (list x y)) + (T (error "~&Invalid direction ~S")))) + +(defun patchindir (x y dir) + "Return the patch in the given direction" + (let* ((coords (coordsindir x y dir)) + (nextx (first coords)) (nexty (second coords))) + (coord nextx nexty))) + +(defun neighbour (p dir) + "Return the neighbouring patch in this direction" + (patchindir (first (patch-pos p)) (second (patch-pos p)) dir)) + + +;; WORLD CREATION FUNCTIONS + +(defun get-patch-feature (patch) + "Find a random feature (or none) to occupy this patch." + (let ((flist (biome-features (patch-biome patch)))) + (dolist (f flist NIL) + (when (chancep (second f)) + (return-from get-patch-feature + (make-instance (first f) :x (first (patch-pos patch)) + :y (second (patch-pos patch)))))))) + +(defun generate-biomes (size-factor) + ;;XXX The maps this produces don't look quite as expected, but for + ;; current purposes they are good enough + (debugging "~&Generating biomes") ;debug + (let* ((wsize (world-size)) (seeds NIL) + (nseeds (round (/ wsize size-factor))) + (biomes (remove-first-if + #'(lambda (e) (eq e 'stream)) + (available-biomes)))) + ;;Initialize a set of biome 'seed' coordinates + (dotimes (n nseeds) + (setf seeds + (cons (list (random wsize) + (random wsize) + (random-elt biomes)) + seeds))) + (debugging "~&~S" seeds) + ;;For each patch, calculate the closest seed and set to that biome + (dotimes (x wsize seeds) + (dotimes (y wsize) + (let ((p (coord x y)) + (b (third (closest-coords (list x y) seeds T)))) + (setf (patch-biome p) (get-biome b)) + (setf (patch-occupant p) (get-patch-feature p))))))) + +(defun generate-stream (x0 y0) + (debugging "~&Generating a stream, starting at ~S/~S" x0 y0) ;debug + (do* ((dir (random-elt *directions*) + (if (probabilityp 75) dir (next-dir dir (random-elt '(T NIL))))) + (patch (coord x0 y0) (neighbour patch dir))) + ((or (null patch) (eq (patch-biome patch) (get-biome 'stream)))) + (setf (patch-biome patch) (get-biome 'stream)) + (setf (patch-occupant patch) NIL))) + +(defun create-world (size) + "Create a world of the specified size (square)" + (set-world (init-matrix size)) + ;;XXX magic numbers + (generate-biomes 10) + (dotimes (s (round (/ (expt size 2) 2000))) + (generate-stream (random size) (random size)))) + +(defun init-world () + "Initialize the log, RNG, and world." + (write-to-file "NALEDI ya AFRICA" *logfile*) + (logging (time-stamp)) + (setf *random-state* (make-random-state t)) + (create-world *world-size*)) diff --git a/src/util.lisp b/src/util.lisp index 28b1b6f..909b6ac 100644 --- a/src/util.lisp +++ b/src/util.lisp @@ -12,6 +12,8 @@ ;;; MACROS +;; --- DEPRECATED --- +;; (use `logging' with log-level >= 3 instead) (defmacro debugging (str &rest format-args) "If *debugging* is true, print str" `(when *debugging* (format t ,str ,@format-args))) diff --git a/TODO b/TODO index d907c04..a0d8fa6 100644 --- a/TODO +++ b/TODO @@ -19,6 +19,8 @@ * split `naledi-ya-africa` package into `naledi-server` and `naledi-client`? * remove `debugging` calls, introduce logging levels + +* cleanup util.lisp, dump what I don't need * (I am legion...) diff --git a/content/animals.lisp b/content/animals.lisp deleted file mode 100644 index 3454951..0000000 --- a/content/animals.lisp +++ /dev/null @@ -1,71 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines animal and species structs and the various species -;;;; found in-game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX bird species? -;;XXX water species? -;;XXX small mammals -> hills - -(new-item animal impala - :strength 3 :max-health 10 - :aggression 0 :group-size 15 - :habitat '(forest grassland) - :char #\a :color :yellow - :drops NIL) ;TODO - -(new-item animal warthog - :strength 5 :max-health 10 - :aggression 2 :group-size 5 - :habitat '(forest grassland swamp hill) - :char #\p :color :magenta - :drops NIL) ;TODO - -(new-item animal lion - :strength 18 :max-health 25 - :aggression 20 :group-size 6 - :habitat '(grassland swamp desert) - :char #\C :color :red - :drops NIL) ;TODO - -(new-item animal leopard - :strength 20 :max-health 30 - :aggression 20 :group-size 1 - :habitat '(forest grassland hill) - :char #\C :color :magenta - :drops NIL) ;TODO - -(new-item animal cheetah - :strength 15 :max-health 20 - :aggression 10 :group-size 1 - :habitat '(grassland) - :char #\C :color :yellow - :drops NIL) ;TODO - -(new-item animal elephant - :strength 40 :max-health 50 - :aggression 5 :group-size 4 - :habitat '(forest grassland) - :char #\E :color :cyan - :drops NIL) ;TODO - -(new-item animal buffalo - :strength 15 :max-health 20 - :aggression 5 :group-size 10 - :habitat '(swamp grassland) - :char #\B :color :magenta - :drops NIL) ;TODO - -(new-item animal oryx - :strength 8 :max-health 12 - :aggression 0 :group-size 2 - :habitat '(desert grassland) - :char #\a :color :blue - :drops NIL) ;TODO diff --git a/content/biomes.lisp b/content/biomes.lisp deleted file mode 100644 index 4f20bd9..0000000 --- a/content/biomes.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; Biomes are habitat types, that provide default functions for patches. -;;;; This file holds the biome definitions. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;XXX lake? -;;TODO optimise frequency numbers - -(new-biome grassland - :ground "tall elephant grass" - :char #\; :col ':yellow - :features '((acacia 100) (boulder 200) - (impala 500) (warthog 600) - (lion 2400) (leopard 3000) - (cheetah 2400) (elephant 800) - (buffalo 1000))) ;TODO - -(new-biome forest - :ground "leaf litter and small shrubs" - :char #\. :col ':green - :features '((miombo 10) (acacia 15) - (warthog 500) (impala 800) - (elephant 1000) (leopard 3200))) ;TODO - -(new-biome stream - :ground "shallow flowing water" - :char #\~ :col ':blue - :features '()) ;TODO - -(new-biome swamp - :ground "short sedge grass growing on boggy black soil" - :char #\w :col ':green - :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO - -(new-biome hill - :ground "hard, stony soil" - :char #\m :col ':cyan ;XXX very bright - hurts the eyes... - :features '((boulder 20) (miombo 80) - (warthog 600) (leopard 3200))) ;TODO - -(new-biome desert - :ground "deep, drifting sand" - :char #\^ :col ':white ;XXX yellow would be better, but shows up brown - :features '((cactus 100) (boulder 200) - (oryx 800) (lion 3600))) ;TODO diff --git a/content/items.lisp b/content/items.lisp deleted file mode 100644 index c0d4e5f..0000000 --- a/content/items.lisp +++ /dev/null @@ -1,87 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines in-game items. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;TODO split up into several files for each category - -;;; RESOURCE ITEMS - -;;TODO sand, glass, clay, bricks - -(new-item resource wood - :description "A block of wood, just right for working with" - :weight 1 :burning-product 'charcoal) - -(new-item resource charcoal - :description "A black lump of charcoal. Burns better than wood." - :weight 1) - -(new-item resource stone - :description "A fist-sized stone. What are you going to do with it?" - :weight 2) - -(new-item resource iron-ore - :description "A lump of iron ore - still needs to be smelted." - :weight 4 :burning-product 'iron) - -(new-item resource iron - :description "An iron ingot, ready for further crafting." - :weight 3) - -;;; LANDSCAPE FEATURES - -(new-item feature acacia - :description "A tall acacia tree, spreading its branches wide." - :destroy-with '(wood) - :drops '(wood) :weight 2000 - :char #\T :color :green) - -(new-item feature miombo - :description "A small, crooked miombo tree." - :destroy-with '(wood) - :drops '(wood) :weight 500 - :char #\Y :color :green) - -(new-item feature boulder - :description "A huge lump of grey basalt, sticking out of the ground." - :destroy-with '(stone) - :drops '(stone) :weight 5000 - :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( - -(new-item feature cactus - :description "A prickly eupharbacae sticks out of the ground here - don't touch!" - :destroy-with '(axe) ;does it drop anything? - :char #\+ :color :green) - -;;TODO termite hill, palm, baobab, pond - -;;; TOOL ITEMS - -;;TODO bronze tools, weapons - -(new-item tool stone-axe - :description "An axe, crudely but effectively fashioned out of stone." - :type 'wood :level 1 - :weight 2 :craft-with '(stone wood)) - -(new-item tool iron-axe - :description "A finely honed iron axe, just right for chopping wood." - :type 'wood :level 3 - :weight 3 :craft-with '(iron wood)) - -(new-item tool stone-pickaxe - :description "A pickaxe, somewhat helplessly made from stone." - :type 'stone :level 1 - :weight 3 :craft-with '(stone stone wood)) - -(new-item tool iron-pickaxe - :description "A solid iron pickaxe. Prepare to split rock!" - :type 'stone :level 3 - :weight 4 :craft-with '(iron iron wood)) diff --git a/naledi.asd b/naledi.asd index 734976d..73453b2 100644 --- a/naledi.asd +++ b/naledi.asd @@ -20,23 +20,30 @@ :depends-on (:bordeaux-threads :croatoan :usocket) :serial t :components - ((:file "package") - (:module "src" - :serial t - :components - ((:file "params") - (:file "util") - (:file "item-classes") - (:file "item-methods") - (:file "player") - (:file "world") - (:file "server") - (:file "client") - (:file "crt-ext"))) - (:module "content" - :components - ((:file "animals") - (:file "biomes") - (:file "items"))) - (:file "naledi")) + ((:module "src" + :serial t + :components + ((:file "package") + (:file "util") + (:file "naledi") + (:module "server" + :serial t + :components + ((:file "params") + (:file "item-classes") + (:file "item-methods") + (:file "player") + (:file "world") + (:file "server"))) + (:module "content" + :components + ((:file "animals") + (:file "biomes") + (:file "items"))) + (:module "client" + :serial t + :components + ((:file "crt-ext") + (:file "networking") + (:file "user-interface")))))) :entry-point "naledi:start-game") diff --git a/naledi.lisp b/naledi.lisp deleted file mode 100644 index 2cd3483..0000000 --- a/naledi.lisp +++ /dev/null @@ -1,229 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is the main program file with the user interface. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -;;XXX move this file to src/ncurses.lisp? - -(in-package :naledi-ya-africa) -;(use-package :croatoan) - -(defun start-game () - "Start the game logic and UI" - (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t - :input-echoing nil :cursor-visibility nil - :input-reading :unbuffered) - (splash-screen scr) - (start-or-connect-to-server scr) - (user-interface scr))) - -(defun splash-screen (scr) - "Display the splash screen with the `Naledi ya Africa' logo" - (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) - ;;FIXME fails when not in the naledi directory - (logo (load-text-file "LOGO")) - (y (halve (- height (length logo)))) - (xoff (halve (- width 80)))) - (croatoan:clear scr) - (dolist (l logo) - (croatoan:move scr y xoff) - (croatoan:add-string scr l) - (incf y)) - (croatoan:move scr (1- height) 0) - (croatoan:add-string scr "Press any key to continue.") - (croatoan:move scr (1- height) (- width 22)) - (croatoan:add-string scr "(c) 2018 Daniel Vedder") - (croatoan:event-case (scr event) - ((nil) nil) - (otherwise (return-from croatoan:event-case))))) - -(defun start-or-connect-to-server (scr) - "Choose whether to start a local game or connect to a remote server" - (croatoan:clear scr) - (croatoan:refresh scr) - (let ((mw (make-instance 'croatoan:dialog-window - :title "Welcome!" - :center t - :border t - :width 50 - :max-item-length 42 - :input-blocking t - :cyclic-selection t - :current-item-mark "* " - :items '("Start a local game" - ;;TODO "Load a saved game" - "Connect to a remote server")))) - ;;XXX I have to effectively reimplement (select-item menu) because - ;; the screen grabs all user input and none arrives at the window - (croatoan:draw-menu mw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:up :down) - (croatoan:update-menu mw event) - (croatoan:draw-menu mw)) - (#\newline - (cond ((= (croatoan:.current-item-number mw) 0) - (start-local-game scr)) - ((= (croatoan:.current-item-number mw) 1) - (connect-remote-game scr))) - (return-from croatoan:event-case))))) - -(defun start-local-game (scr) - "Start a local game" - ;;TODO choose world size - (start-server) - (setf *host* (first *defaulthost*) - *port* (second *defaulthost*)) - ;;give the server time to start - (while (not (runningp)) ;;TODO replace with `loop' - (sleep 0.5)) - (connect-server) - (query-server "signup localuser default")) ;;just use a default user - -;;TODO (choose-world-size) - -(defun connect-remote-game (scr) - "Ask the user which server to connect to and do so" - (setf *host* (query-user scr "Server IP/URL:" :cls T)) - (setf *port* (read-from-string - (query-user scr "Server port:" - :default (second *defaulthost*) :cls T))) - (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) - ;;TODO ask for username/password! - (connect-server) - (start-or-connect-to-server scr))) - -(defun user-interface (scr) - "Create the screen on the ncurses interface and hand over to window functions" - (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) - (me (list (round (/ width 4)) (halve height)))) - (croatoan:clear scr) - (croatoan:refresh scr) - (croatoan:with-windows ((mapwin :position '(0 0) - :input-blocking *framerate* - :border t :width (- width 51) - :height height) - (playerwin :position (list 0 (- width 50)) - :input-blocking *framerate* :border t - :width 50 :height (halve height 'down)) - (placewin :input-blocking *framerate* - :border t - :position (list (halve height) - (- width 50)) - :width 50 :height (halve height 'down)) - (newswin :input-blocking *framerate* - :position (list height 0) - :width width :height 1)) - (update-ui mapwin playerwin placewin newswin me) - ;;TODO - (croatoan:event-case (scr event) - (#\q (disconnect) - ;;terminate if we're running a local game - (when (member "server-thread" (bt:all-threads) :test - #'(lambda (nm th) - (equalp nm (bt:thread-name th)))) - (terminate)) - (return-from croatoan:event-case)) - (#\n (croatoan:draw-menu (message-window))) - ;;FIXME change directions to single letters - (:up (query-server "move north") - (update-ui mapwin playerwin placewin newswin me)) - (:down (query-server "move south") - (update-ui mapwin playerwin placewin newswin me)) - (:left (query-server "move west") - (update-ui mapwin playerwin placewin newswin me)) - (:right (query-server "move east") - (update-ui mapwin playerwin placewin newswin me)) - ((nil) (update-ui mapwin playerwin placewin newswin me)) - (otherwise (notify (string event))))))) - -(defun update-ui (mapwin playerwin placewin newswin me) - "Update all four UI elements" - (draw-map mapwin) - (draw-player-panel playerwin) - (draw-place-panel placewin me) - (draw-news-panel newswin)) - -(defun draw-map (win) - "Draw a portion of the game map in an ncurses window" - (setf (croatoan:.color-pair win) '(:white :black)) - (croatoan:box win) - (croatoan:move win 1 1) - (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) - (map-height (1- (croatoan:.height win))) - (submap (query-server "map" map-width map-height))) - (dotimes (h map-height) - (dotimes (w map-width) - (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) - (croatoan:add-char win pch :color-pair (list pcol black)) - (croatoan:add-char win #\space))) - (croatoan:move win (1+ h) 1))) - (croatoan:refresh win)) - -(defun draw-player-panel (win) - "Draw a panel with information about the player character." - ;;TODO - (croatoan:box win) - (croatoan:move win 1 1) - (croatoan:add-string win "This is the player panel.") - (croatoan:refresh win)) - -(defun draw-place-panel (win me) - "Draw a panel with information about the player's current location." - ;;TODO replace `describe-patch' with `query-server' - (let ((descr (break-lines (describe-patch me) - (- (croatoan:.width win) 2)))) - (croatoan:clear win) - (croatoan:box win) - (croatoan:move win 1 1) - (dolist (d descr) - (croatoan:add-string win d) - (croatoan:move win - (1+ (first (croatoan:.cursor-position win))) 1)) - (croatoan:refresh win))) - -;;FIXME needs to be overhauled for client/server -(let ((news '("Press h for help."))) - (defun draw-news-panel (win) - "Draw a thin panel at the bottom of the screen to display news items." - (croatoan:clear win) - (croatoan:move win 0 0) - ;;TODO get news from server - (croatoan:add-string win (car news)) - (croatoan:refresh win)) - - (defun notify (news-string &rest formats) - "Append a string to the news to notify the user." - ;;TODO needs to be moved to the server - ;;A bit of a kluge, but means that `notify' supports formatting - (setf news - (cons (apply #'format (cons NIL (cons news-string formats))) - news))) - - (defun message-window () - "Return a dialog window with the last game messages." - ;;TODO complete - ;;TODO get news from server - ;;XXX use `user-inform' instead of dialog-window? - (make-instance 'croatoan:dialog-window - :input-blocking t - :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) - :center t - :border t - :stacked t - :layout nil - :title "Game messages" - :max-item-length 50 - :message-height 2 - :message-text "Press b to go back."))) - ;;:event-handlers '((#\b #'exit-event-loop))))) - -;;TODO command/chat window - -(defun process-command (event) - ;;TODO - ) diff --git a/package.lisp b/package.lisp deleted file mode 100644 index 5e0dcc4..0000000 --- a/package.lisp +++ /dev/null @@ -1,42 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This is file defines the naledi-ya-africa package. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(defpackage :naledi-ya-africa - (:documentation "A multiplayer survival game set in Africa.") - (:nicknames :naledi :nya) - (:use :common-lisp) - ;;XXX export anything else? - (:export - ;; standard game function - start-game - ;; server functions - start-server - runningp - age-of-the-world - online-players - terminate - init-world - ;; client functions - use with caution! - ;; XXX security risk? - connect-server - query-server - disconnect - ;; global variables - *host* - *port*)) - -;;set debug level during development -;;(declaim (optimize (debug 3))) - -;;convenience function -(defun start () (nya:start-game)) - -;;XXX utility function during development, remove later -(defun dt (&optional (n 0)) - (bt:destroy-thread (nth n (bt:all-threads)))) diff --git a/src/client.lisp b/src/client.lisp deleted file mode 100644 index fd84476..0000000 --- a/src/client.lisp +++ /dev/null @@ -1,49 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for connecting to the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(let ((naledi-server NIL)) - (defun connect-server (&optional (ip *host*) (port *port*)) - "Connect to the specified server" - ;;FIXME I need to catch some exceptions here... - (setf naledi-server (usocket:socket-connect ip port)) - (if naledi-server - (notify "Connected to server ~A:~A" ip port) - (notify "Connection to server ~A:~A failed." ip port))) - - (defun current-server () naledi-server) ;TODO remove after development - - (defun query-server (&rest request) - "Send a request string to the server and return the answer" - (unless naledi-server ;XXX do this with exceptions - (return-from query-server "You are not connected to a server!")) - (let ((servstr (usocket:socket-stream naledi-server)) - (req (string-from-list request))) - (logging "CLIENT: sending request ~S" request) - (format servstr "~A~%" req) - (finish-output servstr) - (usocket:wait-for-input naledi-server) - (let ((reply (read-line servstr nil))) - ;;XXX do I need to be able to read multiple lines? - ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) - ;; (multiple-value-list (read-line servstr nil))) - ;; (reply-line (first raw-reply) (first raw-reply)) - ;; (eof (second raw-reply) (second raw-reply)) - ;; (reply reply-line (sconc reply reply-line))) - ;; ((or eof (null reply-line)) ;;FIXME - (logging "CLIENT: received reply ~A" reply) - (read-from-string reply)))) - - (defun disconnect () - "Disconnect from the server" - (when naledi-server - (usocket:socket-close naledi-server) - (setf naledi-server NIL) - (notify "Disconnected from server.")))) diff --git a/src/client/crt-ext.lisp b/src/client/crt-ext.lisp new file mode 100644 index 0000000..187a864 --- /dev/null +++ b/src/client/crt-ext.lisp @@ -0,0 +1,76 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds extension functions for croatoan. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) + "Display a popup asking the user to enter a value, then return that value" + ;; I found croatoan's field/form to complicated to use (:form-default-keymap + ;; doesn't seem to work), so I hacked up my own alternative + ;; TODO display additional lines of text + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) + (y0 (- (halve (croatoan:.height scr)) 3)) + (inputwin (make-instance 'croatoan:window :position (list y0 x0) + :width (+ 10 val-width) :height 6))) + (setf (croatoan:.visible inputwin) t) + (croatoan:box inputwin) + (croatoan:add-string inputwin msg :y 2 :x 4) + (croatoan:add-string inputwin ">>> " :y 3 :x 4) + (croatoan:add-string inputwin (to-string default) :y 3 :x 8 + :color-pair '(:blue :black)) + (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) + (croatoan:refresh inputwin) + (croatoan:event-case (scr event) + ((nil) nil) + (:backspace + (when (< 8 (croatoan:.cursor-position-x inputwin)) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:add-char inputwin #\space) + (croatoan:move inputwin 0 -1 :relative t) + (croatoan:refresh inputwin))) + (#\newline + (return-from croatoan:event-case + (trim-whitespace + (croatoan:extract-string inputwin + :y 3 :x 8 :n val-width)))) + (otherwise + (when (and (characterp event) + (> (+ 8 val-width) + (croatoan:.cursor-position-x inputwin))) + (croatoan:add-char inputwin event + :color-pair '(:blue :black)) + (croatoan:refresh inputwin)))))) + +(defun user-confirm-p (scr msg &optional (cls NIL)) + "Ask the user to confirm (Yes/No) a message." + (when cls (croatoan:clear scr)) + (croatoan:refresh scr) + (let ((dw (make-instance 'croatoan:dialog-window + :center t :border t :width (max 20 (+ 4 (length msg))) + :max-item-length (max 4 (halve (length msg))) + :input-blocking t :layout '(1 2) + :message-height 1 :message-text msg + :items '(" Yes" " No")))) + (croatoan:draw-menu dw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:left :right) + (croatoan:update-menu dw event) + (croatoan:draw-menu dw)) + (#\newline + (return-from croatoan:event-case + (equalp (croatoan:.current-item dw) " Yes")))))) + +(defun inform-user (scr msg) + "Display an informational message to the user" + ;;TODO + ) + diff --git a/src/client/networking.lisp b/src/client/networking.lisp new file mode 100644 index 0000000..fd84476 --- /dev/null +++ b/src/client/networking.lisp @@ -0,0 +1,49 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for connecting to the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(let ((naledi-server NIL)) + (defun connect-server (&optional (ip *host*) (port *port*)) + "Connect to the specified server" + ;;FIXME I need to catch some exceptions here... + (setf naledi-server (usocket:socket-connect ip port)) + (if naledi-server + (notify "Connected to server ~A:~A" ip port) + (notify "Connection to server ~A:~A failed." ip port))) + + (defun current-server () naledi-server) ;TODO remove after development + + (defun query-server (&rest request) + "Send a request string to the server and return the answer" + (unless naledi-server ;XXX do this with exceptions + (return-from query-server "You are not connected to a server!")) + (let ((servstr (usocket:socket-stream naledi-server)) + (req (string-from-list request))) + (logging "CLIENT: sending request ~S" request) + (format servstr "~A~%" req) + (finish-output servstr) + (usocket:wait-for-input naledi-server) + (let ((reply (read-line servstr nil))) + ;;XXX do I need to be able to read multiple lines? + ;; (do* ((raw-reply (multiple-value-list (read-line servstr nil)) + ;; (multiple-value-list (read-line servstr nil))) + ;; (reply-line (first raw-reply) (first raw-reply)) + ;; (eof (second raw-reply) (second raw-reply)) + ;; (reply reply-line (sconc reply reply-line))) + ;; ((or eof (null reply-line)) ;;FIXME + (logging "CLIENT: received reply ~A" reply) + (read-from-string reply)))) + + (defun disconnect () + "Disconnect from the server" + (when naledi-server + (usocket:socket-close naledi-server) + (setf naledi-server NIL) + (notify "Disconnected from server.")))) diff --git a/src/client/user-interface.lisp b/src/client/user-interface.lisp new file mode 100644 index 0000000..55964a3 --- /dev/null +++ b/src/client/user-interface.lisp @@ -0,0 +1,230 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file holds the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) +;;(use-package :croatoan) + + +(defun start-game () + "Start the game logic and UI" + (croatoan:with-screen (scr :input-blocking *framerate* :enable-colors t + :input-echoing nil :cursor-visibility nil + :input-reading :unbuffered) + (splash-screen scr) + (start-or-connect-to-server scr) + (user-interface scr))) + +(defun splash-screen (scr) + "Display the splash screen with the `Naledi ya Africa' logo" + (let* ((width (croatoan:.width scr)) (height (croatoan:.height scr)) + ;;FIXME fails when not in the naledi directory + (logo (load-text-file "LOGO")) + (y (halve (- height (length logo)))) + (xoff (halve (- width 80)))) + (croatoan:clear scr) + (dolist (l logo) + (croatoan:move scr y xoff) + (croatoan:add-string scr l) + (incf y)) + (croatoan:move scr (1- height) 0) + (croatoan:add-string scr "Press any key to continue.") + (croatoan:move scr (1- height) (- width 22)) + (croatoan:add-string scr "(c) 2018 Daniel Vedder") + (croatoan:event-case (scr event) + ((nil) nil) + (otherwise (return-from croatoan:event-case))))) + +(defun start-or-connect-to-server (scr) + "Choose whether to start a local game or connect to a remote server" + (croatoan:clear scr) + (croatoan:refresh scr) + (let ((mw (make-instance 'croatoan:dialog-window + :title "Welcome!" + :center t + :border t + :width 50 + :max-item-length 42 + :input-blocking t + :cyclic-selection t + :current-item-mark "* " + :items '("Start a local game" + ;;TODO "Load a saved game" + "Connect to a remote server")))) + ;;XXX I have to effectively reimplement (select-item menu) because + ;; the screen grabs all user input and none arrives at the window + (croatoan:draw-menu mw) + (croatoan:event-case (scr event) + ((nil) nil) + ((:up :down) + (croatoan:update-menu mw event) + (croatoan:draw-menu mw)) + (#\newline + (cond ((= (croatoan:.current-item-number mw) 0) + (start-local-game scr)) + ((= (croatoan:.current-item-number mw) 1) + (connect-remote-game scr))) + (return-from croatoan:event-case))))) + +(defun start-local-game (scr) + "Start a local game" + ;;TODO choose world size + (start-server) + (setf *host* (first *defaulthost*) + *port* (second *defaulthost*)) + ;;give the server time to start + (while (not (runningp)) ;;TODO replace with `loop' + (sleep 0.5)) + (connect-server) + (query-server "signup localuser default")) ;;just use a default user + +;;TODO (choose-world-size) + +(defun connect-remote-game (scr) + "Ask the user which server to connect to and do so" + (setf *host* (query-user scr "Server IP/URL:" :cls T)) + (setf *port* (read-from-string + (query-user scr "Server port:" + :default (second *defaulthost*) :cls T))) + (if (user-confirm-p scr (format nil "Connect to ~A:~S?" *host* *port*) T) + ;;TODO ask for username/password! + (connect-server) + (start-or-connect-to-server scr))) + +(defun user-interface (scr) + "Create the screen on the ncurses interface and hand over to window functions" + (let* ((width (croatoan:.width scr)) (height (1- (croatoan:.height scr))) + (me (list (round (/ width 4)) (halve height)))) + (croatoan:clear scr) + (croatoan:refresh scr) + (croatoan:with-windows ((mapwin :position '(0 0) + :input-blocking *framerate* + :border t :width (- width 51) + :height height) + (playerwin :position (list 0 (- width 50)) + :input-blocking *framerate* :border t + :width 50 :height (halve height 'down)) + (placewin :input-blocking *framerate* + :border t + :position (list (halve height) + (- width 50)) + :width 50 :height (halve height 'down)) + (newswin :input-blocking *framerate* + :position (list height 0) + :width width :height 1)) + (update-ui mapwin playerwin placewin newswin me) + ;;TODO + (croatoan:event-case (scr event) + (#\q (disconnect) + ;;terminate if we're running a local game + (when (member "server-thread" (bt:all-threads) :test + #'(lambda (nm th) + (equalp nm (bt:thread-name th)))) + (terminate)) + (return-from croatoan:event-case)) + (#\n (croatoan:draw-menu (message-window))) + ;;FIXME change directions to single letters + (:up (query-server "move north") + (update-ui mapwin playerwin placewin newswin me)) + (:down (query-server "move south") + (update-ui mapwin playerwin placewin newswin me)) + (:left (query-server "move west") + (update-ui mapwin playerwin placewin newswin me)) + (:right (query-server "move east") + (update-ui mapwin playerwin placewin newswin me)) + ((nil) (update-ui mapwin playerwin placewin newswin me)) + (otherwise (notify (string event))))))) + +(defun update-ui (mapwin playerwin placewin newswin me) + "Update all four UI elements" + (draw-map mapwin) + (draw-player-panel playerwin) + (draw-place-panel placewin me) + (draw-news-panel newswin)) + +(defun draw-map (win) + "Draw a portion of the game map in an ncurses window" + (setf (croatoan:.color-pair win) '(:white :black)) + (croatoan:box win) + (croatoan:move win 1 1) + (let* ((map-width (- (halve (croatoan:.width win) 'floor) 2)) + (map-height (1- (croatoan:.height win))) + (submap (query-server "map" map-width map-height))) + (dotimes (h map-height) + (dotimes (w map-width) + (let ((pch (aref submap w h 0)) (pcol (aref submap w h 1))) + (croatoan:add-char win pch :color-pair (list pcol black)) + (croatoan:add-char win #\space))) + (croatoan:move win (1+ h) 1))) + (croatoan:refresh win)) + +(defun draw-player-panel (win) + "Draw a panel with information about the player character." + ;;TODO + (croatoan:box win) + (croatoan:move win 1 1) + (croatoan:add-string win "This is the player panel.") + (croatoan:refresh win)) + +(defun draw-place-panel (win me) + "Draw a panel with information about the player's current location." + ;;TODO replace `describe-patch' with `query-server' + (let ((descr (break-lines (describe-patch me) + (- (croatoan:.width win) 2)))) + (croatoan:clear win) + (croatoan:box win) + (croatoan:move win 1 1) + (dolist (d descr) + (croatoan:add-string win d) + (croatoan:move win + (1+ (first (croatoan:.cursor-position win))) 1)) + (croatoan:refresh win))) + +;;FIXME needs to be overhauled for client/server +(let ((news '("Press h for help."))) + (defun draw-news-panel (win) + "Draw a thin panel at the bottom of the screen to display news items." + (croatoan:clear win) + (croatoan:move win 0 0) + ;;TODO get news from server + (croatoan:add-string win (car news)) + (croatoan:refresh win)) + + (defun notify (news-string &rest formats) + "Append a string to the news to notify the user." + ;;TODO needs to be moved to the server + ;;A bit of a kluge, but means that `notify' supports formatting + (setf news + (cons (apply #'format (cons NIL (cons news-string formats))) + news))) + + (defun message-window () + "Return a dialog window with the last game messages." + ;;TODO complete + ;;TODO get news from server + ;;XXX use `user-inform' instead of dialog-window? + (make-instance 'croatoan:dialog-window + :input-blocking t + :items (break-lines (mapcar #'(lambda (n) (sconc "* " n)) news) 50) + :center t + :border t + :stacked t + :layout nil + :title "Game messages" + :max-item-length 50 + :message-height 2 + :message-text "Press b to go back."))) + ;;:event-handlers '((#\b #'exit-event-loop))))) + +;;TODO command/chat window + +(defun process-command (event) + ;;TODO + ;;XXX move to another file? + ) + diff --git a/src/content/animals.lisp b/src/content/animals.lisp new file mode 100644 index 0000000..3454951 --- /dev/null +++ b/src/content/animals.lisp @@ -0,0 +1,71 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines animal and species structs and the various species +;;;; found in-game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX bird species? +;;XXX water species? +;;XXX small mammals -> hills + +(new-item animal impala + :strength 3 :max-health 10 + :aggression 0 :group-size 15 + :habitat '(forest grassland) + :char #\a :color :yellow + :drops NIL) ;TODO + +(new-item animal warthog + :strength 5 :max-health 10 + :aggression 2 :group-size 5 + :habitat '(forest grassland swamp hill) + :char #\p :color :magenta + :drops NIL) ;TODO + +(new-item animal lion + :strength 18 :max-health 25 + :aggression 20 :group-size 6 + :habitat '(grassland swamp desert) + :char #\C :color :red + :drops NIL) ;TODO + +(new-item animal leopard + :strength 20 :max-health 30 + :aggression 20 :group-size 1 + :habitat '(forest grassland hill) + :char #\C :color :magenta + :drops NIL) ;TODO + +(new-item animal cheetah + :strength 15 :max-health 20 + :aggression 10 :group-size 1 + :habitat '(grassland) + :char #\C :color :yellow + :drops NIL) ;TODO + +(new-item animal elephant + :strength 40 :max-health 50 + :aggression 5 :group-size 4 + :habitat '(forest grassland) + :char #\E :color :cyan + :drops NIL) ;TODO + +(new-item animal buffalo + :strength 15 :max-health 20 + :aggression 5 :group-size 10 + :habitat '(swamp grassland) + :char #\B :color :magenta + :drops NIL) ;TODO + +(new-item animal oryx + :strength 8 :max-health 12 + :aggression 0 :group-size 2 + :habitat '(desert grassland) + :char #\a :color :blue + :drops NIL) ;TODO diff --git a/src/content/biomes.lisp b/src/content/biomes.lisp new file mode 100644 index 0000000..4f20bd9 --- /dev/null +++ b/src/content/biomes.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; Biomes are habitat types, that provide default functions for patches. +;;;; This file holds the biome definitions. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;XXX lake? +;;TODO optimise frequency numbers + +(new-biome grassland + :ground "tall elephant grass" + :char #\; :col ':yellow + :features '((acacia 100) (boulder 200) + (impala 500) (warthog 600) + (lion 2400) (leopard 3000) + (cheetah 2400) (elephant 800) + (buffalo 1000))) ;TODO + +(new-biome forest + :ground "leaf litter and small shrubs" + :char #\. :col ':green + :features '((miombo 10) (acacia 15) + (warthog 500) (impala 800) + (elephant 1000) (leopard 3200))) ;TODO + +(new-biome stream + :ground "shallow flowing water" + :char #\~ :col ':blue + :features '()) ;TODO + +(new-biome swamp + :ground "short sedge grass growing on boggy black soil" + :char #\w :col ':green + :features '((warthog 500) (buffalo 400) (lion 3000))) ;TODO + +(new-biome hill + :ground "hard, stony soil" + :char #\m :col ':cyan ;XXX very bright - hurts the eyes... + :features '((boulder 20) (miombo 80) + (warthog 600) (leopard 3200))) ;TODO + +(new-biome desert + :ground "deep, drifting sand" + :char #\^ :col ':white ;XXX yellow would be better, but shows up brown + :features '((cactus 100) (boulder 200) + (oryx 800) (lion 3600))) ;TODO diff --git a/src/content/items.lisp b/src/content/items.lisp new file mode 100644 index 0000000..c0d4e5f --- /dev/null +++ b/src/content/items.lisp @@ -0,0 +1,87 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines in-game items. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;TODO split up into several files for each category + +;;; RESOURCE ITEMS + +;;TODO sand, glass, clay, bricks + +(new-item resource wood + :description "A block of wood, just right for working with" + :weight 1 :burning-product 'charcoal) + +(new-item resource charcoal + :description "A black lump of charcoal. Burns better than wood." + :weight 1) + +(new-item resource stone + :description "A fist-sized stone. What are you going to do with it?" + :weight 2) + +(new-item resource iron-ore + :description "A lump of iron ore - still needs to be smelted." + :weight 4 :burning-product 'iron) + +(new-item resource iron + :description "An iron ingot, ready for further crafting." + :weight 3) + +;;; LANDSCAPE FEATURES + +(new-item feature acacia + :description "A tall acacia tree, spreading its branches wide." + :destroy-with '(wood) + :drops '(wood) :weight 2000 + :char #\T :color :green) + +(new-item feature miombo + :description "A small, crooked miombo tree." + :destroy-with '(wood) + :drops '(wood) :weight 500 + :char #\Y :color :green) + +(new-item feature boulder + :description "A huge lump of grey basalt, sticking out of the ground." + :destroy-with '(stone) + :drops '(stone) :weight 5000 + :char #\8 :color :white) ;XXX colour :gray is not supported on Arch :-( + +(new-item feature cactus + :description "A prickly eupharbacae sticks out of the ground here - don't touch!" + :destroy-with '(axe) ;does it drop anything? + :char #\+ :color :green) + +;;TODO termite hill, palm, baobab, pond + +;;; TOOL ITEMS + +;;TODO bronze tools, weapons + +(new-item tool stone-axe + :description "An axe, crudely but effectively fashioned out of stone." + :type 'wood :level 1 + :weight 2 :craft-with '(stone wood)) + +(new-item tool iron-axe + :description "A finely honed iron axe, just right for chopping wood." + :type 'wood :level 3 + :weight 3 :craft-with '(iron wood)) + +(new-item tool stone-pickaxe + :description "A pickaxe, somewhat helplessly made from stone." + :type 'stone :level 1 + :weight 3 :craft-with '(stone stone wood)) + +(new-item tool iron-pickaxe + :description "A solid iron pickaxe. Prepare to split rock!" + :type 'stone :level 3 + :weight 4 :craft-with '(iron iron wood)) diff --git a/src/crt-ext.lisp b/src/crt-ext.lisp deleted file mode 100644 index 187a864..0000000 --- a/src/crt-ext.lisp +++ /dev/null @@ -1,76 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file holds extension functions for croatoan. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defun query-user (scr msg &key (cls NIL) (val-width 30) (default "")) - "Display a popup asking the user to enter a value, then return that value" - ;; I found croatoan's field/form to complicated to use (:form-default-keymap - ;; doesn't seem to work), so I hacked up my own alternative - ;; TODO display additional lines of text - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let* ((x0 (- (halve (croatoan:.width scr)) (+ 5 (halve val-width)))) - (y0 (- (halve (croatoan:.height scr)) 3)) - (inputwin (make-instance 'croatoan:window :position (list y0 x0) - :width (+ 10 val-width) :height 6))) - (setf (croatoan:.visible inputwin) t) - (croatoan:box inputwin) - (croatoan:add-string inputwin msg :y 2 :x 4) - (croatoan:add-string inputwin ">>> " :y 3 :x 4) - (croatoan:add-string inputwin (to-string default) :y 3 :x 8 - :color-pair '(:blue :black)) - (croatoan:move inputwin 3 (+ 8 (length (to-string default)))) - (croatoan:refresh inputwin) - (croatoan:event-case (scr event) - ((nil) nil) - (:backspace - (when (< 8 (croatoan:.cursor-position-x inputwin)) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:add-char inputwin #\space) - (croatoan:move inputwin 0 -1 :relative t) - (croatoan:refresh inputwin))) - (#\newline - (return-from croatoan:event-case - (trim-whitespace - (croatoan:extract-string inputwin - :y 3 :x 8 :n val-width)))) - (otherwise - (when (and (characterp event) - (> (+ 8 val-width) - (croatoan:.cursor-position-x inputwin))) - (croatoan:add-char inputwin event - :color-pair '(:blue :black)) - (croatoan:refresh inputwin)))))) - -(defun user-confirm-p (scr msg &optional (cls NIL)) - "Ask the user to confirm (Yes/No) a message." - (when cls (croatoan:clear scr)) - (croatoan:refresh scr) - (let ((dw (make-instance 'croatoan:dialog-window - :center t :border t :width (max 20 (+ 4 (length msg))) - :max-item-length (max 4 (halve (length msg))) - :input-blocking t :layout '(1 2) - :message-height 1 :message-text msg - :items '(" Yes" " No")))) - (croatoan:draw-menu dw) - (croatoan:event-case (scr event) - ((nil) nil) - ((:left :right) - (croatoan:update-menu dw event) - (croatoan:draw-menu dw)) - (#\newline - (return-from croatoan:event-case - (equalp (croatoan:.current-item dw) " Yes")))))) - -(defun inform-user (scr msg) - "Display an informational message to the user" - ;;TODO - ) - diff --git a/src/item-classes.lisp b/src/item-classes.lisp deleted file mode 100644 index 0350cb9..0000000 --- a/src/item-classes.lisp +++ /dev/null @@ -1,73 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS classes used in the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defclass item () - ;; The base class of all game items. - ((name :accessor .name :initarg :name :initform "") - (description :accessor .description :initarg :description :initform "") - (weight :accessor .weight :initarg :weight :initform 0) - (movable :reader movablep :initarg :movable :initform T))) - -(defclass destructable (item) - ;; An item that can be destroyed - ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) - (health :accessor .health :initarg :health :initform 1) - (drops :reader .drops :initarg :drops :initform '()))) - -(defclass craftable (item) - ;; An item that can be crafted from other items or resources - ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) - (craft-with :reader craft-with :initarg :craft-with :initform '()))) - -(defclass feature (destructable) - ;; A landscape feature such as a rock, a tree, or an animal. - ((symbol-char :reader .char :initarg :char :initform NIL) - (symbol-color :reader .color :initarg :color :initform NIL) - (x-pos :accessor .x :initarg :x :initform 0) - (y-pos :accessor .y :initarg :y :initform 0)) - (:default-initargs :movable NIL)) - -(defclass resource (item) - ;; A resource that can be gathered and used to craft items. - ((burning-product :reader .burning-product :initarg :burning-product))) - -(defclass tool (craftable) - ;; A tool or weapon that can be crafted and used in-game. - ((level :reader .level :initarg :level :initform 0) - ;;TODO the type needs some thought (add/change to efficiency?) - (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood - (condition :accessor .condition :initarg :condition :initform 100))) - -(defclass container (craftable feature) - ;; An item that can store other items (and perhaps manipulate them) - ((contains :accessor .contains :initform '()) - (max-weight :reader .max-weight :initarg :max-weight :initform -1) - (capacity :reader .capacity :initarg :capacity :initform 0))) - -(defclass animal (feature) - ;; An animal species - (;; species properties - (strength :accessor .strength :initarg :strength :initform 1) - (max-health :accessor .max-health :initarg :max-health :initform 1) - (aggression :accessor .aggression :initarg :aggression :initform 0) - (group-size :reader .group-size :initarg :group-size :initform 1) - (habitat :reader .habitat :initarg :habitat :initform '()) - ;; individual properties - (id :reader .id :initarg :id :initform -1) - (last-move :accessor .last-move :initform 0)) - (:default-initargs :destroy-with '(weapon))) - -;; Create a new class for each item type -(defmacro new-item (superclass name &body body) - `(defclass ,name (,superclass) () - (:default-initargs ,@body - :name (string-downcase (to-string ',name))))) - diff --git a/src/item-methods.lisp b/src/item-methods.lisp deleted file mode 100644 index 2e6afbd..0000000 --- a/src/item-methods.lisp +++ /dev/null @@ -1,52 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines all CLOS methods used in the game for the different -;;;; item classes. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; Default `update' and `action' methods are NOP -(defmethod update ((i item))) -(defmethod action ((i item))) - -(defmethod attack ((d destructable) (tl tool)) - "Attack a destructable item with a tool or weapon" - ;;Returns either the damaged item or the items it drops when destroyed - (if (member (class-of tl) (.destructors d)) - (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) - (.drops d) d) - (progn (notify "You cannot attack ~A with ~A." - (leading-vowl (.name d)) (leading-vowel (.name tl))) - NIL))) - -;;TODO (defmethod attack ((f feature) (tl tool))) -;;TODO (defmethod attack ((a animal) (w weapon))) - -(defmethod update ((a animal)) - (when (> (age-of-the-world) (.last-move a)) - (random-move a) - (incf (.last-move a)))) - -(defmethod random-move ((a animal)) - "Move in a random direction within the species' habitat niche" - (do* ((dir (random-elt *directions*) (random-elt *directions*)) - (next-patch (patchindir (.x a) (.y a) dir) - (patchindir (.x a) (.y a) dir)) - (ttl 10 (1- ttl))) - ((zerop ttl) - NIL) - (when (and next-patch (null (patch-occupant next-patch)) - (member (read-from-string - (biome-name (patch-biome next-patch))) - (.habitat a))) - (setf (patch-occupant (coord (.x a) (.y a))) NIL) - (setf (.x a) (first (patch-pos next-patch)) - (.y a) (second (patch-pos next-patch))) - (setf (patch-occupant next-patch) a) - (return-from random-move a)))) - diff --git a/src/naledi.lisp b/src/naledi.lisp new file mode 100644 index 0000000..e2aa372 --- /dev/null +++ b/src/naledi.lisp @@ -0,0 +1,17 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is the main program file with the user interface. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +;;convenience function +(defun start () (nya:start-game)) + +;;XXX utility function during development, remove later +(defun dt (&optional (n 0)) + (bt:destroy-thread (nth n (bt:all-threads)))) + +;;(in-package :naledi-ya-africa) diff --git a/src/package.lisp b/src/package.lisp new file mode 100644 index 0000000..8b530cd --- /dev/null +++ b/src/package.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This is file defines the naledi-ya-africa package. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(defpackage :naledi-ya-africa + (:documentation "A multiplayer survival game set in Africa.") + (:nicknames :naledi :nya) + (:use :common-lisp) + ;;XXX export anything else? + (:export + ;; standard game function + start-game + ;; server functions + start-server + runningp + age-of-the-world + online-players + terminate + init-world + ;; client functions - use with caution! + ;; XXX security risk? + connect-server + query-server + disconnect + ;; global variables + *host* + *port*)) + +;;set debug level during development +;;(declaim (optimize (debug 3))) diff --git a/src/params.lisp b/src/params.lisp deleted file mode 100644 index 6d44097..0000000 --- a/src/params.lisp +++ /dev/null @@ -1,35 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file contains all global parameters used by the game. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;;Enable debugging statements -(defparameter *debugging* NIL) ;;XXX Change back to T? - -;;Specify the logfile -(defparameter *logfile* "naledi.log") - -;;Size of the world (= length of one side of a square) -(defparameter *world-size* 250) - -;;Milliseconds between world updates and screen refreshes -(defparameter *framerate* 1000) - -;;Localhost defaults -(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 - -;;Host server address to connect to -(defparameter *host* (first *defaulthost*)) - -;;Network port to use -(defparameter *port* (second *defaulthost*)) - -;;Compass directions needed for spatial functions -(defparameter *directions* '(N NE E SE S SW W NW)) - diff --git a/src/player.lisp b/src/player.lisp deleted file mode 100644 index 712738b..0000000 --- a/src/player.lisp +++ /dev/null @@ -1,91 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file is responsible for managing player instances. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct player - (name "") - (password "") - (online NIL) - (human NIL)) - -(defclass human (animal) - ;; A human player - ;;XXX add age? - ((experience :accessor .xp :initform 0) - (level :accessor .level :initform 0) - (dexterity :accessor .dex :initarg :dex :initform 1) - (intelligence :accessor .int :initarg :int :initform 1) - (hunger :accessor .hunger :initarg :hunger :initform 10) - (tool :accessor .tool :initarg :tool :initform NIL) - (inventory :accessor .inventory :initarg :inventory - :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) - (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) - ;;XXX change habitats to (available-biomes)? -> requires load order change - (:default-initargs :habitat T)) - -(defmethod update ((h human)) - ;;TODO - ) - -;; INVENTORY HANDLING FUNCTIONS - -;;TODO convert to methods - -(defun stock-size (resource player) - "How many items of this resource type is the player carrying?" - (dolist (i (player-inventory player) 0) - (when (eq (item-name (first i)) resource) - (return-from stock-size (second i))))) - -(defun weight-carried (player) - "Sum up the total weight of all items carried" - (+ (item-weight (player-equipment player)) - (reduce #'+ (mapcar #'(lambda (i) - (* (second i) (item-weight (first i))))) - (player-inventory player)))) - -(defun pickup (item player) - "Add the item object to the inventory" - ;;Can the item be picked up at all? - (unless (item-movable item) - (notify "This item cannot be picked up.") - (return-from pickup)) - ;;Is the player strong enough to pick this up? - (unless (<= (+ (item-weight item) (weight-carried player)) - (* (player-strength player) 20)) ;XXX magic number - (notify "You are too burdened to pick this up.") - (return-from pickup)) - (dolist (inv (player-inventory player)) - ;;Resources may be stacked - (when (and (item-resource item) - (eq (item-name item) (item-name (first inv)))) - (incf (second inv)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup)) - ;;Deposit the item in an empty slot - (when (and (null (first inv)) (item-resource item) ;normal pickup - ;;XXX replace with find-if - (zerop (count-instances (item-name item) - (mapcar #'(lambda (i) (item-name (car i))))))) - (setf inv (list item 1)) - (notify "You have picked up one ~A." (item-name item)) - (return-from pickup))) - ;;If nothing has worked, the inventory is full - (notify "Your inventory is full.")) - -(defun drop (player inv-nr) - "Drop an item from the given inventory index" - ;;TODO add item back to patch - (let* ((item-entry (nth inv-nr (player-inventory player))) - (item (when item-entry (first item-entry)))) - (if (and item (> (stock-size (item-name item) 1))) - (decf (second item-entry)) - (setf item NIL (second item-entry) 0)))) - diff --git a/src/server.lisp b/src/server.lisp deleted file mode 100644 index ae45bad..0000000 --- a/src/server.lisp +++ /dev/null @@ -1,284 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file stores all game data and handles the server. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -;; TODO save and load functions -;; XXX Will probably require `make-load-form-saving-slots' - -;;; PLAYER LIST - -(let ((players NIL)) - (defun reset-players () (setf players NIL)) ;;TODO logout first - - (defun get-player (name) - (first (member name players - :test #'(lambda (s p) (equalp (player-name p) s))))) - - (defun add-player (name passwd) - (let ((np (make-player :name name :password passwd :online NIL - :human (make-instance 'human - :x (random *world-size*) - :y (random *world-size*))))) - (setf players (cons np players)))) - - (defun online-players () - "Return a list of names of players who are online" - (loop for p in players - if (player-online p) - collect (player-name p))) - - (defun save-players (file-name) - ;;TODO - )) - -(defun online-p (name) - (let ((p (get-player name))) - (when p (player-online p)))) - -;;; WORLD OBJECT - -(let ((world NIL)) - (defun set-world (w) (setf world w)) - - (defun world-size () (length world)) - - (defun save-topography (file-name) ;XXX (re)move this? - "Save the world topography as a text file" - (debugging "~&Saving world to file ~A" file-name) ;debug - (with-open-file (tf file-name :direction :output) - (dolist (row world) - (format stream "~&~A~%" - (string-from-list - (mapcar #'(lambda (p) - (biome-char (patch-biome p))) - row) ""))))) - - (defun save-world (&optional (file-name "naledi.save")) - ;;TODO - (save-players file-name)) - - (defun coord (x y) - "Return the patch at the given coordinates or NIL if out of bounds" - (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) - (nth x (nth y world))))) - -;;; WORLD THREADS - -(let ((uptime 0) (world-thread NIL) (server-thread NIL) - (player-threads NIL) (running NIL)) - - (defun start-server (&optional (force T)) - "Start the game server" - ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR - ;; -> comes from not closing connections properly? - ;;XXX change force back to NIL? - (when force - (reset-server-threads) - (reset-world-age)) - (unless (or world-thread server-thread) - (init-world) - (setf running T) - (setf world-thread - (bt:make-thread #'update-loop :name "world-thread")) - (setf server-thread - (bt:make-thread #'run-server :name "server-thread")) - (logging "SERVER: initialized"))) - - (defun terminate () - ;;TODO do some error catching if the threads no longer exist - (logging "SERVER: terminating the world.") - (setf running NIL) - ;;XXX have to use destroy-thread because the server mostly idles, - ;; waiting for connections - only checks 'running' when connecting - (bt:destroy-thread server-thread) - (bt:join-thread world-thread) - (dolist (pt player-threads) - (bt:join-thread (first pt))) - (reset-server-threads) - (save-world)) ;XXX not yet implemented - - (defun age-of-the-world () uptime) - - (defun reset-world-age () (setf uptime 0)) - - (defun runningp () running) - - (defun reset-server-threads () - (set-list NIL server-thread world-thread player-threads)) - - (defun thread-player (&optional (th (bt:current-thread))) - "Return the player name associated with this thread" - (cassoc th player-threads)) - - (defun set-thread-player (name &optional (th (bt:current-thread))) - "Set the player name associated with this thread" - ;;XXX somewhat ugly... - (setf (cassoc th player-threads) name)) - - (defun cleanup-player-threads () - "Remove threads of disconnected players" - (dotimes (i (length player-threads)) - (let ((pt (nth i player-threads))) - (when (equalp (second pt) "terminated") - (bt:join-thread (first pt)) - (setf (cdr (nth (1- i) player-threads)) - (nth (1+ i) player-threads)))))) - - (defun update-loop () - "The main loop, updating the world in the background" - ;;XXX split this up into two or more functions, to be run by - ;; different threads? (e.g. player- & world-update threads) - (logging "UPDATE ~S" uptime) - ;;Update all items and occupants in each patch - (dotimes (y (world-size)) - (dotimes (x (world-size)) - (unless running (return-from update-loop)) - (when (patch-occupant (coord x y)) - (update (patch-occupant (coord x y)))) - (dolist (i (patch-items (coord x y))) - (update i)))) - ;;Update all items each player has - ;;TODO - ;;Do cleanup work - (when (zerop (rem uptime 20)) (cleanup-player-threads)) - ;;Save the world and start over - (save-world) ;XXX not yet implemented - (incf uptime) - (sleep (/ *framerate* 1000)) - (when running (update-loop))) ;;requires Tail-Call Optimization - - (defun run-server () - "Start a server, listening for connections" - (usocket:with-socket-listener (socket "127.0.0.1" *port*) - (while running - (usocket:wait-for-input socket) - (when (eq (usocket:socket-state socket) ':read) - ;;XXX give player threads unique names again? - (let ((thread (bt:make-thread - #'(lambda () - (handle-connection socket)) - :name "player-thread"))) - (setf player-threads - (cons (list thread "anon") player-threads)) - (sleep 1)))))) ;;give the socket a chance to connect - - (defun handle-connection (socket) - "Answer requests until the player disconnects" - (usocket:with-connected-socket (conn (usocket:socket-accept socket)) - (logging "~&SERVER: received a connection") ; cf. `get-peer-name' - (do* ((sockstr (usocket:socket-stream conn)) - (request (read-line sockstr NIL) (read-line sockstr NIL))) - ((or (not running) (null request)) (logout)) - (format sockstr "~S~%" (to-string (answer request))) - (finish-output sockstr))))) - -(defun answer (request) - "Dispatch on the functions registerd in `*API*'" - ;;XXX potential problem with reading all user-received symbols into memory - (let* ((reqelts (split-string request #\space)) - (cmd (car reqelts)) - (args (cdr reqelts))) - (logging "SERVER: received request ~S" reqelts) - (if (member cmd (keys *API*) :test #'equalp) - ;;XXX Surely there must be a way to simplify the next few lines?! - (if (and (equalp (thread-player) "anon") - (not (or (equalp cmd "login") (equalp cmd "signup")))) - "ERROR: not logged in" - (apply (cassoc cmd *API* :test #'equalp) args)) - "ERROR: unknown command"))) - -;;; COMMUNICATION FUNCTIONS - -;;NOTE: all following functions receive string arguments and must convert -;; them as needed - -(defun login (name passwd) - "Log this player in" - ;;XXX name and passwd are converted to symbols by `answer'! - not currently - (logging "SERVER: ~A is trying to log in" name) - (let ((p (get-player name))) - (cond ((not p) "ERROR: nonexistent player") - ((not (equalp passwd (player-password p))) "ERROR: bad password") - (T (setf (player-online p) T) - (setf (patch-occupant ;;TODO check for previous occupants - (coord (.x (player-human p)) (.y (player-human p)))) - (player-human p)) - (set-thread-player name) - (logging "SERVER: player ~A logged in" name) - name)))) - -(defun logout () - "Log the player out again" - (let ((p (get-player (thread-player)))) - (when p - (setf (player-online p) NIL) - (setf (patch-occupant - (coord (.x (player-human p)) (.y (player-human p)))) - NIL) ;;TODO set to "statue of " - (logging "SERVER: player ~A logged out" (thread-player)) - (set-thread-player "terminated")))) - -(defun create-player (name passwd) - "Create and log in a new player" - (if (get-player name) "ERROR: player exists" - (progn - (add-player name passwd) - (login name passwd)))) - -(defun get-map (swidth sheight) - "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" - (let* ((plr (player-human (get-player (thread-player)))) - (width (read-from-string swidth)) - (height (read-from-string sheight)) - (x0 (- (.x plr) (halve width))) - (y0 (- (.y plr) (halve height))) - (submap (make-array (list width height 2)))) - (dotimes (h height) - (dotimes (w width) - (let ((p (coord (+ w x0 1) (+ h y0 1))) - (next-char #\space) (next-col ':black)) - (if (and p (patch-occupant p)) - (setf next-char (.char (patch-occupant p)) - next-col (.color (patch-occupant p))) - (when p (setf next-char (biome-char (patch-biome p)) - next-col (biome-col (patch-biome p))))) - (setf (aref submap w h 0) next-char - (aref submap w h 1) next-col)))) - ;;FIXME arrays are pretty-printed with linebreaks, this causes a - ;; client-side error (only expecting one line) - submap)) - -(defun describe-patch (coords) - "Return a list of lines describing the patch at these coordinates." - (let ((p (coord (first coords) (second coords)))) - (list (string-upcase (biome-name (patch-biome p))) "" - (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" - (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) - (when (patch-occupant p) ;TODO players -> "$name is here." - (format NIL "There is ~A here." - (leading-vowel (.name (patch-occupant p))))) - (when (patch-items p) - (format NIL "The following items are here:~A *~A" #\newline - (string-from-list (mapcar #'.name (patch-items p))) - (format NIL "~% *")))))) - -(defun move-player (dir) - "Move a player in the given direction" - ;;TODO - ) - -(defparameter *API* - ;; An alist of API commands and their corresponding functions - ;;XXX move all listed functions to another file? - (list (list "login" #'login) - (list "signup" #'create-player) - (list "map" #'get-map) - (list "describe-patch" #'describe-patch) - (list "move" #'move-player))) diff --git a/src/server/item-classes.lisp b/src/server/item-classes.lisp new file mode 100644 index 0000000..0350cb9 --- /dev/null +++ b/src/server/item-classes.lisp @@ -0,0 +1,73 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS classes used in the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defclass item () + ;; The base class of all game items. + ((name :accessor .name :initarg :name :initform "") + (description :accessor .description :initarg :description :initform "") + (weight :accessor .weight :initarg :weight :initform 0) + (movable :reader movablep :initarg :movable :initform T))) + +(defclass destructable (item) + ;; An item that can be destroyed + ((destroy-with :reader .destructors :initarg :destroy-with :initform NIL) + (health :accessor .health :initarg :health :initform 1) + (drops :reader .drops :initarg :drops :initform '()))) + +(defclass craftable (item) + ;; An item that can be crafted from other items or resources + ((requires-tool :reader requires-tool :initarg :requires-tool :initform '()) + (craft-with :reader craft-with :initarg :craft-with :initform '()))) + +(defclass feature (destructable) + ;; A landscape feature such as a rock, a tree, or an animal. + ((symbol-char :reader .char :initarg :char :initform NIL) + (symbol-color :reader .color :initarg :color :initform NIL) + (x-pos :accessor .x :initarg :x :initform 0) + (y-pos :accessor .y :initarg :y :initform 0)) + (:default-initargs :movable NIL)) + +(defclass resource (item) + ;; A resource that can be gathered and used to craft items. + ((burning-product :reader .burning-product :initarg :burning-product))) + +(defclass tool (craftable) + ;; A tool or weapon that can be crafted and used in-game. + ((level :reader .level :initarg :level :initform 0) + ;;TODO the type needs some thought (add/change to efficiency?) + (type :reader .type :initarg :type :initform NIL) ;e.g. 'weapon, 'wood + (condition :accessor .condition :initarg :condition :initform 100))) + +(defclass container (craftable feature) + ;; An item that can store other items (and perhaps manipulate them) + ((contains :accessor .contains :initform '()) + (max-weight :reader .max-weight :initarg :max-weight :initform -1) + (capacity :reader .capacity :initarg :capacity :initform 0))) + +(defclass animal (feature) + ;; An animal species + (;; species properties + (strength :accessor .strength :initarg :strength :initform 1) + (max-health :accessor .max-health :initarg :max-health :initform 1) + (aggression :accessor .aggression :initarg :aggression :initform 0) + (group-size :reader .group-size :initarg :group-size :initform 1) + (habitat :reader .habitat :initarg :habitat :initform '()) + ;; individual properties + (id :reader .id :initarg :id :initform -1) + (last-move :accessor .last-move :initform 0)) + (:default-initargs :destroy-with '(weapon))) + +;; Create a new class for each item type +(defmacro new-item (superclass name &body body) + `(defclass ,name (,superclass) () + (:default-initargs ,@body + :name (string-downcase (to-string ',name))))) + diff --git a/src/server/item-methods.lisp b/src/server/item-methods.lisp new file mode 100644 index 0000000..2e6afbd --- /dev/null +++ b/src/server/item-methods.lisp @@ -0,0 +1,52 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines all CLOS methods used in the game for the different +;;;; item classes. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; Default `update' and `action' methods are NOP +(defmethod update ((i item))) +(defmethod action ((i item))) + +(defmethod attack ((d destructable) (tl tool)) + "Attack a destructable item with a tool or weapon" + ;;Returns either the damaged item or the items it drops when destroyed + (if (member (class-of tl) (.destructors d)) + (if (>= 0 (decf (.health d) (random (* 10 (.level tl))))) + (.drops d) d) + (progn (notify "You cannot attack ~A with ~A." + (leading-vowl (.name d)) (leading-vowel (.name tl))) + NIL))) + +;;TODO (defmethod attack ((f feature) (tl tool))) +;;TODO (defmethod attack ((a animal) (w weapon))) + +(defmethod update ((a animal)) + (when (> (age-of-the-world) (.last-move a)) + (random-move a) + (incf (.last-move a)))) + +(defmethod random-move ((a animal)) + "Move in a random direction within the species' habitat niche" + (do* ((dir (random-elt *directions*) (random-elt *directions*)) + (next-patch (patchindir (.x a) (.y a) dir) + (patchindir (.x a) (.y a) dir)) + (ttl 10 (1- ttl))) + ((zerop ttl) + NIL) + (when (and next-patch (null (patch-occupant next-patch)) + (member (read-from-string + (biome-name (patch-biome next-patch))) + (.habitat a))) + (setf (patch-occupant (coord (.x a) (.y a))) NIL) + (setf (.x a) (first (patch-pos next-patch)) + (.y a) (second (patch-pos next-patch))) + (setf (patch-occupant next-patch) a) + (return-from random-move a)))) + diff --git a/src/server/params.lisp b/src/server/params.lisp new file mode 100644 index 0000000..6d44097 --- /dev/null +++ b/src/server/params.lisp @@ -0,0 +1,35 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file contains all global parameters used by the game. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;;Enable debugging statements +(defparameter *debugging* NIL) ;;XXX Change back to T? + +;;Specify the logfile +(defparameter *logfile* "naledi.log") + +;;Size of the world (= length of one side of a square) +(defparameter *world-size* 250) + +;;Milliseconds between world updates and screen refreshes +(defparameter *framerate* 1000) + +;;Localhost defaults +(defparameter *defaulthost* '("127.0.0.1" 21895)) ;default port: 21895 + +;;Host server address to connect to +(defparameter *host* (first *defaulthost*)) + +;;Network port to use +(defparameter *port* (second *defaulthost*)) + +;;Compass directions needed for spatial functions +(defparameter *directions* '(N NE E SE S SW W NW)) + diff --git a/src/server/player.lisp b/src/server/player.lisp new file mode 100644 index 0000000..712738b --- /dev/null +++ b/src/server/player.lisp @@ -0,0 +1,91 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file is responsible for managing player instances. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct player + (name "") + (password "") + (online NIL) + (human NIL)) + +(defclass human (animal) + ;; A human player + ;;XXX add age? + ((experience :accessor .xp :initform 0) + (level :accessor .level :initform 0) + (dexterity :accessor .dex :initarg :dex :initform 1) + (intelligence :accessor .int :initarg :int :initform 1) + (hunger :accessor .hunger :initarg :hunger :initform 10) + (tool :accessor .tool :initarg :tool :initform NIL) + (inventory :accessor .inventory :initarg :inventory + :initform '((NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0) + (NIL 0) (NIL 0) (NIL 0) (NIL 0) (NIL 0)))) + ;;XXX change habitats to (available-biomes)? -> requires load order change + (:default-initargs :habitat T)) + +(defmethod update ((h human)) + ;;TODO + ) + +;; INVENTORY HANDLING FUNCTIONS + +;;TODO convert to methods + +(defun stock-size (resource player) + "How many items of this resource type is the player carrying?" + (dolist (i (player-inventory player) 0) + (when (eq (item-name (first i)) resource) + (return-from stock-size (second i))))) + +(defun weight-carried (player) + "Sum up the total weight of all items carried" + (+ (item-weight (player-equipment player)) + (reduce #'+ (mapcar #'(lambda (i) + (* (second i) (item-weight (first i))))) + (player-inventory player)))) + +(defun pickup (item player) + "Add the item object to the inventory" + ;;Can the item be picked up at all? + (unless (item-movable item) + (notify "This item cannot be picked up.") + (return-from pickup)) + ;;Is the player strong enough to pick this up? + (unless (<= (+ (item-weight item) (weight-carried player)) + (* (player-strength player) 20)) ;XXX magic number + (notify "You are too burdened to pick this up.") + (return-from pickup)) + (dolist (inv (player-inventory player)) + ;;Resources may be stacked + (when (and (item-resource item) + (eq (item-name item) (item-name (first inv)))) + (incf (second inv)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup)) + ;;Deposit the item in an empty slot + (when (and (null (first inv)) (item-resource item) ;normal pickup + ;;XXX replace with find-if + (zerop (count-instances (item-name item) + (mapcar #'(lambda (i) (item-name (car i))))))) + (setf inv (list item 1)) + (notify "You have picked up one ~A." (item-name item)) + (return-from pickup))) + ;;If nothing has worked, the inventory is full + (notify "Your inventory is full.")) + +(defun drop (player inv-nr) + "Drop an item from the given inventory index" + ;;TODO add item back to patch + (let* ((item-entry (nth inv-nr (player-inventory player))) + (item (when item-entry (first item-entry)))) + (if (and item (> (stock-size (item-name item) 1))) + (decf (second item-entry)) + (setf item NIL (second item-entry) 0)))) + diff --git a/src/server/server.lisp b/src/server/server.lisp new file mode 100644 index 0000000..ae45bad --- /dev/null +++ b/src/server/server.lisp @@ -0,0 +1,284 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file stores all game data and handles the server. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +;; TODO save and load functions +;; XXX Will probably require `make-load-form-saving-slots' + +;;; PLAYER LIST + +(let ((players NIL)) + (defun reset-players () (setf players NIL)) ;;TODO logout first + + (defun get-player (name) + (first (member name players + :test #'(lambda (s p) (equalp (player-name p) s))))) + + (defun add-player (name passwd) + (let ((np (make-player :name name :password passwd :online NIL + :human (make-instance 'human + :x (random *world-size*) + :y (random *world-size*))))) + (setf players (cons np players)))) + + (defun online-players () + "Return a list of names of players who are online" + (loop for p in players + if (player-online p) + collect (player-name p))) + + (defun save-players (file-name) + ;;TODO + )) + +(defun online-p (name) + (let ((p (get-player name))) + (when p (player-online p)))) + +;;; WORLD OBJECT + +(let ((world NIL)) + (defun set-world (w) (setf world w)) + + (defun world-size () (length world)) + + (defun save-topography (file-name) ;XXX (re)move this? + "Save the world topography as a text file" + (debugging "~&Saving world to file ~A" file-name) ;debug + (with-open-file (tf file-name :direction :output) + (dolist (row world) + (format stream "~&~A~%" + (string-from-list + (mapcar #'(lambda (p) + (biome-char (patch-biome p))) + row) ""))))) + + (defun save-world (&optional (file-name "naledi.save")) + ;;TODO + (save-players file-name)) + + (defun coord (x y) + "Return the patch at the given coordinates or NIL if out of bounds" + (unless (or (< x 0) (< y 0) (> x (length world)) (> y (length world))) + (nth x (nth y world))))) + +;;; WORLD THREADS + +(let ((uptime 0) (world-thread NIL) (server-thread NIL) + (player-threads NIL) (running NIL)) + + (defun start-server (&optional (force T)) + "Start the game server" + ;;FIXME cannot restart -> ADDRESS-IN-USE ERROR + ;; -> comes from not closing connections properly? + ;;XXX change force back to NIL? + (when force + (reset-server-threads) + (reset-world-age)) + (unless (or world-thread server-thread) + (init-world) + (setf running T) + (setf world-thread + (bt:make-thread #'update-loop :name "world-thread")) + (setf server-thread + (bt:make-thread #'run-server :name "server-thread")) + (logging "SERVER: initialized"))) + + (defun terminate () + ;;TODO do some error catching if the threads no longer exist + (logging "SERVER: terminating the world.") + (setf running NIL) + ;;XXX have to use destroy-thread because the server mostly idles, + ;; waiting for connections - only checks 'running' when connecting + (bt:destroy-thread server-thread) + (bt:join-thread world-thread) + (dolist (pt player-threads) + (bt:join-thread (first pt))) + (reset-server-threads) + (save-world)) ;XXX not yet implemented + + (defun age-of-the-world () uptime) + + (defun reset-world-age () (setf uptime 0)) + + (defun runningp () running) + + (defun reset-server-threads () + (set-list NIL server-thread world-thread player-threads)) + + (defun thread-player (&optional (th (bt:current-thread))) + "Return the player name associated with this thread" + (cassoc th player-threads)) + + (defun set-thread-player (name &optional (th (bt:current-thread))) + "Set the player name associated with this thread" + ;;XXX somewhat ugly... + (setf (cassoc th player-threads) name)) + + (defun cleanup-player-threads () + "Remove threads of disconnected players" + (dotimes (i (length player-threads)) + (let ((pt (nth i player-threads))) + (when (equalp (second pt) "terminated") + (bt:join-thread (first pt)) + (setf (cdr (nth (1- i) player-threads)) + (nth (1+ i) player-threads)))))) + + (defun update-loop () + "The main loop, updating the world in the background" + ;;XXX split this up into two or more functions, to be run by + ;; different threads? (e.g. player- & world-update threads) + (logging "UPDATE ~S" uptime) + ;;Update all items and occupants in each patch + (dotimes (y (world-size)) + (dotimes (x (world-size)) + (unless running (return-from update-loop)) + (when (patch-occupant (coord x y)) + (update (patch-occupant (coord x y)))) + (dolist (i (patch-items (coord x y))) + (update i)))) + ;;Update all items each player has + ;;TODO + ;;Do cleanup work + (when (zerop (rem uptime 20)) (cleanup-player-threads)) + ;;Save the world and start over + (save-world) ;XXX not yet implemented + (incf uptime) + (sleep (/ *framerate* 1000)) + (when running (update-loop))) ;;requires Tail-Call Optimization + + (defun run-server () + "Start a server, listening for connections" + (usocket:with-socket-listener (socket "127.0.0.1" *port*) + (while running + (usocket:wait-for-input socket) + (when (eq (usocket:socket-state socket) ':read) + ;;XXX give player threads unique names again? + (let ((thread (bt:make-thread + #'(lambda () + (handle-connection socket)) + :name "player-thread"))) + (setf player-threads + (cons (list thread "anon") player-threads)) + (sleep 1)))))) ;;give the socket a chance to connect + + (defun handle-connection (socket) + "Answer requests until the player disconnects" + (usocket:with-connected-socket (conn (usocket:socket-accept socket)) + (logging "~&SERVER: received a connection") ; cf. `get-peer-name' + (do* ((sockstr (usocket:socket-stream conn)) + (request (read-line sockstr NIL) (read-line sockstr NIL))) + ((or (not running) (null request)) (logout)) + (format sockstr "~S~%" (to-string (answer request))) + (finish-output sockstr))))) + +(defun answer (request) + "Dispatch on the functions registerd in `*API*'" + ;;XXX potential problem with reading all user-received symbols into memory + (let* ((reqelts (split-string request #\space)) + (cmd (car reqelts)) + (args (cdr reqelts))) + (logging "SERVER: received request ~S" reqelts) + (if (member cmd (keys *API*) :test #'equalp) + ;;XXX Surely there must be a way to simplify the next few lines?! + (if (and (equalp (thread-player) "anon") + (not (or (equalp cmd "login") (equalp cmd "signup")))) + "ERROR: not logged in" + (apply (cassoc cmd *API* :test #'equalp) args)) + "ERROR: unknown command"))) + +;;; COMMUNICATION FUNCTIONS + +;;NOTE: all following functions receive string arguments and must convert +;; them as needed + +(defun login (name passwd) + "Log this player in" + ;;XXX name and passwd are converted to symbols by `answer'! - not currently + (logging "SERVER: ~A is trying to log in" name) + (let ((p (get-player name))) + (cond ((not p) "ERROR: nonexistent player") + ((not (equalp passwd (player-password p))) "ERROR: bad password") + (T (setf (player-online p) T) + (setf (patch-occupant ;;TODO check for previous occupants + (coord (.x (player-human p)) (.y (player-human p)))) + (player-human p)) + (set-thread-player name) + (logging "SERVER: player ~A logged in" name) + name)))) + +(defun logout () + "Log the player out again" + (let ((p (get-player (thread-player)))) + (when p + (setf (player-online p) NIL) + (setf (patch-occupant + (coord (.x (player-human p)) (.y (player-human p)))) + NIL) ;;TODO set to "statue of " + (logging "SERVER: player ~A logged out" (thread-player)) + (set-thread-player "terminated")))) + +(defun create-player (name passwd) + "Create and log in a new player" + (if (get-player name) "ERROR: player exists" + (progn + (add-player name passwd) + (login name passwd)))) + +(defun get-map (swidth sheight) + "Return a 3d array (x-coord, y-coord, character/colour) of the visible map" + (let* ((plr (player-human (get-player (thread-player)))) + (width (read-from-string swidth)) + (height (read-from-string sheight)) + (x0 (- (.x plr) (halve width))) + (y0 (- (.y plr) (halve height))) + (submap (make-array (list width height 2)))) + (dotimes (h height) + (dotimes (w width) + (let ((p (coord (+ w x0 1) (+ h y0 1))) + (next-char #\space) (next-col ':black)) + (if (and p (patch-occupant p)) + (setf next-char (.char (patch-occupant p)) + next-col (.color (patch-occupant p))) + (when p (setf next-char (biome-char (patch-biome p)) + next-col (biome-col (patch-biome p))))) + (setf (aref submap w h 0) next-char + (aref submap w h 1) next-col)))) + ;;FIXME arrays are pretty-printed with linebreaks, this causes a + ;; client-side error (only expecting one line) + submap)) + +(defun describe-patch (coords) + "Return a list of lines describing the patch at these coordinates." + (let ((p (coord (first coords) (second coords)))) + (list (string-upcase (biome-name (patch-biome p))) "" + (format NIL "~A / ~A" (first (patch-pos p)) (second (patch-pos p))) "" + (format NIL "The ground here is ~A." (biome-ground (patch-biome p))) + (when (patch-occupant p) ;TODO players -> "$name is here." + (format NIL "There is ~A here." + (leading-vowel (.name (patch-occupant p))))) + (when (patch-items p) + (format NIL "The following items are here:~A *~A" #\newline + (string-from-list (mapcar #'.name (patch-items p))) + (format NIL "~% *")))))) + +(defun move-player (dir) + "Move a player in the given direction" + ;;TODO + ) + +(defparameter *API* + ;; An alist of API commands and their corresponding functions + ;;XXX move all listed functions to another file? + (list (list "login" #'login) + (list "signup" #'create-player) + (list "map" #'get-map) + (list "describe-patch" #'describe-patch) + (list "move" #'move-player))) diff --git a/src/server/world.lisp b/src/server/world.lisp new file mode 100644 index 0000000..5365554 --- /dev/null +++ b/src/server/world.lisp @@ -0,0 +1,187 @@ +;;;; +;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game +;;;; set in Africa. +;;;; +;;;; This file defines patches and administrates the world object. +;;;; +;;;; (c) 2018 Daniel Vedder, MIT license +;;;; + +(in-package :naledi-ya-africa) + +(defstruct patch + (pos '(0 0)) ;position + (biome NIL) + (items '()) + (occupant NIL)) + +(defstruct biome + (name "") + (ground "") + (features '()) ;an alist of possible features and their 1/p probabilities + (char #\.) ;default map display character + (col ':white)) ;default map display colour + +;; BIOME LIST + +(let ((biome-list NIL)) + (defun register-biome (symbol-name biome-object) + (setf biome-list (cons (list symbol-name biome-object) biome-list))) + + (defun available-biomes () + (keys biome-list)) + + (defun get-biome (symbol-name) + (cassoc symbol-name biome-list))) + +(defmacro new-biome (name &body body) + `(register-biome ',name + (make-biome + :name ,(symbol-to-string name) + ,@body))) + +;; MATRIX FUNCTIONS + +(defun init-matrix (size) + "Create a square matrix of empty patches" + ;;TODO change this to arrays for performance + (debugging "~&Creating a ~S/~S matrix." size size) + (do ((y 0 (1+ y)) (world NIL) (row NIL NIL)) + ((= y size) world) + (dotimes (x size) + (setf row (append row (list (make-patch :pos (list x y)))))) + (setf world (append world (list row))))) + +(defun distance (x1 y1 x2 y2 &optional (pythag NIL)) + "Find the distance between two sets of coordinates" + (if pythag (round (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2)))) + (min (abs (- x1 x2)) (abs (- y1 y2))))) + +(defun closest-coords (here coord-list &optional (abs-dist NIL)) + "Find the closest position to 'here' from a list of coordinates" + (do* ((clist coord-list (cdr clist)) (c (car clist) (car clist)) + (dist (when c (distance (first here) (second here) + (first c) (second c) abs-dist)) + (when c (distance (first here) (second here) + (first c) (second c) abs-dist))) + (mindist dist) (closest c)) + ((null clist) closest) + (when (< dist mindist) + (setf mindist dist closest c)))) + +(defun opposite-dir (dir) + "Return the direction opposite the input" + (let ((pos (position dir *directions*))) + (when pos (nth (rem (+ 4 pos) 8) *directions*)))) + +(defun next-dir (dir &optional (cw T)) + "Get the neighbouring direction (clockwise or anticlockwise)" + (let ((pos (position dir *directions*)) + (diff (if cw 1 -1))) + (when pos (nth (rem (+ diff pos 8) 8) *directions*)))) + +(defun orth-dir (dir &optional (cw T)) + "Get the direction orthogonal (at right angles) to the given one." + (next-dir (next-dir dir cw) cw)) + +(defun diagonalp (dir) + "Is dir a diagonal direction?" + (member dir '(NE SE SW NW) :test #'eq)) + +(defun dir2patch (herex herey therex therey) + "Calculate the direction to a patch" + (cond ((> herex therex) + (cond ((> herey therey) 'NW) + ((< herey therey) 'SW) + (T 'W))) + ((< herex therex) + (cond ((> herey therey) 'NE) + ((< herey therey) 'SE) + (T 'E))) + (T (cond ((> herey therey) 'N) + ((< herey therey) 'S) + (T NIL))))) + +(defun coordsindir (x y dir) + "Return the coordinates in the given direction" + (cond ((eq dir 'N) (list x (1- y))) + ((eq dir 'NE) (list (1+ x) (1- y))) + ((eq dir 'E) (list (1+ x) y)) + ((eq dir 'SE) (list (1+ x) (1+ y))) + ((eq dir 'S) (list x (1+ y))) + ((eq dir 'SW) (list (1- x) (1+ y))) + ((eq dir 'W) (list (1- x) y)) + ((eq dir 'NW) (list (1- x) (1- y))) + ((null dir) (list x y)) + (T (error "~&Invalid direction ~S")))) + +(defun patchindir (x y dir) + "Return the patch in the given direction" + (let* ((coords (coordsindir x y dir)) + (nextx (first coords)) (nexty (second coords))) + (coord nextx nexty))) + +(defun neighbour (p dir) + "Return the neighbouring patch in this direction" + (patchindir (first (patch-pos p)) (second (patch-pos p)) dir)) + + +;; WORLD CREATION FUNCTIONS + +(defun get-patch-feature (patch) + "Find a random feature (or none) to occupy this patch." + (let ((flist (biome-features (patch-biome patch)))) + (dolist (f flist NIL) + (when (chancep (second f)) + (return-from get-patch-feature + (make-instance (first f) :x (first (patch-pos patch)) + :y (second (patch-pos patch)))))))) + +(defun generate-biomes (size-factor) + ;;XXX The maps this produces don't look quite as expected, but for + ;; current purposes they are good enough + (debugging "~&Generating biomes") ;debug + (let* ((wsize (world-size)) (seeds NIL) + (nseeds (round (/ wsize size-factor))) + (biomes (remove-first-if + #'(lambda (e) (eq e 'stream)) + (available-biomes)))) + ;;Initialize a set of biome 'seed' coordinates + (dotimes (n nseeds) + (setf seeds + (cons (list (random wsize) + (random wsize) + (random-elt biomes)) + seeds))) + (debugging "~&~S" seeds) + ;;For each patch, calculate the closest seed and set to that biome + (dotimes (x wsize seeds) + (dotimes (y wsize) + (let ((p (coord x y)) + (b (third (closest-coords (list x y) seeds T)))) + (setf (patch-biome p) (get-biome b)) + (setf (patch-occupant p) (get-patch-feature p))))))) + +(defun generate-stream (x0 y0) + (debugging "~&Generating a stream, starting at ~S/~S" x0 y0) ;debug + (do* ((dir (random-elt *directions*) + (if (probabilityp 75) dir (next-dir dir (random-elt '(T NIL))))) + (patch (coord x0 y0) (neighbour patch dir))) + ((or (null patch) (eq (patch-biome patch) (get-biome 'stream)))) + (setf (patch-biome patch) (get-biome 'stream)) + (setf (patch-occupant patch) NIL))) + +(defun create-world (size) + "Create a world of the specified size (square)" + (set-world (init-matrix size)) + ;;XXX magic numbers + (generate-biomes 10) + (dotimes (s (round (/ (expt size 2) 2000))) + (generate-stream (random size) (random size)))) + +(defun init-world () + "Initialize the log, RNG, and world." + (write-to-file "NALEDI ya AFRICA" *logfile*) + (logging (time-stamp)) + (setf *random-state* (make-random-state t)) + (create-world *world-size*)) diff --git a/src/util.lisp b/src/util.lisp index 28b1b6f..909b6ac 100644 --- a/src/util.lisp +++ b/src/util.lisp @@ -12,6 +12,8 @@ ;;; MACROS +;; --- DEPRECATED --- +;; (use `logging' with log-level >= 3 instead) (defmacro debugging (str &rest format-args) "If *debugging* is true, print str" `(when *debugging* (format t ,str ,@format-args))) diff --git a/src/world.lisp b/src/world.lisp deleted file mode 100644 index 5365554..0000000 --- a/src/world.lisp +++ /dev/null @@ -1,187 +0,0 @@ -;;;; -;;;; Naledi ya Africa ("Star of Africa") is an ncurses-based survival game -;;;; set in Africa. -;;;; -;;;; This file defines patches and administrates the world object. -;;;; -;;;; (c) 2018 Daniel Vedder, MIT license -;;;; - -(in-package :naledi-ya-africa) - -(defstruct patch - (pos '(0 0)) ;position - (biome NIL) - (items '()) - (occupant NIL)) - -(defstruct biome - (name "") - (ground "") - (features '()) ;an alist of possible features and their 1/p probabilities - (char #\.) ;default map display character - (col ':white)) ;default map display colour - -;; BIOME LIST - -(let ((biome-list NIL)) - (defun register-biome (symbol-name biome-object) - (setf biome-list (cons (list symbol-name biome-object) biome-list))) - - (defun available-biomes () - (keys biome-list)) - - (defun get-biome (symbol-name) - (cassoc symbol-name biome-list))) - -(defmacro new-biome (name &body body) - `(register-biome ',name - (make-biome - :name ,(symbol-to-string name) - ,@body))) - -;; MATRIX FUNCTIONS - -(defun init-matrix (size) - "Create a square matrix of empty patches" - ;;TODO change this to arrays for performance - (debugging "~&Creating a ~S/~S matrix." size size) - (do ((y 0 (1+ y)) (world NIL) (row NIL NIL)) - ((= y size) world) - (dotimes (x size) - (setf row (append row (list (make-patch :pos (list x y)))))) - (setf world (append world (list row))))) - -(defun distance (x1 y1 x2 y2 &optional (pythag NIL)) - "Find the distance between two sets of coordinates" - (if pythag (round (sqrt (+ (expt (- x1 x2) 2) (expt (- y1 y2) 2)))) - (min (abs (- x1 x2)) (abs (- y1 y2))))) - -(defun closest-coords (here coord-list &optional (abs-dist NIL)) - "Find the closest position to 'here' from a list of coordinates" - (do* ((clist coord-list (cdr clist)) (c (car clist) (car clist)) - (dist (when c (distance (first here) (second here) - (first c) (second c) abs-dist)) - (when c (distance (first here) (second here) - (first c) (second c) abs-dist))) - (mindist dist) (closest c)) - ((null clist) closest) - (when (< dist mindist) - (setf mindist dist closest c)))) - -(defun opposite-dir (dir) - "Return the direction opposite the input" - (let ((pos (position dir *directions*))) - (when pos (nth (rem (+ 4 pos) 8) *directions*)))) - -(defun next-dir (dir &optional (cw T)) - "Get the neighbouring direction (clockwise or anticlockwise)" - (let ((pos (position dir *directions*)) - (diff (if cw 1 -1))) - (when pos (nth (rem (+ diff pos 8) 8) *directions*)))) - -(defun orth-dir (dir &optional (cw T)) - "Get the direction orthogonal (at right angles) to the given one." - (next-dir (next-dir dir cw) cw)) - -(defun diagonalp (dir) - "Is dir a diagonal direction?" - (member dir '(NE SE SW NW) :test #'eq)) - -(defun dir2patch (herex herey therex therey) - "Calculate the direction to a patch" - (cond ((> herex therex) - (cond ((> herey therey) 'NW) - ((< herey therey) 'SW) - (T 'W))) - ((< herex therex) - (cond ((> herey therey) 'NE) - ((< herey therey) 'SE) - (T 'E))) - (T (cond ((> herey therey) 'N) - ((< herey therey) 'S) - (T NIL))))) - -(defun coordsindir (x y dir) - "Return the coordinates in the given direction" - (cond ((eq dir 'N) (list x (1- y))) - ((eq dir 'NE) (list (1+ x) (1- y))) - ((eq dir 'E) (list (1+ x) y)) - ((eq dir 'SE) (list (1+ x) (1+ y))) - ((eq dir 'S) (list x (1+ y))) - ((eq dir 'SW) (list (1- x) (1+ y))) - ((eq dir 'W) (list (1- x) y)) - ((eq dir 'NW) (list (1- x) (1- y))) - ((null dir) (list x y)) - (T (error "~&Invalid direction ~S")))) - -(defun patchindir (x y dir) - "Return the patch in the given direction" - (let* ((coords (coordsindir x y dir)) - (nextx (first coords)) (nexty (second coords))) - (coord nextx nexty))) - -(defun neighbour (p dir) - "Return the neighbouring patch in this direction" - (patchindir (first (patch-pos p)) (second (patch-pos p)) dir)) - - -;; WORLD CREATION FUNCTIONS - -(defun get-patch-feature (patch) - "Find a random feature (or none) to occupy this patch." - (let ((flist (biome-features (patch-biome patch)))) - (dolist (f flist NIL) - (when (chancep (second f)) - (return-from get-patch-feature - (make-instance (first f) :x (first (patch-pos patch)) - :y (second (patch-pos patch)))))))) - -(defun generate-biomes (size-factor) - ;;XXX The maps this produces don't look quite as expected, but for - ;; current purposes they are good enough - (debugging "~&Generating biomes") ;debug - (let* ((wsize (world-size)) (seeds NIL) - (nseeds (round (/ wsize size-factor))) - (biomes (remove-first-if - #'(lambda (e) (eq e 'stream)) - (available-biomes)))) - ;;Initialize a set of biome 'seed' coordinates - (dotimes (n nseeds) - (setf seeds - (cons (list (random wsize) - (random wsize) - (random-elt biomes)) - seeds))) - (debugging "~&~S" seeds) - ;;For each patch, calculate the closest seed and set to that biome - (dotimes (x wsize seeds) - (dotimes (y wsize) - (let ((p (coord x y)) - (b (third (closest-coords (list x y) seeds T)))) - (setf (patch-biome p) (get-biome b)) - (setf (patch-occupant p) (get-patch-feature p))))))) - -(defun generate-stream (x0 y0) - (debugging "~&Generating a stream, starting at ~S/~S" x0 y0) ;debug - (do* ((dir (random-elt *directions*) - (if (probabilityp 75) dir (next-dir dir (random-elt '(T NIL))))) - (patch (coord x0 y0) (neighbour patch dir))) - ((or (null patch) (eq (patch-biome patch) (get-biome 'stream)))) - (setf (patch-biome patch) (get-biome 'stream)) - (setf (patch-occupant patch) NIL))) - -(defun create-world (size) - "Create a world of the specified size (square)" - (set-world (init-matrix size)) - ;;XXX magic numbers - (generate-biomes 10) - (dotimes (s (round (/ (expt size 2) 2000))) - (generate-stream (random size) (random size)))) - -(defun init-world () - "Initialize the log, RNG, and world." - (write-to-file "NALEDI ya AFRICA" *logfile*) - (logging (time-stamp)) - (setf *random-state* (make-random-state t)) - (create-world *world-size*))