Compare commits

...
Sign in to create a new pull request.

45 commits

Author SHA1 Message Date
Charles Dang
713cbca0ad Help Browser: don't generate nodes for hidden topics, updated link color
See 1903b05 for GUI2 label link color change. Really, this shouldn't be hardcoded here,
but I'll get to that.
2018-10-23 21:55:57 -04:00
Charles Dang
29ffc91bdc Help: fixed unit section such as the Walking Corpse's not generating
I didn't realize that is_valid_id was only meant to check IDs provided in the help config and
not IDs generated dynamically; the latter are always correct. Granted, the old code did actually
call its analogous codepath for race and era generation, but it did not for unit sections. My
refactor made it so this check happened *any* time a section was created, regardless of its source.

To rectify that, I moved ID validation for sections into the static section generation loop like
for topics. I also moved the topic id validation prior to the ToD generation, tweaked the invalid
id message, and renamed a variable for clarity and consistency.
2018-10-23 21:55:55 -04:00
Charles Dang
c68968a900 Fixup 7f60fb0 (missing include) 2018-10-23 21:55:53 -04:00
Charles Dang
ffbc768de5 Help/Manager: dead code begone! 2018-10-23 21:55:52 -04:00
Charles Dang
0a394732a0 Help: moved a util function to the only file in which it is used 2018-10-23 21:55:50 -04:00
Charles Dang
a638e8cc33 Fixed crash when starting the editor (fixes #2816)
If no display_context was passed to the display ctor (such as is the case in the editor),
the terrain_builder's gamemap pointer would be null. Really not sure why this didn't cause
a crash n the 1.14 branch, but oh well.

I just removed the ctor call to rebuild_all() since it's not really needed. The map is
rebuilt when change_display_context is called anyway, which both the editor_controller and
play_controller do.
2018-10-23 21:55:47 -04:00
Charles Dang
50e27afc47 Help: mark currently unused-parameter as such for now 2018-10-23 21:55:23 -04:00
Charles Dang
86debf7c21 Fixup 3865a16 2018-10-23 21:55:22 -04:00
Charles Dang
5d69e6c03c Help: fixed ambiguous overload, config copying 2018-10-23 21:55:21 -04:00
Charles Dang
116edad6f4 Help: removed useless 'is nonnull' debug info from terrain topics
is_nonnull() just means "is not "none" or void terrain".
2018-10-23 21:55:19 -04:00
Charles Dang
92f786f7e0 Fix extra qualifier on function declaration 2018-10-23 21:55:18 -04:00
Charles Dang
0e21a6425b Help/Manager: don't need to keep a game config pointer anymore
I had removed the code that used it. We can just keep a help config pointer.
2018-10-23 21:55:16 -04:00
Charles Dang
b81b5ba66e How did these sneak past... (fixup 23e78ab) 2018-10-23 21:55:13 -04:00
Charles Dang
81388ad250 Help: massive refactor and cleanup of the help backend
This basically splits all the stuff in help/help_impl.*pp into multiple files by their
function, and refactors the entire workflow into a proper object-oriented interface in
modern C++.

There are still a features missing (such as hidden section parsing in the manager) that
I'll get back to.

Few incidental changes and fixes:
* Terrain topics now sorted alphabetically.
* Help text now small
* Fixed wrong toggle button id in browser. This is what was making it impossible to expand
  any sections.

The GUI2 help browser is now back in working order, inasmuch as you can view all sections'
and topics' text (save units').
2018-10-23 21:54:48 -04:00
Charles Dang
0dc8869110 Units: convert header and italic help markup to Pango markup
[ci skip]
2018-10-23 21:53:12 -04:00
Charles Dang
b2f6b174d4 MP Factions: converted bold help markup to Pango markup
[ci skip]
2018-10-23 21:53:10 -04:00
Charles Dang
23026434ff I missed a case of <header>
[ci skip]
2018-10-23 21:53:08 -04:00
Charles Dang
be8340db2e Help: explicitly sort Terrain sections
[ci skip]

This happened incidentally with the current help implementation, but that no longer happens
with my in-progress refactor. We still want these sections sorted, though.

The removal of the "sort_sections=generated" key from the Eras section is a forward-port of
6b20ccdb62 from 1.14.
2018-10-23 21:53:06 -04:00
Charles Dang
669c80c255 Terrains: convert italic help formatting to Pango markup
[ci skip]
2018-10-23 21:53:04 -04:00
Charles Dang
39d823ccde Use proper bool instead of yes/no when setting config values 2018-10-23 21:52:43 -04:00
Charles Dang
d7dc0cbdf6 Removed help_manager struct (it will be replaced)
This is in preparation for a larger refactor.
2018-10-23 21:52:40 -04:00
Charles Dang
36571cad61 Help Browser: removed custom formatting conversion for <bold>, <italic>, and <header> 2018-10-23 21:52:00 -04:00
Charles Dang
06fb491e1e Help: replaced custom <header> formatting tag with <big>
[ci skip]
2018-10-23 21:51:58 -04:00
Charles Dang
4ba895f0fc Help: replaced custom <bold> and <italic> formatting tags with Pango equivalents
[ci skip]
2018-10-23 21:51:55 -04:00
Charles Dang
e82471a972 Help: formatting/doc/include cleanup 2018-10-23 21:50:50 -04:00
Charles Dang
88d8b9c933 Removed a whole bunch of GUI1 stuff
This almost completely removes GUI1, save for the button widget (will remove once I get
all the Theme handling sorted out) and the editor's own GUI1 widgets.

This includes the GUI1 textbox, scrollbar, scrollarea, and menu widgets, as well as the
dialog_frame and dialog_manager classes. I've also removed floating_textbox. It was only
kept around for reference (it's unused as of the inclusion of the GUI2 command console).

gui::in_dialog has been replaced by GUI2's is_in_dialog directly now that we have no more
GUI1 dialogs.

\o/
2018-10-23 21:50:02 -04:00
Charles Dang
e8d23ccf33 Removed GUI1 Help interface code
Not needed anymore since we're moving to GUI2 for this dialog.
2018-10-23 21:45:06 -04:00
Charles Dang
a4beeee390 Help: removed code to show old GUI1 help browser
Attempting to launch it crashes the game (probably to do with a null video surface),
and since it's being replaced there's no point in restoring it to functionality.

The implementation details (help_menu, etc) have been left for now. Will remove soon.

This also means the GUI2 help browser is shown by default now.
2018-10-23 21:44:30 -04:00
Charles Dang
bfe5f4a2ec Some build fixups to reflect changes since the help commits were originally made 2018-10-23 21:42:26 -04:00
Charles Dang
5b99f3ae83 Help Browser: removed help topic padding 2018-10-23 21:42:23 -04:00
Charles Dang
1fc9500af1 Help Browser: fixup topic title layout 2018-10-23 21:42:22 -04:00
Celtic Minstrel
c2e100a937 Help Browser: Support arbitrary initial topic
Hidden topics still untested.
2018-10-23 21:42:20 -04:00
Celtic Minstrel
25ab7f04f7 Help Browser: Don't double-list a section's root topic 2018-10-23 21:42:17 -04:00
Celtic Minstrel
9fa470ddfc Help Browser: Show topic title 2018-10-23 21:42:16 -04:00
Celtic Minstrel
67e4235918 Help Browser: Fix back button being visible when dialog opens 2018-10-23 21:42:14 -04:00
Charles Dang
8dbd895b21 Help Browser: properly implement toggling by clicking the book icons 2018-10-23 21:42:12 -04:00
Celtic Minstrel
8ac0ba61b4 Fix GUI1 help topics not showing 2018-10-23 21:42:10 -04:00
Celtic Minstrel
f421a74864 Help Browser: Implement history 2018-10-23 21:42:08 -04:00
Charles Dang
ec5cdb4fe4 Moved new help entry point to a common location 2018-10-23 21:41:44 -04:00
Charles Dang
0630ca6d9a Help Browser: implemented viewing of sub-sections/topics 2018-10-23 21:40:44 -04:00
Celtic Minstrel
3ddd5670d9 Help Viewer: Parse help markup to Pango markup... mostly.
The help parser now outputs a config rather than a vector of strings of
which some should be taken literally and others parsed as WML.
2018-10-23 21:39:57 -04:00
Charles Dang
02b472b05c Help Browser: improved layout stability and added next/back buttons (non-functional, of course) 2018-10-23 21:39:54 -04:00
Celtic Minstrel
820687baf3 Help: That parameter isn't really needed 2018-10-23 21:39:52 -04:00
Celtic Minstrel
b12ffe752b Help Viewer: Show topics with generated text 2018-10-23 21:39:50 -04:00
Celtic Minstrel
40214da0a1 Help Browser: Actually show the correct topics with the correct icons
This also fixes the lag when opening help and makes the topic text appear.
(The topic text is not parsed at the moment, though.)
2018-10-23 21:39:44 -04:00
82 changed files with 5176 additions and 12435 deletions

View file

@ -141,9 +141,9 @@ Have a look at the addon server for easy to use additional music tracks."
# generator="contents:editor"
text= "<img>src=icons/icon-editor.png align=left box=no float=yes</img>" + _ "Wesnoth's Map and Scenario Editor allows users to create and edit the maps on which every Wesnoth scenario takes place. It also provides a limited set of features for setting up a basic scenario.
The editor can be launched from the <italic>text='Map Editor'</italic>" + _ " option at the title screen.
The editor can be launched from the <i>Map Editor</i>" + _ " option at the title screen.
<header>text='What you get'</header>" + _ "
<big>What you get</big>" + _ "
• <ref>dst='editor_terrain' text='Terrain Editor'</ref>
An easy to use map editor, similar to simple paint applications.
@ -155,7 +155,7 @@ Predefine the scenario's music track playlist.
• Time Schedule Editor
" + "<header>text='What you do *not* get'</header>" + _ "
" + "<big>What you do *not* get</big>" + _ "
• What-you-see-is-what-you-get
The editor is not a WYSIWYG application.
@ -169,7 +169,7 @@ The editor is not a tool to help you scripting the scenario's event handlers.
The editor can't load maps from versions prior to 1.10.
TODO is that true?
" + "<header>text='Basic Concepts'</header>" + _ "
" + "<big>Basic Concepts</big>" + _ "
• <ref>dst='editor_modes' text='Editing Modes'</ref>
• <ref>dst='editor_toolkit' text='Editor Toolkit'</ref>
• <ref>dst='editor_palette' text='Editor Palette'</ref>
@ -184,20 +184,20 @@ TODO is that true?
title= _ "Editing Modes"
text= _ "The editor features two separate modes of operation:" + _ "
<header>text='Pure Map Mode'</header>" + _ "
<big>Pure Map Mode</big>" + _ "
Allows only the composing of the terrain map itself and the definition of leader starting positions." + _ "
How the information is saved depends on the loaded file:
<bold>text='Native'</bold>" + _ "
<b>Native</b>" + _ "
A new map or file containing only the arguments to the map_data attribute.
The produced map can be played in the “User Maps” game type at the create multiplayer game dialog if saved to the default directory.
<bold>text='Embedded'</bold>" + _ "
<b>Embedded</b>" + _ "
Scenario files containing a valid map_data attribute (not a file include) will be opened in this submode. The editor replaces only the content of map_data and leaves everything else in the scenario untouched. Maps opened this way are marked [e] in the Maps menu." + _ "
<header>text='Scenario Mode'</header>" + _ "
<big>Scenario Mode</big>" + _ "
The Scenario mode allows several extra tools to be used, such as the Unit tool. At least one side must be defined in order to use these tools, however.

View file

@ -61,7 +61,6 @@
id=eras_section
title= _ "Eras"
sections_generator=eras
sort_sections=generated
sort_topics=yes
topics=..eras_section
[/section]
@ -72,6 +71,7 @@
sections_generator=terrains
topics=..terrains_section
sort_topics=generated
sort_sections=yes
[/section]
[section]
@ -96,7 +96,7 @@
title= _ "Introduction"
text="<img>src=misc/logo-bg.png~BLIT(misc/logo.png) align=middle box=no</img>" + _ "
<italic>text='Battle for Wesnoth'</italic> is a turn-based fantasy strategy game somewhat unusual among modern strategy games. While other games strive for complexity, <italic>text='Battle for Wesnoth'</italic> strives for simplicity of both rules and gameplay. This does not make the game simple, however — from these simple rules arise a wealth of strategy, making the game easy to learn but a challenge to master.
<i>Battle for Wesnoth</i> is a turn-based fantasy strategy game somewhat unusual among modern strategy games. While other games strive for complexity, <i>Battle for Wesnoth</i> strives for simplicity of both rules and gameplay. This does not make the game simple, however — from these simple rules arise a wealth of strategy, making the game easy to learn but a challenge to master.
The following pages outline all you need to know to play Wesnoth. As you play, new information is added to the various categories as you come across new aspects of the game. For more detailed information on special situations and exceptions, follow the included links."
[/topic]
@ -107,11 +107,11 @@ The following pages outline all you need to know to play Wesnoth. As you play, n
title= _ "About the Game"
text= _ "The game takes place on a hex-based game field, where your units battle against those controlled by the computer, friends who each take turns on the same computer (hotseat play), other players on the same network, or players worldwide in multiplayer mode.
Each of these battles is called a <italic>text='scenario'</italic>, which can be strung together to make <italic>text='campaigns'</italic>. Besides the campaigns that ship with the game, Wesnoth supports user-made content, and the add-on server boasts hundreds of custom maps, campaigns, eras, factions, and resources.
Each of these battles is called a <i>scenario</i>, which can be strung together to make <i>campaigns</i>. Besides the campaigns that ship with the game, Wesnoth supports user-made content, and the add-on server boasts hundreds of custom maps, campaigns, eras, factions, and resources.
The game also features a human-readable markup called Wesnoth Markup Language (WML) to easily allow users to create their own content, as well as a fully-featured Map and Scenario Editor for designing your own battlefields.
The <italic>text='Battle for Wesnoth'</italic> project was begun in 2003, and has been worked on by a multitude of volunteers ever since."
The <i>Battle for Wesnoth</i> project was begun in 2003, and has been worked on by a multitude of volunteers ever since."
[/topic]
[topic]
@ -166,16 +166,16 @@ This unit is unknown for the moment. You must discover it in the game to be allo
[topic]
id=..gameplay
title= _ "Gameplay"
text= _ "Wesnoth is comprised of a series of battles, called <italic>text='scenarios'</italic>, that pit your troops against the troops of one or more adversaries. Multiple scenarios that follow on from each other, telling a story, make up <italic>text='campaigns'</italic>. In a campaign, you often need to play more carefully, preserving your best troops for use again in later scenarios.
text= _ "Wesnoth is comprised of a series of battles, called <i>scenarios</i>, that pit your troops against the troops of one or more adversaries. Multiple scenarios that follow on from each other, telling a story, make up <i>campaigns</i>. In a campaign, you often need to play more carefully, preserving your best troops for use again in later scenarios.
The interactive <bold>text='Tutorial'</bold> introduces the basics of Wesnoth gameplay in the context of a scenario. Most material covered in the tutorial is explained more in-depth in these pages, so you can always refer back here if you forget something.
The interactive <b>Tutorial</b> introduces the basics of Wesnoth gameplay in the context of a scenario. Most material covered in the tutorial is explained more in-depth in these pages, so you can always refer back here if you forget something.
After you master the basics, try out a beginner campaign, such as <italic>text='Heir to the Throne'</italic> or <italic>text='The South Guard'</italic>. A full list of installed campaigns can be found via the <bold>text='Campaign'</bold> option on the main menu. As Wesnoth can be quite challenging, you may wish to start on easy before progressing to higher difficulties." + "
After you master the basics, try out a beginner campaign, such as <i>Heir to the Throne</i> or <i>The South Guard</i>. A full list of installed campaigns can be found via the <b>Campaign</b> option on the main menu. As Wesnoth can be quite challenging, you may wish to start on easy before progressing to higher difficulties." + "
<img>src=help/tooltip.png align=right float=yes</img>" +
_ "While playing, keep in mind that you can mouse-over many items in the game, such as the information displayed in the status pane, to see a brief description explaining each item. This is especially useful when you encounter new elements, such as <ref>dst='..abilities_section' text='abilities'</ref>, for the first time." + _ "
<header>text='Fundamentals of Gameplay'</header>
<big>Fundamentals of Gameplay</big>
"
generator="contents:gameplay"
@ -186,9 +186,9 @@ After you master the basics, try out a beginner campaign, such as <italic>text='
[topic]
id=victory_and_defeat
title= _ "Victory and Defeat"
text= _ "Pay careful attention to the <bold>text='Objectives'</bold> pop-up box at the beginning of each scenario. In most scenarios, you will achieve victory by killing all enemy leaders ; likewise, the death of your own leader generally results in defeat. However, some scenarios may have other victory objectives, such as getting your leader to a designated point, rescuing an ally, solving a puzzle, or holding out against a siege until a certain number of turns have elapsed." + _ "
text= _ "Pay careful attention to the <b>Objectives</b> pop-up box at the beginning of each scenario. In most scenarios, you will achieve victory by killing all enemy leaders ; likewise, the death of your own leader generally results in defeat. However, some scenarios may have other victory objectives, such as getting your leader to a designated point, rescuing an ally, solving a puzzle, or holding out against a siege until a certain number of turns have elapsed." + _ "
When you win a scenario, the map grays over and the <bold>text='End Turn'</bold> button changes to <bold>text='End Scenario'</bold>. You can now do things like changing your save options or (if you are in a multiplayer game) chatting with other players before pressing that button to advance."
When you win a scenario, the map grays over and the <b>End Turn</b> button changes to <b>End Scenario</b>. You can now do things like changing your save options or (if you are in a multiplayer game) chatting with other players before pressing that button to advance."
[/topic]
# wmllint: markcheck on
@ -196,7 +196,7 @@ When you win a scenario, the map grays over and the <bold>text='End Turn'</bold>
[topic]
id=recruit_and_recall
title= _ "Recruiting and Recalling"
text="<img>src=help/recruit.png align=left float=yes</img>" + _ "Each side begins with one leader in their keep. At the start of any battle, and at times during it, you will need to recruit <ref>dst='..units' text='units'</ref> into your army. To recruit, you must have your leader (for instance, Konrad in the <italic>text='Heir to the Throne'</italic> campaign) on the keep hex of a <ref>dst='..terrain_castle' text='castle'</ref>. Then you may recruit by either choosing <bold>text='Recruit'</bold> from the menu or right-clicking on a hex and selecting <bold>text='Recruit'</bold>. This brings up the recruit menu, which lists units available for recruitment, along with their gold cost. Click on a unit to see its statistics, then press the <bold>text='Recruit'</bold> button to recruit it." + _ "
text="<img>src=help/recruit.png align=left float=yes</img>" + _ "Each side begins with one leader in their keep. At the start of any battle, and at times during it, you will need to recruit <ref>dst='..units' text='units'</ref> into your army. To recruit, you must have your leader (for instance, Konrad in the <i>Heir to the Throne</i> campaign) on the keep hex of a <ref>dst='terrain_castle' text='castle'</ref>. Then you may recruit by either choosing <b>Recruit</b> from the menu or right-clicking on a hex and selecting <b>Recruit</b>. This brings up the recruit menu, which lists units available for recruitment, along with their gold cost. Click on a unit to see its statistics, then press the OK button to recruit it." + _ "
If you right-clicked on a castle hex and selected recruit, the new unit will appear in that hex. Otherwise, it will appear in a free hex near the keep. You may only recruit as many units as you have free hexes in your castle, and you cannot spend more gold than you actually have on recruiting." + _ "
@ -212,7 +212,7 @@ Units not only cost gold to Recruit or Recall, they also require money to suppor
[topic]
id=income_and_upkeep
title= _ "Income and Upkeep"
text= _ "In Wesnoth, it is not enough simply to recruit units and fight. You must watch your gold as well, especially in campaigns, where you can carry extra gold over from one scenario to the next. There are two aspects to this; <italic>text='income'</italic> and <italic>text='upkeep'</italic>." + _ "
text= _ "In Wesnoth, it is not enough simply to recruit units and fight. You must watch your gold as well, especially in campaigns, where you can carry extra gold over from one scenario to the next. There are two aspects to this; <i>income</i> and <i>upkeep</i>." + _ "
Income is simple. You have a base income of 2 gold per turn. For every village you control, you gain one additional gold each turn. (In general this is configurable but in campaigns it is almost always one gold per village.) Thus, if you have ten villages, you would normally gain 12 gold each turn. Your upkeep costs are subtracted from this income, as detailed below." + _ "
@ -220,7 +220,7 @@ Upkeep is also fairly simple. Each unit requires an amount of Upkeep equal to it
Upkeep costs are subtracted from your income, so in the case of twelve levels of units and ten villages, your resultant Income would be 10 gold per turn." + _ "
There are two important exceptions to upkeep: units with the loyal trait and leaders never incur upkeep. Units you begin the scenario with (such as Delfador), or units who join you during a scenario (such as the horseman in the second scenario of <italic>text='Heir to the Throne'</italic>) will usually have the <italic>text='loyal'</italic> trait. The unit you are playing (such as Konrad) will almost always be a leader."
There are two important exceptions to upkeep: units with the loyal trait and leaders never incur upkeep. Units you begin the scenario with (such as Delfador), or units who join you during a scenario (such as the horseman in the second scenario of <i>Heir to the Throne</i>) will usually have the <i>loyal</i> trait. The unit you are playing (such as Konrad) will almost always be a leader."
[/topic]
# wmllint: markcheck on
@ -228,7 +228,7 @@ There are two important exceptions to upkeep: units with the loyal trait and lea
[topic]
id=hitpoints
title= _ "Hitpoints and Experience"
text= "<img>src=help/hpxp.png align=right float=yes</img>" + _ "Each unit has a certain number of <italic>text='hitpoints'</italic> (HP). If the hitpoints of a unit drop below 1, the unit dies. Each unit also has a certain number of <italic>text='experience points'</italic> (XP). A freshly recruited unit starts with no experience points, and gains experience by fighting enemies." +
text= "<img>src=help/hpxp.png align=right float=yes</img>" + _ "Each unit has a certain number of <i>hitpoints</i> (HP). If the hitpoints of a unit drop below 1, the unit dies. Each unit also has a certain number of <i>experience points</i> (XP). A freshly recruited unit starts with no experience points, and gains experience by fighting enemies." +
"
" +
@ -246,7 +246,7 @@ There are two important exceptions to upkeep: units with the loyal trait and lea
Units have a certain amount of experience required to advance (this is 20% less for units with the Intelligent trait). Once they achieve this amount, they immediately advance to the next level, healing fully in the process. In some cases, you will be given a choice of advancement options." + _ "
While most units have three levels, not all do. Occasional units (such as <ref>dst='unit_Mage' text='magi'</ref>) may have four. Once a unit has reached its maximum level, it may have an <italic>text='After Maximum Level Advancement'</italic> (AMLA) available to it. The AMLA will modify the unit each time the unit reaches the experience goal, but the unit will remain the same level. The typical AMLA effect is for the unit to raise the maximum HP by 3 and full-heal it. The first AMLA will normally be reached with 150 XP gained (120 XP for intelligent units). However, gaining an AMLA becomes progressively harder for each AMLA the unit receives, and so it is usually more useful to try to advance your lower level units."
While most units have three levels, not all do. Occasional units (such as <ref>dst='unit_Mage' text='magi'</ref>) may have four. Once a unit has reached its maximum level, it may have an <i>After Maximum Level Advancement</i> (AMLA) available to it. The AMLA will modify the unit each time the unit reaches the experience goal, but the unit will remain the same level. The typical AMLA effect is for the unit to raise the maximum HP by 3 and full-heal it. The first AMLA will normally be reached with 150 XP gained (120 XP for intelligent units). However, gaining an AMLA becomes progressively harder for each AMLA the unit receives, and so it is usually more useful to try to advance your lower level units."
[/topic]
# wmllint: markcheck on
@ -254,11 +254,11 @@ While most units have three levels, not all do. Occasional units (such as <ref>d
[topic]
id=movement
title= _ "Movement"
text="<img>src=help/moving1.png align=left float=yes</img>" + "<img>src=help/moving2.png align=left float=yes</img>" + _ "Movement in <italic>text='Battle for Wesnoth'</italic> is simple. Click on the unit you wish to move to select it, then click on the hex you wish to move it to. When a unit is selected, everywhere it can move this turn will be highlighted, and all other hexes on the map are made dull. Mousing over a highlighted hex shows the defense rating the unit would have if you moved it to that hex. Mousing over a dull hex will also show the number of turns required to reach it, and clicking will cause the unit to move towards it by the fastest route over this and subsequent turns. If you dont use up all of a units movement when you first move a unit, you may move it again. This is useful when having two units switch places. Attacking with a unit will use up its movement. Ending a move in a village you dont already own will also use up a units movement, but will still allow it to attack." + _ "
text="<img>src=help/moving1.png align=left float=yes</img>" + "<img>src=help/moving2.png align=left float=yes</img>" + _ "Movement in <i>Battle for Wesnoth</i> is simple. Click on the unit you wish to move to select it, then click on the hex you wish to move it to. When a unit is selected, everywhere it can move this turn will be highlighted, and all other hexes on the map are made dull. Mousing over a highlighted hex shows the defense rating the unit would have if you moved it to that hex. Mousing over a dull hex will also show the number of turns required to reach it, and clicking will cause the unit to move towards it by the fastest route over this and subsequent turns. If you dont use up all of a units movement when you first move a unit, you may move it again. This is useful when having two units switch places. Attacking with a unit will use up its movement. Ending a move in a village you dont already own will also use up a units movement, but will still allow it to attack." + _ "
Each unit has a certain number of movement points which are used up when moving into a new hex, depending on the Terrain of that particular hex. For instance, grassland nearly always costs 1 movement point to enter. Exactly how many movement points are spent entering a hex depends on the unit type — in forest, elvish units only spend 1 movement point, most human and orc units spend 2, while horsemen spend 3. You can learn how many movement points a unit requires to enter a certain terrain type by right-clicking on it, selecting <bold>text='Unit Description'</bold>, and then looking at <bold>text='Terrain Modifiers'</bold>." + _ "
Each unit has a certain number of movement points which are used up when moving into a new hex, depending on the Terrain of that particular hex. For instance, grassland nearly always costs 1 movement point to enter. Exactly how many movement points are spent entering a hex depends on the unit type — in forest, elvish units only spend 1 movement point, most human and orc units spend 2, while horsemen spend 3. You can learn how many movement points a unit requires to enter a certain terrain type by right-clicking on it, selecting <b>Unit Description</b>, and then looking at <b>Terrain Modifiers</b>." + _ "
Another thing to keep in mind while moving is <italic>text='zones of control'</italic>. Each unit — except for level 0 units — generates a zone of control in the hexes immediately surrounding it, and any enemy unit entering those hexes immediately ends its movement. Learning how to use zones of control to your advantage is an important part of Wesnoth, as only <ref>dst='ability_skirmisherskirmisher' text='skirmishers'</ref> can ignore zones of control." + _ "
Another thing to keep in mind while moving is <i>zones of control</i>. Each unit — except for level 0 units — generates a zone of control in the hexes immediately surrounding it, and any enemy unit entering those hexes immediately ends its movement. Learning how to use zones of control to your advantage is an important part of Wesnoth, as only <ref>dst='ability_skirmisher' text='skirmishers'</ref> can ignore zones of control." + _ "
To see where the enemy can move to during their next turn, press Ctrl-v or Cmd-v. Ctrl-b or Cmd-b shows where the enemy could move, if your units were not on the map to block their progress."
[/topic]
@ -268,9 +268,9 @@ To see where the enemy can move to during their next turn, press Ctrl-v or Cmd-v
[topic]
id=shroud_and_fog
title= _ "Shroud and Fog of War"
text= _ "In some scenarios, parts of the map will be hidden from you. There are two mechanisms that can be used separately or together. The <italic>text='shroud'</italic> hides both the terrain and any units at a location. However, once it is cleared, you can always see that location. The <italic>text='fog of war'</italic> only hides units and ownership of villages (other than by you or your allies). The fog of war is cleared temporarily when you have units nearby, but returns when they leave. Both the shroud and the fog of war are cleared by units. Each unit clears locations adjacent to those within one turns move (ignoring zones of control and enemy units).
text= _ "In some scenarios, parts of the map will be hidden from you. There are two mechanisms that can be used separately or together. The <i>shroud</i> hides both the terrain and any units at a location. However, once it is cleared, you can always see that location. The <i>fog of war</i> only hides units and ownership of villages (other than by you or your allies). The fog of war is cleared temporarily when you have units nearby, but returns when they leave. Both the shroud and the fog of war are cleared by units. Each unit clears locations adjacent to those within one turns move (ignoring zones of control and enemy units).
Normally you can undo a units movement, as long as an event with a randomized result has not occurred, such as combat or recruitment (as most units receive random traits when recruited). Exploring hidden terrain by clearing shroud or fog will also prevent undos to a previous state. You may wish to activate <bold>text='Delay Shroud Updates'</bold> in the actions menu. This will prevent units from clearing shroud or fog until the next randomized event or a manual update via <bold>text='Update Shroud Now'</bold> (or the end of your turn) and thereby preserve your ability to undo movement."
Normally you can undo a units movement, as long as an event with a randomized result has not occurred, such as combat or recruitment (as most units receive random traits when recruited). Exploring hidden terrain by clearing shroud or fog will also prevent undos to a previous state. You may wish to activate <b>Delay Shroud Updates</b> in the actions menu. This will prevent units from clearing shroud or fog until the next randomized event or a manual update via <b>Update Shroud Now</b> (or the end of your turn) and thereby preserve your ability to undo movement."
[/topic]
# wmllint: markcheck on
@ -278,21 +278,21 @@ Normally you can undo a units movement, as long as an event with a randomized
[topic]
id=combat
title= _ "Combat"
text= _ "Combat in <italic>text='Battle for Wesnoth'</italic> always takes place between units in adjacent hexes. Click on your unit, and click on the enemy you want to attack: your unit will move towards the enemy unit, and when they are next to each other, combat will begin. The attacker and defender alternate strikes until each has used their allotted number of strikes. The attacker chooses one of its weapons to attack with, and the defender retaliates with one of its attacks of the same type. There are two types of attacks: <italic>text='melee'</italic>, which usually involves weapons such as swords, axes or fangs; and <italic>text='ranged'</italic>, which usually involves weapons such as bows, spears and fireballs." + _ "
text= _ "Combat in <i>Battle for Wesnoth</i> always takes place between units in adjacent hexes. Click on your unit, and click on the enemy you want to attack: your unit will move towards the enemy unit, and when they are next to each other, combat will begin. The attacker and defender alternate strikes until each has used their allotted number of strikes. The attacker chooses one of its weapons to attack with, and the defender retaliates with one of its attacks of the same type. There are two types of attacks: <i>melee</i>, which usually involves weapons such as swords, axes or fangs; and <i>ranged</i>, which usually involves weapons such as bows, spears and fireballs." + _ "
<header>text='Order and Number of Strikes'</header>" + _ "
<big>Order and Number of Strikes</big>" + _ "
The attacker gets the first strike, then the defender retaliates. Each strike either hits, doing a given amount of damage, or misses, doing no damage at all. Strikes alternate until each unit has used up all of its strikes. The number of strikes a unit has varies; for instance, an elvish fighter with a 5×4 attack may strike 4 times, each successful strike dealing 5 damage, while an orcish grunt with a 9×2 attack can only strike twice (but at 9 damage for each hit)." + _ "
<header>text='Chance to Hit'</header>" + _ "
<big>Chance to Hit</big>" + _ "
Every unit has a chance of being hit, based on the <italic>text='terrain'</italic> it is in. This is shown in the status pane, and may also be found by right-clicking a unit, selecting <bold>text='Unit Description'</bold>, and then looking at <bold>text='Terrain Modifiers'</bold>. For instance, many elves have a defense rating of 70% in forest, so a unit attacking them has only a 30% chance of hitting. Conversely, the elfs chance of hitting the attacker in return depends on what terrain the attacker is in." + _ "
Every unit has a chance of being hit, based on the <i>terrain</i> it is in. This is shown in the status pane, and may also be found by right-clicking a unit, selecting <b>Unit Description</b>, and then looking at <b>Terrain Modifiers</b>. For instance, many elves have a defense rating of 70% in forest, so a unit attacking them has only a 30% chance of hitting. Conversely, the elfs chance of hitting the attacker in return depends on what terrain the attacker is in." + _ "
There are two exceptions to this rule: <ref>dst='weaponspecial_magical' text='magical attacks'</ref> and <ref>dst='weaponspecial_marksman' text='marksmen'</ref>. Magical attacks always have a 70% chance to hit, regardless of terrain, and, when used offensively, marksmen always have at least a 60% chance to hit, regardless of terrain." + _ "
<header>text=Damage</header>" + _ "
<big>Damage</big>" + _ "
Each strike which hits causes a base amount of damage depending on the attack type. For instance, an elvish fighter with a 5×4 attack does 5 base damage. This is usually modified by two things: <ref>dst='damage_types_and_resistance' text='resistance'</ref> and <ref>dst='time_of_day' text='time of day'</ref>. To see how base damage is modified by the circumstances, select <bold>text='Damage Calculations'</bold> in the attack selection menu." + _ "
Each strike which hits causes a base amount of damage depending on the attack type. For instance, an elvish fighter with a 5×4 attack does 5 base damage. This is usually modified by two things: <ref>dst='damage_types_and_resistance' text='resistance'</ref> and <ref>dst='time_of_day' text='time of day'</ref>. To see how base damage is modified by the circumstances, select <b>Damage Calculations</b> in the attack selection menu." + _ "
A few units have special <ref>dst='..abilities_section' text='abilities'</ref> which affect damage dealt in combat. The most common of these is <ref>dst='weaponspecial_charge' text='charge'</ref>, which doubles the damage dealt by both attacker and defender when the unit with charge attacks."
[/topic]
@ -301,7 +301,7 @@ A few units have special <ref>dst='..abilities_section' text='abilities'</ref> w
[topic]
id=damage_types_and_resistance
title= _ "Damage Types and Resistance"
text= _ "In Wesnoth, there are three types of damage usually associated with physical attacks: <italic>text='blade, pierce, and impact damage'</italic>. Additionally, there are three further types of damage usually associated with magical attacks: <italic>text='fire, cold, and arcane attacks'</italic>. Different units may have resistances which alter the damage which they take from certain damage types." + _ "
text= _ "In Wesnoth, there are three types of damage usually associated with physical attacks: <i>blade, pierce, and impact damage</i>. Additionally, there are three further types of damage usually associated with magical attacks: <i>fire, cold, and arcane attacks</i>. Different units may have resistances which alter the damage which they take from certain damage types." + _ "
Resistances work very simply: if a unit has 40% resistance against a damage type, then it will suffer 40% less damage when hit with that damage type. It is also possible for a unit to be vulnerable against some damage types. If a unit has 100% resistance against a damage type, it will suffer 100% more damage when hit by that type." + _ "
@ -330,10 +330,10 @@ If a strike is determined to hit, it will always do at least 1 point of damage.
title= _ "Time of Day"
text= _ "The time of day affects the damage of certain units as follows:
• <bold>text='Lawful'</bold> units get +25% damage in daytime, and 25% damage at night.
• <bold>text='Chaotic'</bold> units get +25% damage at night, and 25% in daytime.
• <bold>text='Neutral'</bold> units are unaffected by the time of day.
• <bold>text='Liminal'</bold> units get 25% damage during both night and daytime." + _ "
• <b>Lawful</b> units get +25% damage in daytime, and 25% damage at night.
• <b>Chaotic</b> units get +25% damage at night, and 25% in daytime.
• <b>Neutral</b> units are unaffected by the time of day.
• <b>Liminal</b> units get 25% damage during both night and daytime." + _ "
The current time of day can be observed under the minimap in the status pane. For the usual day/night cycle, morning and afternoon count as day, first and second watch count as night:
@ -358,16 +358,16 @@ The current time of day can be observed under the minimap in the status pane. Fo
title= _ "Healing"
text= _ "In combat, your units will inevitably take damage. There are several ways to heal your units. However, with the exception of resting, these do not stack; only one can occur per turn." + _ "
• <bold>text='Resting'</bold>: A unit which neither moves, attacks, nor is attacked will heal 2 HP in its next turn." + _ "
• <bold>text='Villages'</bold>: A unit which starts a turn in a village or oasis will heal 8HP. If the unit is poisoned, the poison will be cured instead." + _ "
• <ref>dst='ability_regeneratesregenerates' text='Regeneration'</ref>: Certain units (such as trolls) will automatically heal 8HP every turn. If the unit is poisoned, the poison will be cured instead." + _ "
• <bold>text='Healing units'</bold>: Units with the <ref>dst='ability_healingheals +4' text='Heals'</ref> ability will heal each allied adjacent unit, usually <ref>dst='ability_healingheals +4' text='4HP'</ref> or <ref>dst='ability_healingheals +8' text='8HP'</ref> per turn, or prevent Poison from causing that unit damage." + _ "
• <bold>text='Curing units'</bold>: Units with the <ref>dst='ability_curingcures' text='cures'</ref> ability will cure Poison in all allied adjacent units (in preference to healing, if it has that ability as well)." + _ "
• <bold>text='Advancement'</bold>: When a unit <ref>dst='advancement' text='advances'</ref>, it will heal fully. This can happen as soon as your unit gains enough experience, whether it is your turn or not." + _ "
• <b>Resting</b>: A unit which neither moves, attacks, nor is attacked will heal 2 HP in its next turn." + _ "
• <b>Villages</b>: A unit which starts a turn in a village or oasis will heal 8HP. If the unit is poisoned, the poison will be cured instead." + _ "
• <ref>dst='ability_regenerates' text='Regeneration'</ref>: Certain units (such as trolls) will automatically heal 8HP every turn. If the unit is poisoned, the poison will be cured instead." + _ "
• <b>Healing units</b>: Units with the <ref>dst='ability_heals +4' text='Heals'</ref> ability will heal each allied adjacent unit, usually <ref>dst='ability_heals +4' text='4HP'</ref> or <ref>dst='ability_heals +8' text='8HP'</ref> per turn, or prevent Poison from causing that unit damage." + _ "
• <b>Curing units</b>: Units with the <ref>dst='ability_cures' text='cures'</ref> ability will cure Poison in all allied adjacent units (in preference to healing, if it has that ability as well)." + _ "
• <b>Advancement</b>: When a unit <ref>dst='advancement' text='advances'</ref>, it will heal fully. This can happen as soon as your unit gains enough experience, whether it is your turn or not." + _ "
Resting can be combined with other forms of healing, but villages, regeneration, healing and curing cannot combine with each other: the best option will be used. Finally, units heal fully between scenarios." + _ "
<bold>text='Advanced'</bold>" + _ "
<b>Advanced</b>" + _ "
Unlike other forms of healing, the heals ability will not take effect on the healed units turn, but on the healers turn. This means that while a unit surrounded by several healers on the same side will only receive healing from one healer per turn, a unit surrounded by healers from several allied sides will be healed once on each of said allied sides turns."
[/topic]
@ -376,7 +376,7 @@ Unlike other forms of healing, the heals ability will not take effect on the hea
[topic]
id=wrap_up
title= _ "Wrap Up"
text= _ "This concludes the fundamentals of Wesnoth. You might want to read up on basic strategy, or familiarize yourself with <ref>dst='..traits_section' text='traits'</ref> and <ref>dst='..abilities_section' text='abilities'</ref>, but you now know everything you need to know to play the <italic>text='Heir to the Throne'</italic> campaign. Have fun, and good luck!"
text= _ "This concludes the fundamentals of Wesnoth. You might want to read up on basic strategy, or familiarize yourself with <ref>dst='..traits_section' text='traits'</ref> and <ref>dst='..abilities_section' text='abilities'</ref>, but you now know everything you need to know to play the <i>Heir to the Throne</i> campaign. Have fun, and good luck!"
[/topic]
[topic]
@ -390,9 +390,9 @@ Unlike other forms of healing, the heals ability will not take effect on the hea
id=..traits_section
title= _ "Traits"
generator="contents:generated"
text= _ "Traits are modifications that change a units attributes slightly. They are usually randomly assigned to a unit when it is recruited. The traits available to a unit are largely determined by its <italic>text='race'</italic>." + _ "
text= _ "Traits are modifications that change a units attributes slightly. They are usually randomly assigned to a unit when it is recruited. The traits available to a unit are largely determined by its <i>race</i>." + _ "
Most units have two traits. However, goblins have only one trait, undead units are always assigned the single trait <italic>text='undead'</italic> and in some cases <italic>text='fearless'</italic>, and woses do not receive any traits. " + _ "
Most units have two traits. However, goblins have only one trait, undead units are always assigned the single trait <i>undead</i> and in some cases <i>fearless</i>, and woses do not receive any traits. " + _ "
The traits that are available to most nonundead units are <ref>dst='traits_intelligent' text='intelligent'</ref>, <ref>dst='traits_quick' text='quick'</ref>, <ref>dst='traits_resilient' text='resilient'</ref>, and <ref>dst='traits_strong' text='strong'</ref>." + _ "
@ -406,9 +406,9 @@ Elves may also receive <ref>dst='traits_dextrous' text='dextrous'</ref>, and dwa
title= _ "Terrains"
text= _ "Game maps feature a variety of terrains that affect both unit movement and a units defensive capability in combat." + _ "
Terrains come in two types: <italic>text='basic'</italic> and <italic>text='mixed'</italic>." + _ "
Terrains come in two types: <i>basic</i> and <i>mixed</i>." + _ "
<header>text='Basic Terrain Types'</header>" + _ "
<big>Basic Terrain Types</big>" + _ "
Basic terrain types include Flat, Hills, Mountains, Sands, Water and Swamps. There are many more besides these — a list of the terrain types you have discovered can be found at the end of this page.
@ -416,13 +416,13 @@ Basic terrain types include Flat, Hills, Mountains, Sands, Water and Swamps. The
Every unit has a defense rating and a movement cost for each of the basic terrain types, and these values are listed in the units help page. Basic terrain types may have unique properties like illumination effects as well." + _ "
<header>text='Mixed Terrain Types'</header>" + _ "
<big>Mixed Terrain Types</big>" + _ "
Mixed terrain types share the properties of multiple basic terrain types — units generally receive the <italic>text='best defense'</italic> and <italic>text='worst movement'</italic> of the underlying basic types when they move onto a mixed type. For example, this is the case with <italic>text='forested hills'</italic>, <italic>text='sand hills'</italic>, and <italic>text='cave hills'</italic>.
Mixed terrain types share the properties of multiple basic terrain types — units generally receive the <i>best defense</i> and <i>worst movement</i> of the underlying basic types when they move onto a mixed type. For example, this is the case with <i>forested hills</i>, <i>sand hills</i>, and <i>cave hills</i>.
" + "<img>src='terrain/hills/regular.png~BLIT(terrain/forest/pine-tile.png)'</img>" + "<img>src='terrain/hills/desert.png'</img>" + "<img>src='terrain/cave/hills-variation.png'</img>" + _ "
One notable exception is bridge terrains, such as <italic>text='bridges over shallow water'</italic>, <italic>text='fords'</italic>, and <italic>text='bridges over chasms'</italic>. Fords are easily passable to both merfolk and humans — all units moving on a ford enjoy the best defense and best movement out of flat and shallow water, rather than the worse movement of the two. Similarly, bridges over chasms are passable to nonfliers (unsurprisingly).
One notable exception is bridge terrains, such as <i>bridges over shallow water</i>, <i>fords</i>, and <i>bridges over chasms</i>. Fords are easily passable to both merfolk and humans — all units moving on a ford enjoy the best defense and best movement out of flat and shallow water, rather than the worse movement of the two. Similarly, bridges over chasms are passable to nonfliers (unsurprisingly).
" + "<img>src='terrain/water/coast-tile.png~BLIT(terrain/bridge/wood-se-nw.png)'</img>" + "<img>src='terrain/water/ford-tile.png'</img>" + "<img>src='terrain/chasm/regular-tile.png~BLIT(terrain/cave/chasm-stone-bridge-sw-ne-tile.png)'</img>" + _ "
@ -436,17 +436,17 @@ Finally, water villages are generally inhospitable to land units, and do not giv
You can see what basic types a mixed terrain is comprised of by mousing over its hex and checking the terrain type icons displayed in the upper right (under the default theme)." + _ "
You can see what type of behavior the mixed terrain gives by mousing over its hex and viewing the <italic>text='terrain description'</italic> by either pressing the hotkey, or right clicking and selecting from the context menu." + _ "
You can see what type of behavior the mixed terrain gives by mousing over its hex and viewing the <i>terrain description</i> by either pressing the hotkey, or right clicking and selecting from the context menu." + _ "
<header>text='Defense Caps'</header>" + _ "
<big>Defense Caps</big>" + _ "
Some units have <italic>text='defense caps'</italic> for a particular basic terrain type. These units suffer a penalty on mixed terrains with that type — their defense cannot exceed the cap." + _ "
Some units have <i>defense caps</i> for a particular basic terrain type. These units suffer a penalty on mixed terrains with that type — their defense cannot exceed the cap." + _ "
For example, the <ref>text='Loyalist Cavalryman' dst='unit_Cavalryman'</ref> has a defense rating of 30% on forests, and a defense cap for forests. Thus, on forested hills, he has a defense rating of 30% rather than 40%, because the mixed rating cannot exceed the cap." + _ "
If a unit has a defense cap for some terrain, it is always the same as its defense rating on that terrain (it cannot be larger)." + _ "
<header>text='Basic Terrain Types'</header>
<big>Basic Terrain Types</big>
"
generator="contents:generated"
@ -470,33 +470,27 @@ If a unit has a defense cap for some terrain, it is always the same as its defen
title= _ "Using Add-ons"
text= _ "The game supports different types of add-on content, which are not all available in every gameplay mode." + _ "
<header>text='Campaigns and Scenarios'</header>" + _ "
<big>Campaigns and Scenarios</big>" + _ "
Single-player campaigns are collections of scenarios that fit together to tell a story. Both stand-alone scenarios—if intended to be played as such—and regular campaigns are available from the <italic>text='Campaigns'</italic> menu at the title screen." + _ "
Single-player campaigns are collections of scenarios that fit together to tell a story. Both stand-alone scenarios—if intended to be played as such—and regular campaigns are available from the <i>Campaigns</i> menu at the title screen." + _ "
<header>text='Multiplayer Campaigns, Scenarios, and Map Packs'</header>" + _ "
<big>Multiplayer Campaigns, Scenarios, and Map Packs</big>" + _ "
<italic>text='Multiplayer'</italic> games can be played in fully customized, scripted scenarios or even specially designed campaigns. There are also packs providing sets of individual multiplayer scenarios." + _ "
<i>Multiplayer</i> games can be played in fully customized, scripted scenarios or even specially designed campaigns. There are also packs providing sets of individual multiplayer scenarios." + _ "
<header>text='Multiplayer Eras and Factions'</header>" + _ "
<big>Multiplayer Eras and Factions</big>" + _ "
For gameplay purposes, various races of creatures in the world cooperate with each other in factions. Factions are grouped in balanced sets with a common theme; for example, the main factions of Wesnoth can be found in the included Default Era.
In <italic>text='Multiplayer'</italic> mode, you can choose an era when creating a new game, and players can pick from the available factions for that era when setting up their sides and teams." + _ "
In <i>Multiplayer</i> mode, you can choose an era when creating a new game, and players can pick from the available factions for that era when setting up their sides and teams." + _ "
You can see what eras you have loaded in the <ref>text='eras' dst='..eras_section'</ref> of the help." + _ "
<header>text='Modifications'</header>" + _ "
<big>Multiplayer Modifications</big>" + _ "
Modifications are optional scenario- and era-independent scripts that can alter the default ruleset in various ways. You can choose and configure modifications when creating a new game." + _ "
Modifications are optional scenario- and era-independent scripts for <i>Multiplayer</i> games that can alter the default ruleset in various ways. You can choose and configure modifications when creating a new game." + _ "
<big>Cores</big>" + _ "
Cores enable total conversion of The Battle for Wesnoth. A core can replace all the content in Wesnoth: when a different core is loaded, the regular units, terrains and the like do not exist.
Cores allow a significantly different game experience: for example, a World War II campaign, or even a different game altogether, as long as it can be represented as a hexagonal map with units in some way." + _ "
<header>text='Creator Resources'</header>" + _ "
<big>Creator Resources</big>" + _ "
Content authors can use resource packs available on the add-ons server to enrich their own content with existing assets such as images, music, and code. These are not generally intended for direct use in-game; however, other playable add-ons may depend on them and suggest or require their installation during download."
[/topic]
@ -506,13 +500,13 @@ Content authors can use resource packs available on the add-ons server to enrich
[topic]
id=installing_addons
title= _ "Installing Add-ons"
text = _ "User-made add-ons can be obtained and updated through the <italic>text='Add-ons'</italic> option in the main menu. After connecting to the add-ons server (by default <italic>text='add-ons.wesnoth.org'</italic>), you will be presented with a list of add-ons available on the server for downloading.
text = _ "User-made add-ons can be obtained and updated through the <i>Add-ons</i> option in the main menu. After connecting to the add-ons server (by default <i>add-ons.wesnoth.org</i>), you will be presented with a list of add-ons available on the server for downloading.
The installation status for each add-on is shown below each entry. For add-ons that are <italic>text='upgradable'</italic> or <italic>text='outdated'</italic> on the server, their installed and published versions will be shown in the <italic>text='Version'</italic> column.
The installation status for each add-on is shown below each entry. For add-ons that are <i>upgradable</i> or <i>outdated</i> on the server, their installed and published versions will be shown in the <i>Version</i> column.
To search for add-ons by keywords, type any relevant terms in any order in the search box, separated by spaces. You can also sort the add-on list by clicking the column headers. It is also possible to choose to only display add-ons of specific categories by clicking on the <bold>text='Type'</bold> dropdown.
To search for add-ons by keywords, type any relevant terms in any order in the <b>Filter</b> box, separated by spaces. You can also sort the add-on list by clicking the column headers. It is also possible to choose to only display add-ons of specific categories by clicking on the <b>Options</b> button in the top-right corner.
To install an add-on, select it from the list and click the <b>+</b> icon, or simply double-click on the add-ons title. If the window is too small to show them inline, the <bold>text='Addon Details'</bold> button provides you with additional details about the add-on, such as its full description, installation status, and available translations."
To install an add-on, select it from the list and click <b>OK</b>, or simply double-click on the add-ons title. The <b>Description</b> button provides you with additional details about the add-on, such as its full description, installation status, and available languages."
[/topic]
# wmllint: markcheck on
@ -520,9 +514,9 @@ To install an add-on, select it from the list and click the <b>+</b> icon, or si
[topic]
id=removing_addons
title= _ "Removing Add-ons"
text = _ "To remove add-ons, choose <bold>text='Remove Add-ons'</bold> in the add-ons server connection dialog. You will be presented with options to remove any number of add-ons you currently have installed.
text = _ "To remove add-ons, choose <b>Remove Add-ons</b> in the add-ons server connection dialog. You will be presented with options to remove any number of add-ons you currently have installed.
It is not possible to remove add-ons for which there is publishing information (<italic>text='.pbl'</italic> files) attached, in order to prevent its accidental loss. If necessary, you must manually delete the information files or the add-ons themselves using a file manager provided by your platform."
It is not possible to remove add-ons for which there is publishing information (<i>.pbl</i> files) attached, in order to prevent its accidental loss. If necessary, you must manually delete the information files or the add-ons themselves using a file manager provided by your platform."
[/topic]
# wmllint: markcheck on
@ -539,38 +533,38 @@ It is not possible to remove add-ons for which there is publishing information (
# wmllint: display on
text= _ "These commands can either be issued via the command line by prefixing them with ':' (as shown here) or via the chat by prefixing them with '/' (press 'm' first to open the chat line).
" + "<header>text=':clear'</header>" + _ "
" + "<big>:clear</big>" + _ "
Clear chat messages.
" + "<header>text=':debug'</header>" + _ "
" + "<big>:debug</big>" + _ "
Switch debug mode on (does not work in multiplayer). See <ref>dst='debug_commands' text='debug mode commands'</ref>.
Debug mode is turned off by quitting the game or :nodebug.
" + "<header>text=':droid [<side>] [on|off]'</header>" + _ "
" + "<big>:droid [<side>] [on|off]</big>" + _ "
Set or toggle player on side between human and AI player. The player/client who controls that side needs to issue this command. If no second parameter is supplied, toggle between human and AI. If it is on, set an AI controller. If it is off set a human controller. Defaults to the currently active side if no parameter is supplied.
" + "<header>text=':controller <side>'</header>" + _ "
" + "<big>:controller <side></big>" + _ "
Display the controller status of a side.
" + "<header>text=':fps'</header>" + _ "
" + "<big>:fps</big>" + _ "
Toggle the display of the current frames per second.
" + "<header>text=':log <level> <domain>'</header>" + _ "
" + "<big>:log <level> <domain></big>" + _ "
Switch a log domain to a different log level.
" + "<header>text=':refresh'</header>" + _ "
" + "<big>:refresh</big>" + _ "
Redraws the screen and reloads any image files that have been changed.
" + "<header>text=':theme'</header>" + _ "
" + "<big>:theme</big>" + _ "
Bring up theme selection menu.
" + "<header>text=':q or :q!'</header>" + _ "
" + "<big>:q or :q!</big>" + _ "
Quit the scenario (without prompting).
" + "<header>text=':w'</header>" + _ "
" + "<big>:w</big>" + _ "
Save the game (without prompting).
" + "<header>text=':wq'</header>" + _ "
" + "<big>:wq</big>" + _ "
Save the game and quit the scenario (without prompting)."
# wmllint: display off
[/topic]
@ -582,34 +576,34 @@ Save the game and quit the scenario (without prompting)."
title= _ "Multiplayer Commands"
text= _ "These commands can either be issued via the command line by prefixing them with ':' (as shown here) or via the chat by prefixing them with '/' (press 'm' first to open the chat line).
" + "<header>text=':ban <username>'</header>" + _ "
" + "<big>:ban <username></big>" + _ "
Ban a user in a multiplayer game by the IP address used by that username and kick him. Can be used on users not in the game but on the server. (Of course they wont be kicked then.)
" + "<header>text=':control <side> <username>'</header>" + _ "
Change the controller for side (write here the number of the side) to username (write here the nickname of the player or observer). You can check what side belongs to which player in the <bold>text='Scenario Settings'</bold> dialog (Press the <bold>text='More'</bold> button in the <bold>text='Status Table'</bold> (alt+s by default) to get there.). The host can change control of any side.
" + "<big>:control <side> <username></big>" + _ "
Change the controller for side (write here the number of the side) to username (write here the nickname of the player or observer). You can check what side belongs to which player in the <b>Scenario Settings</b> dialog (Press the <b>More</b> button in the <b>Status Table</b> (alt+s by default) to get there.). The host can change control of any side.
" + "<header>text=':give_control'</header>" + _ "
" + "<big>:give_control</big>" + _ "
Launch a dialog to assist the host in changing the human controllers of sides.
" + "<header>text=':idle <side>'</header>" + _ "
" + "<big>:idle <side></big>" + _ "
Toggle the idle state for a side. The host may make a side idle after a network disconnection.
" + "<header>text=':kick <username>'</header>" + _ "
" + "<big>:kick <username></big>" + _ "
Kick a user in multiplayer. They will be able to rejoin the game. If you just want to change control of their side(s) use the :control command instead.
" + "<header>text=':m <username> or :msg <username> or :whisper <username>'</header>" + _ "
" + "<big>:m <username> or :msg <username> or :whisper <username></big>" + _ "
Send a private message to a user. When in a game, it is not possible to send private messages to players who are currently controlling a side in the same game.
" + "<header>text=':mute [<username>]'</header>" + _ "
" + "<big>:mute [<username>]</big>" + _ "
Mute a specific observer. If no username is supplied the muted usernames are displayed.
" + "<header>text=':muteall'</header>" + _ "
" + "<big>:muteall</big>" + _ "
Toggle muting/silencing of all observers on/off.
" + "<header>text=':unban <username>'</header>" + _ "
" + "<big>:unban <username></big>" + _ "
Unban a user in a multiplayer game by the IP address used by that username. Can be used on users not in the game but on the server.
" + "<header>text=':unmute [<username>]'</header>" + _ "
" + "<big>:unmute [<username>]</big>" + _ "
Unmute a specific observer. If no username is supplied the list of muted observers is cleared."
[/topic]
# wmllint: markcheck on
@ -620,34 +614,34 @@ Unmute a specific observer. If no username is supplied the list of muted observe
title= _ "Debug Mode Commands"
text= _ "These commands can either be issued via the command line by prefixing them with ':' (as shown here) or via the chat by prefixing them with '/' (press 'm' first to open the chat line).
" + "<header>text=':choose_level or :cl'</header>" + _ "
" + "<big>:choose_level or :cl</big>" + _ "
Brings up a menu for choosing a scenario to immediately advance to in a campaign.
" + "<header>text=':create <unit type id>'</header>" + _ "
" + "<big>:create <unit type id></big>" + _ "
Create a unit of the specified type on the selected hex.
" + "<header>text=':fog/shroud'</header>" + _ "
" + "<big>:fog/shroud</big>" + _ "
Toggle fog/shroud for the current side.
" + "<header>text=':gold <amount>'</header>" + _ "
" + "<big>:gold <amount></big>" + _ "
Adds the specified amount to the current sides gold.
" + "<header>text=':n'</header>" + _ "
" + "<big>:n</big>" + _ "
Immediately advances to the next scenario in a campaign.
" + "<header>text=':set_var <variable=value>'</header>" + _ "
" + "<big>:set_var <variable=value></big>" + _ "
Manually set a gamestate variable to value.
" + "<header>text=':show_var <variable>'</header>" + _ "
" + "<big>:show_var <variable></big>" + _ "
Show a gamestate variable.
" + "<header>text=':throw/fire <event>'</header>" + _ "
" + "<big>:throw/fire <event></big>" + _ "
Manually fire the specified event.
" + "<header>text=':unit <key=value>'</header>" + _ "
" + "<big>:unit <key=value></big>" + _ "
Modifies the specified property of the selected unit. Example: :unit hitpoints=100
" + "<header>text=':unit <advances=N>'</header>" + _ "
" + "<big>:unit <advances=N></big>" + _ "
Makes the selected unit level up N times. Example: :unit advances=2"
[/topic]
# wmllint: markcheck on

View file

@ -238,7 +238,7 @@
string=Rr
aliasof=Gt
editor_group=flat
help_topic_text= _ "<italic>text='Roads'</italic> are beaten paths of dirt, formed by many travelers passing over them. As far as gameplay is concerned, roads behave as flat terrain."
help_topic_text= _ "<i>Roads</i> are beaten paths of dirt, formed by many travelers passing over them. As far as gameplay is concerned, roads behave as flat terrain."
[/terrain_type]
[terrain_type]
@ -295,7 +295,7 @@
string=Dd
aliasof=Dt
editor_group=desert
help_topic_text= _ "<italic>text='Deserts'</italic> have a somewhat different composition than small sand pits or beaches, however for gameplay purposes they are identical. See <ref>dst='..terrain_sand' text='sand'</ref>."
help_topic_text= _ "<i>Deserts</i> have a somewhat different composition than small sand pits or beaches, however for gameplay purposes they are identical. See <ref>dst='terrain_sand' text='sand'</ref>."
[/terrain_type]
[terrain_type]
@ -306,7 +306,7 @@
string=Ds
aliasof=Dt
editor_group=desert
help_topic_text= _ "The instability of <italic>text='sand'</italic> makes it harder for most units to cross, and leaves them wide open to attack. In contrast, the wide feet or snakelike bodies of the reptilian races make sand much easier for them to navigate.
help_topic_text= _ "The instability of <i>sand</i> makes it harder for most units to cross, and leaves them wide open to attack. In contrast, the wide feet or snakelike bodies of the reptilian races make sand much easier for them to navigate.
Most units receive 20 to 40% defense in sand."
[/terrain_type]
@ -428,7 +428,6 @@ Most units receive 20 to 40% defense in sand."
editor_name= _ "Campfire"
string=^Ecf
aliasof=_bas
light=25
editor_group=embellishments
[/terrain_type]
@ -438,7 +437,6 @@ Most units receive 20 to 40% defense in sand."
editor_name= _ "Sconce"
string=^Efs
aliasof=_bas
light=25
editor_group=embellishments
[/terrain_type]
@ -457,7 +455,6 @@ Most units receive 20 to 40% defense in sand."
editor_name= _ "Lit Brazier"
string=^Ebn
aliasof=_bas
light=25
editor_group=embellishments
[/terrain_type]
@ -967,7 +964,7 @@ Most units receive 20 to 40% defense in sand."
aliasof=Ut, Ht
mvt_alias=-,Ut, Ht
editor_group=cave, rough
help_topic_text= _ "<italic>text='Rockbound cave'</italic> terrain is formed by the action of water and wind, carrying erosive particles that carve the rock. It resembles a scraggy underground cavern which shoulders defense, but is hard for most units to traverse. Only settlers of caves, such as dwarves and trolls, are fully capable of navigating such topography.
help_topic_text= _ "<i>Rockbound cave</i> terrain is formed by the action of water and wind, carrying erosive particles that carve the rock. It resembles a scraggy underground cavern which shoulders defense, but is hard for most units to traverse. Only settlers of caves, such as dwarves and trolls, are fully capable of navigating such topography.
Most units have about 50% defense in rocky caves, whereas cavalry are limited to 40%. Dwarves, by dint of their small size, enjoy 60% defense in rockbound caves.
@ -1075,7 +1072,7 @@ Occasionally caves are <ref>dst='terrain_illuminated_cave' text='illuminated'</r
light=25
max_light=35
editor_group=cave, obstacle
help_topic_text= _ "The dangers inherent in trying to walk on <italic>text='lava'</italic> are fairly obvious. As far as movement is concerned, lava is equivalent to <ref>dst='terrain_unwalkable' text='unwalkable'</ref> terrain, and can only be crossed by those units capable of flying a considerable distance above it. The molten magma also produces a substantial glow, illuminating the area immediately above it. This provides an attack bonus for lawful units and removes the attack bonus from chaotic units."
help_topic_text= _ "The dangers inherent in trying to walk on <i>lava</i> are fairly obvious. As far as movement is concerned, lava is equivalent to <ref>dst='terrain_unwalkable' text='unwalkable'</ref> terrain, and can only be crossed by those units capable of flying a considerable distance above it. The molten magma also produces a substantial glow, illuminating the area immediately above it. This provides an attack bonus for lawful units and removes the attack bonus from chaotic units."
[/terrain_type]
[terrain_type]
@ -1202,7 +1199,6 @@ Occasionally caves are <ref>dst='terrain_illuminated_cave' text='illuminated'</r
aliasof=Xt
editor_group=cave,obstacle
hidden=yes # Superseded by the Sconce embellishment.
hide_help=yes
[/terrain_type]
[terrain_type]
@ -1404,6 +1400,69 @@ Occasionally caves are <ref>dst='terrain_illuminated_cave' text='illuminated'</r
hide_help=yes
[/terrain_type]
#
# ## Previous directionless gate terrains (for backwards compatibility) ##
# ## Should be removed by BfW 1.14.0
#
[terrain_type]
symbol_image=void/void
editor_image=portals/gate-rusty-long-se
id=portal_gate_rusty
name= _ "Gate"
editor_name= _ "Rusty Gate"
string=^Prg
default_base=Rr
aliasof=_bas,Xt
mvt_alias=Xt
editor_group=embellishments,obstacle
hidden=yes
hide_help=yes
[/terrain_type]
[terrain_type]
symbol_image=void/void
editor_image=portals/door-wooden-long-se
id=portal_door_wooden
name= _ "Door"
editor_name= _ "Wooden Door"
string=^Pwd
default_base=Rr
aliasof=_bas,Xt
mvt_alias=Xt
editor_group=embellishments,obstacle
hidden=yes
hide_help=yes
[/terrain_type]
[terrain_type]
symbol_image=void/void
editor_image=portals/gate-rusty-open-long-se
id=p_open_rusty
name= _ "Gate"
editor_name= _ "Rusty Open Gate"
string=^Prgo
default_base=Rr
aliasof=_bas
editor_group=embellishments
hidden=yes
hide_help=yes
[/terrain_type]
[terrain_type]
symbol_image=void/void
editor_image=portals/door-wooden-open-long-se
id=p_open_wooden
name= _ "Door"
editor_name= _ "Wooden Open Door"
string=^Pwdo
default_base=Rr
aliasof=_bas
editor_group=embellishments
hidden=yes
hide_help=yes
[/terrain_type]
#
# ## Invisible obstacle overlays ##
#
@ -1828,7 +1887,7 @@ Occasionally caves are <ref>dst='terrain_illuminated_cave' text='illuminated'</r
id=mermen-village
name= _ "Village"
editor_name= _ "Merfolk Village"
help_topic_text= _ "<italic>text='Submerged villages'</italic> are the homes of merfolk and nagas. While water-dwelling creatures are at home here, land-dwellers have a hard time navigating and defending these villages. However, like any village, the facilities are available to all creatures which allow units to tend to their wounds. Any unit stationed in a village can heal eight hitpoints each turn, or be cured of poison.
help_topic_text= _ "<i>Submerged villages</i> are the homes of merfolk and nagas. While water-dwelling creatures are at home here, land-dwellers have a hard time navigating and defending these villages. However, like any village, the facilities are available to all creatures which allow units to tend to their wounds. Any unit stationed in a village can heal eight hitpoints each turn, or be cured of poison.
Merfolk and nagas have 60% defense in submerged villages, whereas land based units usually have a low defense."
string=^Vm
@ -2370,7 +2429,7 @@ Merfolk and nagas have 60% defense in submerged villages, whereas land based uni
submerge=0
unit_height_adjust=22
editor_group=bridge, water
help_topic_text= _ "To those capable of building one, the ability to lay a <italic>text='bridge'</italic> offers a liberation from the fickle nature of waterways, whose fords come and go with the rise and fall of the waterline. This is to say nothing of the luxury of dry feet, the loss of which is no laughing matter in the cold months of the year.
help_topic_text= _ "To those capable of building one, the ability to lay a <i>bridge</i> offers a liberation from the fickle nature of waterways, whose fords come and go with the rise and fall of the waterline. This is to say nothing of the luxury of dry feet, the loss of which is no laughing matter in the cold months of the year.
For those who go by land or sea, a bridge is the best of both worlds — for gameplay purposes, it is treated either as grassland or the underlying water, whichever offers the best movement and defensive bonuses for the unit occupying the bridge hex. Note that a swimming unit and a land unit are not capable of occupying a bridge hex at the same time."
[/terrain_type]
@ -2720,7 +2779,7 @@ For those who go by land or sea, a bridge is the best of both worlds — for gam
editor_name= _ "Fungus"
string=Uft
hidden=yes
help_topic_text= _ "<italic>text='Mushroom groves'</italic> are vast underground forests of giant mushrooms,
help_topic_text= _ "<i>Mushroom groves</i> are vast underground forests of giant mushrooms,
which thrive in the damp darkness. Most units have trouble negotiating the spongy floor of smaller fungi, but they have plenty of cover behind the larger stalks. Mounted units, however, become completely mired and lack proper freedom of movement in combat. Undead units have a natural affinity for decay and function quite well in mushroom forests.
Most units receive 50% to 60% defense in mushroom groves, whereas cavalry receive only 20%."
@ -2732,7 +2791,7 @@ Most units receive 50% to 60% defense in mushroom groves, whereas cavalry receiv
name= _ "Cave"
editor_name= _ "Cave"
string=Ut
help_topic_text= _ "<italic>text='Cave'</italic> terrain represents any underground cavern with enough room for a unit to pass.
help_topic_text= _ "<i>Cave</i> terrain represents any underground cavern with enough room for a unit to pass.
Most units are wholly unfamiliar with the terrain, and thus are both slowed down and hindered in defense. Dwarves and trolls, who make their homes in caves, both have a relatively easy time navigating this terrain, especially dwarves, who by dint of their small size can navigate many obstacles that other races cannot. Occasionally caves are <ref>dst='terrain_illuminated_cave' text='illuminated'</ref>.
Rare patches of the underground world are illuminated by light from the surface shining down into the gloomy darkness. This provides an attack bonus for lawful units and removes the attack bonus from chaotic units. In all other regards this terrain is functionally identical to normal cave terrains.
@ -2748,7 +2807,7 @@ Most units receive 20 to 40% defense in caves, whereas dwarves have 50%."
editor_name= _ "Sands"
string=Dt
editor_group=desert
help_topic_text= _ "The instability of <italic>text='sand'</italic> makes it harder for most units to cross, and leaves them wide open to attack. In contrast, the wide feet or snakelike bodies of the reptilian races make sand much easier for them to navigate.
help_topic_text= _ "The instability of <i>sand</i> makes it harder for most units to cross, and leaves them wide open to attack. In contrast, the wide feet or snakelike bodies of the reptilian races make sand much easier for them to navigate.
Most units receive 20 to 40% defense in sand."
hidden=yes
@ -2762,10 +2821,10 @@ Most units receive 20 to 40% defense in sand."
string=Wrt
submerge=0.3
editor_group=water
help_topic_text= _ "<italic>text='Coastal reefs'</italic> are shallows formed by stone, coral and sand.
help_topic_text= _ "<i>Coastal reefs</i> are shallows formed by stone, coral and sand.
This provides most land units with a more steady footing and defensive positions than wading in shallow water normally would and also grants most water-dwelling races an exceptionally high defense.
Merfolk and Naga both receive 70% defense on coastal reefs."
Mermen and Naga both receive 70% defense on coastal reefs."
hidden=yes
[/terrain_type]
@ -2776,7 +2835,7 @@ Merfolk and Naga both receive 70% defense on coastal reefs."
editor_name= _ "Hills"
string=Ht
editor_group=rough
help_topic_text= _ "<italic>text='Hills'</italic> represent any reasonably rough terrain,
help_topic_text= _ "<i>Hills</i> represent any reasonably rough terrain,
with enough dips and rises in the ground to provide some cover. Hills are difficult for most troops to navigate. Dwarves, trolls, and orcs have enough familiarity with the terrain that they can pass through it without being slowed down. Cavalry have enough trouble navigating the terrain that any defensive aid lent by cover is negated.
Most units have about 50% defense in hills, whereas cavalry are limited to 40%. Dwarves enjoy 60% defense in hills."
@ -2791,10 +2850,10 @@ Most units have about 50% defense in hills, whereas cavalry are limited to 40%.
string=St
submerge=0.4
editor_group=water
help_topic_text= _ "<italic>text='Swamps'</italic> represent any sort of wetlands.
help_topic_text= _ "<i>Swamps</i> represent any sort of wetlands.
Swamps slow down nearly everyone, and inhibit their ability to defend themselves. An exception to this is any race bodily skilled in navigating water; these receive both full movement and a defensive bonus. Those that make their living in the wetlands are also adept at using this terrain for cover.
Most units make do with 30% defense in swamps. Merfolk, naga, and saurians all generally enjoy 60%."
Most units make do with 30% defense in swamps. Mermen, naga, and saurians all generally enjoy 60%."
hidden=yes
[/terrain_type]
@ -2804,9 +2863,9 @@ Most units make do with 30% defense in swamps. Merfolk, naga, and saurians all g
string=Wst
name= _ "Shallow Water"
editor_name= _ "Shallow Water"
help_topic_text= _ "<italic>text='Shallow water'</italic> represents any body of water deep enough to come up to roughly a mans waist. This is enough to slow down nearly anyone and leave them wide open to attack. Dwarves, given that the water reaches up almost to their heads, have an extremely hard time of this. The exception is any race whose bodies naturally lend themselves to swimming, for which they receive a considerable defensive bonus and full movement.
help_topic_text= _ "<i>Shallow water</i> represents any body of water deep enough to come up to roughly a mans waist. This is enough to slow down nearly anyone and leave them wide open to attack. Dwarves, given that the water reaches up almost to their heads, have an extremely hard time of this. The exception is any race whose bodies naturally lend themselves to swimming, for which they receive a considerable defensive bonus and full movement.
Most units make do with 20 to 30% defense in shallow water, whereas both naga and merfolk enjoy 60%."
Most units make do with 20 to 30% defense in shallow water, whereas both naga and mermen enjoy 60%."
hidden=yes
[/terrain_type]
@ -2819,7 +2878,7 @@ Most units make do with 20 to 30% defense in shallow water, whereas both naga an
unit_height_adjust=3
recruit_onto=yes
editor_group=castle
help_topic_text= _ "<italic>text='Castles'</italic> are any sort of permanent fortification.
help_topic_text= _ "<i>Castles</i> are any sort of permanent fortification.
Nearly all units receive a considerable bonus to their defense by being stationed in a castle, and most units receive full movement in a castle. Stationing units in a castle represents its defensive capability. Without a unit in each wall hex, an enemy can simply sneak into the castle unchallenged, gaining the same defensive bonus as everyone inside.
Most units have about 60% defense in a castle."
@ -2833,7 +2892,7 @@ Most units have about 60% defense in a castle."
editor_name= _ "Mountains"
string=Mt
editor_group=rough
help_topic_text= _ "<italic>text='Mountains'</italic> are steep enough that units often have to climb over obstacles to move.
help_topic_text= _ "<i>Mountains</i> are steep enough that units often have to climb over obstacles to move.
By this nature, they provide a considerable defensive bonus for most troops, but they also severely impede any passage through them. Most cavalry simply cannot enter mountainous terrain; however, elvish cavalry is an exception to this, as are the goblin wolf riders. Both dwarves and trolls are native to mountainous terrain, and have a very easy time getting around.
Most units receive about 60% defense in mountains, whereas Dwarves enjoy 70%."
@ -2845,10 +2904,10 @@ Most units receive about 60% defense in mountains, whereas Dwarves enjoy 70%."
id=deep_water
name= _ "Deep Water"
editor_name= _ "Deep Water"
help_topic_text= _ "<italic>text='Deep water'</italic> represents any body of water deep enough to cover a mans head.
help_topic_text= _ "<i>Deep water</i> represents any body of water deep enough to cover a mans head.
Most units cannot enter deep water: it is the domain of units which can either fly, or are exceptionally strong swimmers.
Merfolk and naga both receive 50% defense in deep water, with full movement."
Mermen and naga both receive 50% defense in deep water, with full movement."
string=Wdt
hidden=yes
[/terrain_type]
@ -2858,7 +2917,7 @@ Merfolk and naga both receive 50% defense in deep water, with full movement."
id=flat
name= _ "Flat"
editor_name= _ "Flat"
help_topic_text= _ "<italic>text='Grassland'</italic> represents open plains, whether cultivated, cut back for grazing, or wild.
help_topic_text= _ "<i>Grassland</i> represents open plains, whether cultivated, cut back for grazing, or wild.
Being open ground, grassland is both very easy to move across, but is also difficult to defend oneself in. Typically, those units that perform best on grassland are either cavalry, or very agile units which take advantage of the open space.
Most units have defense of 30 to 40% on grassland."
@ -2871,7 +2930,7 @@ Most units have defense of 30 to 40% on grassland."
id=forest
name= _ "Forest"
editor_name= _ "Forest"
help_topic_text= _ "<italic>text='Forests'</italic> represent any woodland with significant undergrowth, enough to hinder passage. Though they slow nearly everyone down, forests do offer better defense to most units than open ground. Cavalry, however, have so much trouble navigating them that any benefit gained by stealth is negated. Elves are an exception to this general rule for forests. Not only do they possess full movement in forests, but they also gain a considerable defensive bonus. Dwarves are another exception to this rule; though they are able to plow through the forests without much loss of speed, their utter unfamiliarity with the terrain causes them to receive no defensive bonus.
help_topic_text= _ "<i>Forests</i> represent any woodland with significant undergrowth, enough to hinder passage. Though they slow nearly everyone down, forests do offer better defense to most units than open ground. Cavalry, however, have so much trouble navigating them that any benefit gained by stealth is negated. Elves are an exception to this general rule for forests. Not only do they possess full movement in forests, but they also gain a considerable defensive bonus. Dwarves are another exception to this rule; though they are able to plow through the forests without much loss of speed, their utter unfamiliarity with the terrain causes them to receive no defensive bonus.
Most units have 50% defense in forests, but cavalry are limited to 30%. Elves, on the other hand, enjoy 60 to 70% defense, even their mounted units. Dwarves generally receive only 30% defense in forests."
string=Ft # wmllint: ignore
@ -2884,7 +2943,7 @@ Most units have 50% defense in forests, but cavalry are limited to 30%. Elves, o
name= _ "Frozen"
editor_name= _ "Frozen"
string=At # wmllint: ignore
help_topic_text= _ "<italic>text='Frozen'</italic> terrain represents any flat area that is covered by snow or ice.
help_topic_text= _ "<i>Frozen</i> terrain represents any flat area that is covered by snow or ice.
Most units are slowed down on it, and have a harder time defending themselves. Note that swimming units, even those who can breathe underwater, cannot swim underneath ice.
Most units have 20 to 40% defense in frozen terrain."
@ -2897,7 +2956,7 @@ Most units have 20 to 40% defense in frozen terrain."
name= _ "Village"
editor_name= _ "Village"
string=Vt # wmllint: ignore
help_topic_text= _ "<italic>text='Villages'</italic> represent any group of buildings, human or otherwise.
help_topic_text= _ "<i>Villages</i> represent any group of buildings, human or otherwise.
Almost all units, even cavalry, have an easy time navigating villages, and most units gain a defensive bonus from being stationed in a village. Villages allow units the resources to clean and tend to their wounds, which allows any unit stationed therein to heal eight hitpoints each turn, or to be cured of poison.
Most units have 50 to 60% defense in villages, whereas cavalry receive only 40%."
@ -2920,7 +2979,7 @@ Most units have 50 to 60% defense in villages, whereas cavalry receive only 40%.
name= _ "Unwalkable"
editor_name= _ "Unwalkable"
string=Qt # wmllint: ignore
help_topic_text= _ "<italic>text='Unwalkable terrain'</italic> covers any chasm or gorge which, as the name implies, cannot be crossed simply by walking. Chasms are noted for sheer walls which would take days to traverse. As far as gameplay is concerned, only units capable of flying can cross this terrain."
help_topic_text= _ "<i>Unwalkable terrain</i> covers any chasm or gorge which, as the name implies, cannot be crossed simply by walking. Chasms are noted for sheer walls which would take days to traverse. As far as gameplay is concerned, only units capable of flying can cross this terrain."
hidden=yes
[/terrain_type]
@ -2930,7 +2989,7 @@ Most units have 50 to 60% defense in villages, whereas cavalry receive only 40%.
name= _ "Rails"
editor_name= _ "Rails"
string=Rt # wmllint: ignore
help_topic_text= _ "<italic>text='Rails'</italic> are used to transport ore, mostly by dwarves."
help_topic_text= _ "<i>Rails</i> are used to transport ore, mostly by dwarves."
hide_if_impassable=yes
hidden=yes
[/terrain_type]

View file

@ -62,14 +62,14 @@ _ "Despite orcs reliance on raw strength, few of their children are destined
Drakes are inherently magical creatures, with a mysterious internal fire fueling their very lives. This can easily be witnessed when one of their kind perishes in combat; its internal fire is released, burning their remains in to ashes. Their internal fire is also their greatest weakness; it makes them extremely vulnerable to cold attacks. Despite their magical nature, drakes are incapable of channeling magic in a controlled manner. While the magic imbued within a drakes body enables it to spit fire and gives it life, they have no willful control over the functions of this magic.
<header>text='Society'</header>
<big>Society</big>
Drakes are a relatively warlike race and their societies can be best described as cultured martial societies. The core of a drake tribe is a small group of veteran warriors headed by a mutually respected — or simply feared — dominant who rules the society with an iron fist. Every drake is expected to earn their place in the strict hierarchy, to obey their superiors and command their inferiors. Entry to the ruling elite is only possible through challenging and defeating a superior in single combat, which is the way the hierarchy within the elite itself is established. The use of deception of any kind towards any fellow drake is, without exception, seen as cowardly and unacceptable.
While their warlike nature and sense of territory drives them to defend their territories savagely, drakes rarely invade or trespass on areas already occupied by the other major races. Instead, they settle in unpopulated areas to establish their own territory there. They primarily feed on large game they hunt in the lowlands around their homes, but hatchlings and lower caste drakes are known to feed also on certain kinds of moss and fungi they cultivate deep in their caverns. The only technology drakes value is armor- and weapon-smithing, and neither know or need other science and culture besides this. However, the few implements they do fashion are almost unrivaled in quality, only matched by those produced in the finest Dwarvish foundries.
Drakes hatch from eggs and usually live naturally between 20 to 30 years. Death in battle is the most preferred way for a drake to leave this world. Unlike the elder members of other races, drakes naturally grow more aggressive and reckless towards the ends of their natural lives, perhaps to help ensure their place in the heroic legends of their kind.
<header>text='Geography'</header>
<big>Geography</big>
Drakes originated from an archipelago of volcanic islands called <ref>dst='morogor' text='Morogor'</ref> in the <ref>dst='great_ocean' text='Great Ocean'</ref>. A combination of population pressure and the subsidence of many of their home islands has caused colonies of drakes to spread to the <ref>dst='great_continent' text='Great Continent'</ref>. Drakes tend to make their homes in mountain caverns near volcanoes to protect their eggs, hatchings and forges. While drakes naturally prefer warmth, their internal fire is more than capable of sustaining them even in a relatively cold climate, a feature which has allowed them to populate even some of the mountains of the far north of the Great Continent."
num_traits=2
undead_variation=drake
@ -203,10 +203,10 @@ The means by which gryphons are able to fly despite their great weight has been
plural_name= _ "race^Humans"
description= _"The race of men is an extremely diverse one. Although they originally came from the Old Continent, men have spread all over the world and split into many different cultures and races. Although they are not imbued with magic like other creatures, humans can learn to wield it and are able to learn more types than most others. They have no extra special abilities or aptitudes except their versatility and drive. While often at odds with other races, they can occasionally form alliances with the less aggressive races such as elves and dwarves. The less scrupulous among them do not shrink back from hiring orcish mercenaries, either. They have no natural enemies, although the majority of men, like most people of all races, have an instinctive dislike of the undead. Men are shorter than the elves, but taller still than dwarves. Their skin color can vary, from almost white to dark brown.
<header>text='Subjects of the Crown'</header>
<big>Subjects of the Crown</big>
Many different groups of men exist, but the majority of them on the Great Continent live under the rule of the Crown of Wesnoth. The humans first appeared on the Great Continent from a land far across the ocean to the West, the Green Isle, and soon established their capital at the inland city of Weldyn. Over the following centuries they have built up a number cities across the continent. The soldiers from the Crown of Wesnoth protect the country, forming the most organized military force in the known world. Its warriors come from the main provinces, where all men are conscripted at an early age.
<header>text='The Clansmen'</header>
<big>The Clansmen</big>
The eastern provinces of Wesnoth, known as the Clan Homelands, have a geography consisting of more open plains and rolling hills than the western, more civilized provinces. They are home to the Horse Clans, who are allied with the Crown of Wesnoth but operate independently and maintain their own identity. Some consider them to be a tributary state, which sends food and soldiers to Crown in exchange for protection. Others say they are on equal footing with the western half of Wesnoth. In any case, the eastern provinces do not have a conscript army the way Western Wesnoth does. Training for fighting is part of the way of life of the Clans; the parents teach the children to ride horses, fight and shoot a bow from an early age. In general, the Clan warriors are less organized than the civilized fighters, and the strengths and weaknesses of these groups complement each other."
num_traits=2
{HUMAN_NAMES}
@ -271,7 +271,7 @@ Saurians live spectacularly short lives by comparison to most of the other races
id=mechanical
name= _ "race^Mechanical"
plural_name= _ "race+plural^Mechanical"
description= _ "Animated neither by natural life nor by necromancy, the term <italic>text='mechanical'</italic> describes a created artifact of an intelligent being. Most mechanical things neither move nor think on their own, but some do so as a result of magical enchantment."
description= _ "Animated neither by natural life nor by necromancy, the term <i>mechanical</i> describes a created artifact of an intelligent being. Most mechanical things neither move nor think on their own, but some do so as a result of magical enchantment."
num_traits=1
ignore_global_traits=yes
{TRAIT_MECHANICAL}
@ -329,7 +329,7 @@ Saurians live spectacularly short lives by comparison to most of the other races
plural_name= _ "race^Orcs"
description= _"In appearance, orcs resemble humans but with some bestial features. They are taller, sturdier and stronger than humans. They are warlike, savage, and cruel by nature. Their blood is darker and thicker than that of humans, and they have little care for personal hygiene or their personal appearance. Although Orcs are violent even among themselves, they are pack-oriented; an orc never travels alone or lives in groups smaller than half a dozen.
<header>text='Society'</header>
<big>Society</big>
Almost every orc is a member of a tribe or a clan. Relations between neighboring tribes are usually violent, except in cases of a mutual enemy threatens their existence or prospects of great plunder override mutual animosity. Occasionally, a single strong chieftain may emerge to lead multiple tribes from time to time, usually through intimidation of followers. An orc tribe in times of peace tends to focus almost solely on strengthening itself in preparation for the next armed conflict. Orcs are known to possess a crude system of writing — usually in blood — although its most commonly used to trade insults or threats among tribal leaders.
Orc societies are based on little else but strength; might makes right, and a leader leads and survives only as long as no one manages to wrest the title from him. A constant struggle for power simmers among potential tribal chiefs. An orcish leader rarely lives more than a handful of years to enjoy his absolute authority before being killed for his position — although history knows some notable exceptions. Orcs hold no particular honor code and while indisputable raw strength is usually the preferred method of displaying power, assassination, poisoning and backstabbing are completely viable means to further ones own goals.
@ -354,7 +354,7 @@ Orcs who were not the strongest of their litter tend to specialize in other skil
Trolls are seen by many as being little more than yet another race of savage monsters. This common misconception is in part perpetuated by orcs to persuade trolls to join their armies. Because they are rather simple and do not understand the ways of other races or sometimes cannot even tell them apart, it is usually easy for an orcish band to convince a group of trolls that by joining them they get to exact revenge on those that have before hunted them. These new recruits are then directed to attack whoever the orcs themselves are currently in conflict with, whether previously a foe of the trolls or not, accumulating even more enemies for the misled trolls. The most common enemy of trolls are dwarves, and the animosity between these two races is ancient.
<header>text='Geography'</header>
<big>Geography</big>
Trolls have inhabited the mountains of the Great Continent longer than the dwarves who migrated there. Trolls are a common sight on the mountain ranges north and east of Wesnoth, and wherever Orcish hordes travel."
num_traits=2
undead_variation=troll
@ -377,7 +377,7 @@ Trolls have inhabited the mountains of the Great Continent longer than the dwarv
Necromancy is almost solely limited to humans. Even the legends of magically apt races like elves and merfolk tell of very few of their kind who have ever delved in the dark arts. It is surmised that necromantic magic requires great adaptability and a flexible mind, extremes of which are most commonly found in humans. The ultimate goal of most necromancers is to turn the same art of preserving and imbuing life upon themselves, to alter themselves at whatever cost, to ultimately escape death by preserving their own mind and spirit.
<header>text='Geography'</header>
<big>Geography</big>
While undead lords arrived on the Great Continent in considerable numbers only in the wake of Haldric I, they were not completely unheard of by elves and dwarves before that."
num_traits=1
ignore_global_traits=yes

View file

@ -0,0 +1,106 @@
#textdomain wesnoth-lib
###
### Definition of a toggle button to toggle sections in the help browser.
###
#define _GUI_ICON ICON
[image]
x = 0
y = 1 # The images are 24px high
w = "(image_width)"
h = "(image_height)"
name = {ICON}
[/image]
#enddef
[toggle_button_definition]
id = "help_section_toggle"
description = "This toggle button is meant to be used in topic tree in the help browser"
[resolution]
min_width = 26
min_height = 26
default_width = 26
default_height = 26
max_width = 26
max_height = 26
text_extra_width = 0
text_font_size = 0
[state]
[enabled]
[draw]
{_GUI_ICON ("help/closed_section.png")}
[/draw]
[/enabled]
[disabled]
[draw]
{_GUI_ICON ("help/closed_section.png~GS()")}
[/draw]
[/disabled]
[focused]
[draw]
{_GUI_ICON ("help/closed_section.png")}
[/draw]
[/focused]
[/state]
###
### Selected
###
[state]
[enabled]
[draw]
{_GUI_ICON ("help/open_section.png")}
[/draw]
[/enabled]
[disabled]
[draw]
{_GUI_ICON ("help/open_section.png~GS()")}
[/draw]
[/disabled]
[focused]
[draw]
{_GUI_ICON ("help/open_section.png")}
[/draw]
[/focused]
[/state]
[/resolution]
[/toggle_button_definition]
#undef _GUI_ICON

View file

@ -1,5 +1,60 @@
#textdomain wesnoth-lib
#define _GUI_NODE _ID _TOGGLE_OR_IMAGE_WML
[node]
id = {_ID}
[node_definition]
[row]
[column]
horizontal_grow = true
[toggle_panel]
id = "tree_view_node_label"
[grid]
[row]
[column]
grow_factor = 0
border = "all"
border_size = 5
{_TOGGLE_OR_IMAGE_WML}
[/column]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_grow = true
[label]
id = "topic_name"
linked_group = "names"
definition = "default_small"
[/label]
[/column]
[/row]
[/grid]
[/toggle_panel]
[/column]
[/row]
[/node_definition]
[/node]
#enddef
#define _GUI_TOPIC_TREE
[tree_view]
id = "topic_tree"
@ -10,66 +65,22 @@
indentation_step_size = 20
[node]
id = "topic"
{_GUI_NODE "section" (
[toggle_button]
id = "tree_view_node_toggle"
definition = "help_section_toggle"
linked_group = "images"
[/toggle_button]
)}
[node_definition]
[row]
[column]
horizontal_grow = true
[toggle_panel]
id = "tree_view_node_label"
[grid]
[row]
[column]
border = "all"
border_size = 5
[image]
id = "topic_icon"
label = "help/topic.png"
linked_group = "images"
[/image]
[/column]
[column]
border = "all"
border_size = 5
[label]
id = "topic_name"
linked_group = "names"
[/label]
[/column]
[column]
border = "all"
border_size = 5
[spacer]
width = 5
[/spacer]
[/column]
[/row]
[/grid]
[/toggle_panel]
[/column]
[/row]
[/node_definition]
[/node]
{_GUI_NODE "topic" (
[image]
id = "topic_image"
definition = "default"
label = "help/topic.png"
linked_group = "images"
[/image]
)}
[/tree_view]
#enddef
@ -104,7 +115,7 @@
[grid]
[row]
grow_factor = 1
grow_factor = 0
[column]
border = "all"
@ -119,16 +130,19 @@
[/row]
[row]
grow_factor = 1
[column]
grow_factor = 1
horizontal_grow = true
vertical_grow = true
[grid]
[row]
grow_factor = 0
[column]
grow_factor = 0
border = "all"
border_size = 5
horizontal_grow = true
@ -139,12 +153,31 @@
[column]
grow_factor = 1
horizontal_grow = true
vertical_grow = true
[multi_page]
id = "topic_text_pages"
[page_definition]
[row]
grow_factor = 0
[column]
border = "left,right,bottom"
border_size = 5
horizontal_grow = true
vertical_grow = true
[label]
definition = "title"
id = "topic_title"
[/label]
[/column]
[/row]
[row]
grow_factor = 1
@ -155,8 +188,9 @@
vertical_grow = true
[scroll_label]
definition = "default"
definition = "default_small"
id = "topic_text"
use_markup = true
[/scroll_label]
[/column]
@ -177,16 +211,57 @@
[/row]
[row]
grow_factor = 0
[column]
border = "all"
border_size = 5
horizontal_alignment = "right"
horizontal_grow = true
[button]
id = "cancel"
label = _ "Close"
[/button]
[grid]
[row]
[column]
grow_factor = 0
border = "all"
border_size = 5
horizontal_alignment = "left"
[button]
id = "back"
definition = "left_arrow_ornate"
[/button]
[/column]
[column]
grow_factor = 0
border = "all"
border_size = 5
horizontal_alignment = "left"
[button]
id = "next"
definition = "right_arrow_ornate"
[/button]
[/column]
[column]
grow_factor = 1
border = "all"
border_size = 5
horizontal_alignment = "right"
[button]
id = "cancel"
label = _ "Close"
[/button]
[/column]
[/row]
[/grid]
[/column]
@ -199,3 +274,4 @@
[/window]
#undef _GUI_TOPIC_TREE
#undef _GUI_NODE

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/drakes/inferno.png~BG()' align='middle'</img>
"+_"The <bold>text='Drakes'</bold> are a faction of dragon-like <ref>text='Drakes' dst='..race_drake'</ref> and their lizard <ref>text='Saurian' dst='..race_lizard'</ref> allies. Drakes are descendants of dragons, but smaller in size. Saurians are considerably smaller than Drakes and from different ancestry. Together, the Drake faction has high mobility but low defense, leading to unusual tactics for a <ref>text='Default Era' dst='..era_era_default'</ref> faction."
"+_"The <b>Drakes</b> are a faction of dragon-like <ref>text='Drakes' dst='..race_drake'</ref> and their lizard <ref>text='Saurian' dst='..race_lizard'</ref> allies. Drakes are descendants of dragons, but smaller in size. Saurians are considerably smaller than Drakes and from different ancestry. Together, the Drake faction has high mobility but low defense, leading to unusual tactics for a <ref>text='Default Era' dst='..era_era_default'</ref> faction."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,archer,healer,scout,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/drakes/inferno.png~BG()' align='middle'</img>
"+_"The <bold>text='Drakes'</bold> are a faction of dragon-like <ref>text='Drakes' dst='..race_drake'</ref> and their lizard <ref>text='Saurian' dst='..race_lizard'</ref> allies. Drakes are descendants of dragons, but smaller in size. Saurians are considerably smaller than Drakes and from different ancestry. Together, the Drake faction has high mobility but low defense, leading to unusual tactics for a <ref>text='Default Era' dst='..era_era_default'</ref> faction."
"+_"The <b>Drakes</b> are a faction of dragon-like <ref>text='Drakes' dst='..race_drake'</ref> and their lizard <ref>text='Saurian' dst='..race_lizard'</ref> allies. Drakes are descendants of dragons, but smaller in size. Saurians are considerably smaller than Drakes and from different ancestry. Together, the Drake faction has high mobility but low defense, leading to unusual tactics for a <ref>text='Default Era' dst='..era_era_default'</ref> faction."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,archer,healer,scout,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/dunefolk/herbalist.png~BG()' align='middle'</img>
"+_"The <bold>text='Dunefolk'</bold> are a faction of humans from the deserts and hills of the southern lands. The Dunefolk specialize in using terrain features to coordinate attacks at dawn or dusk. Their ranks boast lawful and liminal units, healers, high-accuracy melee fighters, and fearsome horse-mounted archers. While Dunefolk units tend to be more expensive than those of the Loyalist faction, they make up for this with high mobility — especially on hilly terrains."
"+_"The <b>Dunefolk</b> are a faction of humans from the deserts and hills of the southern lands. The Dunefolk specialize in using terrain features to coordinate attacks at dawn or dusk. Their ranks boast lawful and liminal units, healers, high-accuracy melee fighters, and fearsome horse-mounted archers. While Dunefolk units tend to be more expensive than those of the Loyalist faction, they make up for this with high mobility — especially on hilly terrains."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,mixed fighter,archer,archer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/dunefolk/herbalist.png~BG()' align='middle'</img>
"+_"The <bold>text='Dunefolk'</bold> are a faction of humans from the deserts and hills of the southern lands. The Dunefolk specialize in using terrain features to coordinate attacks at dawn or dusk. Their ranks boast lawful and liminal units, healers, high-accuracy melee fighters, and fearsome horse-mounted archers. While Dunefolk units tend to be more expensive than those of the Loyalist faction, they make up for this with high mobility — especially on hilly terrains."
"+_"The <b>Dunefolk</b> are a faction of humans from the deserts and hills of the southern lands. The Dunefolk specialize in using terrain features to coordinate attacks at dawn or dusk. Their ranks boast lawful and liminal units, healers, high-accuracy melee fighters, and fearsome horse-mounted archers. While Dunefolk units tend to be more expensive than those of the Loyalist faction, they make up for this with high mobility — especially on hilly terrains."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,mixed fighter,archer,archer,scout

View file

@ -10,7 +10,7 @@
# wmllint: markcheck off
description="<img>src='portraits/dwarves/ulfserker.png~BG()' align='middle'</img>
"+_"The <bold>text='Knalgan Alliance'</bold> is a faction of <ref>text='Dwarves' dst='..race_dwarf'</ref> and their outlaw <ref>text='Human' dst='..race_human'</ref> allies. Dwarves are an old race who live underground and have tough, but short, warriors. The outlaws are humans who are not socially acceptable among others of their race, but have become allies of the dwarves due to common enemies. This leads to a combination of tough and defensive dwarves who are only good on certain terrain and humans who can cover ground that dwarves are not good at fighting in."
"+_"The <b>Knalgan Alliance</b> is a faction of <ref>text='Dwarves' dst='..race_dwarf'</ref> and their outlaw <ref>text='Human' dst='..race_human'</ref> allies. Dwarves are an old race who live underground and have tough, but short, warriors. The outlaws are humans who are not socially acceptable among others of their race, but have become allies of the dwarves due to common enemies. This leads to a combination of tough and defensive dwarves who are only good on certain terrain and humans who can cover ground that dwarves are not good at fighting in."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,archer,scout

View file

@ -10,7 +10,7 @@
# wmllint: markcheck off
description="<img>src='portraits/dwarves/ulfserker.png~BG()' align='middle'</img>
"+_"The <bold>text='Knalgan Alliance'</bold> is a faction of <ref>text='Dwarves' dst='..race_dwarf'</ref> and their outlaw <ref>text='Human' dst='..race_human'</ref> allies. Dwarves are an old race who live underground and have tough, but short, warriors. The outlaws are humans who are not socially acceptable among others of their race, but have become allies of the dwarves due to common enemies. This leads to a combination of tough and defensive dwarves who are only good on certain terrain and humans who can cover ground that dwarves are not good at fighting in."
"+_"The <b>Knalgan Alliance</b> is a faction of <ref>text='Dwarves' dst='..race_dwarf'</ref> and their outlaw <ref>text='Human' dst='..race_human'</ref> allies. Dwarves are an old race who live underground and have tough, but short, warriors. The outlaws are humans who are not socially acceptable among others of their race, but have become allies of the dwarves due to common enemies. This leads to a combination of tough and defensive dwarves who are only good on certain terrain and humans who can cover ground that dwarves are not good at fighting in."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,archer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/humans/grand-knight.png~BG()' align='middle'</img>
"+_"The <bold>text='Loyalists'</bold> are a faction of <ref>text='Humans' dst='..race_human'</ref> who are loyal to the throne of Wesnoth. Humans are a versatile race who specialize in many different areas. Similarly, the Loyalist faction is a very versatile melee-oriented faction with important ranged support from bowmen and mages."
"+_"The <b>Loyalists</b> are a faction of <ref>text='Humans' dst='..race_human'</ref> who are loyal to the throne of Wesnoth. Humans are a versatile race who specialize in many different areas. Similarly, the Loyalist faction is a very versatile melee-oriented faction with important ranged support from bowmen and mages."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,archer,mixed fighter,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/humans/grand-knight.png~BG()' align='middle'</img>
"+_"The <bold>text='Loyalists'</bold> are a faction of <ref>text='Humans' dst='..race_human'</ref> who are loyal to the throne of Wesnoth. Humans are a versatile race who specialize in many different areas. Similarly, the Loyalist faction is a very versatile melee-oriented faction with important ranged support from bowmen and mages."
"+_"The <b>Loyalists</b> are a faction of <ref>text='Humans' dst='..race_human'</ref> who are loyal to the throne of Wesnoth. Humans are a versatile race who specialize in many different areas. Similarly, the Loyalist faction is a very versatile melee-oriented faction with important ranged support from bowmen and mages."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,archer,mixed fighter,scout

View file

@ -13,7 +13,7 @@
description="<img>src='portraits/orcs/warlord.png~BG()' align='middle'</img>"+# wmllint: noconvert
"
"+_"The <bold>text='Northerners'</bold> are a faction of <ref>text='Orcs' dst='..race_orc'</ref> and their allies who live in the north of the Great Continent, thus their name. Northerners consist of the warrior orcs race, the enslaved <ref>text='goblins' dst='..race_goblin'</ref>, <ref>text='trolls' dst='..race_troll'</ref> who are tricked into combat by the orcs, and the serpentine <ref>text='naga' dst='..race_naga'</ref>. The Northerners play best by taking advantage of having many low-cost and high HP soldiers."
"+_"The <b>Northerners</b> are a faction of <ref>text='Orcs' dst='..race_orc'</ref> and their allies who live in the north of the Great Continent, thus their name. Northerners consist of the warrior orcs race, the enslaved <ref>text='goblins' dst='..race_goblin'</ref>, <ref>text='trolls' dst='..race_troll'</ref> who are tricked into combat by the orcs, and the serpentine <ref>text='naga' dst='..race_naga'</ref>. The Northerners play best by taking advantage of having many low-cost and high HP soldiers."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,archer,scout

View file

@ -12,7 +12,7 @@
description="<img>src='portraits/orcs/warlord.png~BG()' align='middle'</img>"+# wmllint: noconvert
"
"+_"The <bold>text='Northerners'</bold> are a faction of <ref>text='Orcs' dst='..race_orc'</ref> and their allies who live in the north of the Great Continent, thus their name. Northerners consist of the warrior orcs race, the enslaved <ref>text='goblins' dst='..race_goblin'</ref>, <ref>text='trolls' dst='..race_troll'</ref> who are tricked into combat by the orcs, and the serpentine <ref>text='naga' dst='..race_naga'</ref>. The Northerners play best by taking advantage of having many low-cost and high HP soldiers."
"+_"The <b>Northerners</b> are a faction of <ref>text='Orcs' dst='..race_orc'</ref> and their allies who live in the north of the Great Continent, thus their name. Northerners consist of the warrior orcs race, the enslaved <ref>text='goblins' dst='..race_goblin'</ref>, <ref>text='trolls' dst='..race_troll'</ref> who are tricked into combat by the orcs, and the serpentine <ref>text='naga' dst='..race_naga'</ref>. The Northerners play best by taking advantage of having many low-cost and high HP soldiers."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,archer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/elves/druid.png~BG()' align='middle'</img>
"+_"The <bold>text='Rebels'</bold> are a faction of <ref>text='Elves' dst='..race_elf'</ref> and their various forest-dwelling allies. They get their human name, Rebels, from the time of Heir to the Throne, when they started the rebellion against the evil Queen Asheviere. Elves are a magical race that are masters of the bow and are capable of living many years longer than humans. In harmony with nature, the elves find allies with the <ref>text='human' dst='..race_human'</ref> mages, certain <ref>text='merfolk' dst='..race_merman'</ref>, and tree creatures called <ref>text='Woses' dst='..race_wose'</ref>. Rebels are best played taking advantage of their high forest defense, mastery of ranged attacks, and the elves neutral alignment."
"+_"The <b>Rebels</b> are a faction of <ref>text='Elves' dst='..race_elf'</ref> and their various forest-dwelling allies. They get their human name, Rebels, from the time of Heir to the Throne, when they started the rebellion against the evil Queen Asheviere. Elves are a magical race that are masters of the bow and are capable of living many years longer than humans. In harmony with nature, the elves find allies with the <ref>text='human' dst='..race_human'</ref> mages, certain <ref>text='merfolk' dst='..race_merman'</ref>, and tree creatures called <ref>text='Woses' dst='..race_wose'</ref>. Rebels are best played taking advantage of their high forest defense, mastery of ranged attacks, and the elves neutral alignment."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,archer,archer,mixed fighter,healer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/elves/druid.png~BG()' align='middle'</img>
"+_"The <bold>text='Rebels'</bold> are a faction of <ref>text='Elves' dst='..race_elf'</ref> and their various forest-dwelling allies. They get their human name, Rebels, from the time of Heir to the Throne, when they started the rebellion against the evil Queen Asheviere. Elves are a magical race that are masters of the bow and are capable of living many years longer than humans. In harmony with nature, the elves find allies with the <ref>text='human' dst='..race_human'</ref> mages, certain <ref>text='merfolk' dst='..race_merman'</ref>, and tree creatures called <ref>text='Woses' dst='..race_wose'</ref>. Rebels are best played taking advantage of their high forest defense, mastery of ranged attacks, and the elves neutral alignment."
"+_"The <b>Rebels</b> are a faction of <ref>text='Elves' dst='..race_elf'</ref> and their various forest-dwelling allies. They get their human name, Rebels, from the time of Heir to the Throne, when they started the rebellion against the evil Queen Asheviere. Elves are a magical race that are masters of the bow and are capable of living many years longer than humans. In harmony with nature, the elves find allies with the <ref>text='human' dst='..race_human'</ref> mages, certain <ref>text='merfolk' dst='..race_merman'</ref>, and tree creatures called <ref>text='Woses' dst='..race_wose'</ref>. Rebels are best played taking advantage of their high forest defense, mastery of ranged attacks, and the elves neutral alignment."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,archer,archer,mixed fighter,healer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/undead/spectre.png~BG()' align='middle'</img>
"+_"The <bold>text='Undead'</bold> are a faction of <ref>text='undead' dst='..race_undead'</ref> creatures and <ref>text='human' dst='..race_human'</ref> practitioners of dark arts that usually accompany them. Often, these “Dark Adepts” are the units that do the most damage for the faction, but they have a major vulnerability — their practicing of this forbidden, evil magic has consumed all their energy and so they have no melee attack at all. The Undead are a very aggressive faction and the most powerful <ref>text='Default Era' dst='..era_era_default'</ref> faction at nighttime."
"+_"The <b>Undead</b> are a faction of <ref>text='undead' dst='..race_undead'</ref> creatures and <ref>text='human' dst='..race_human'</ref> practitioners of dark arts that usually accompany them. Often, these “Dark Adepts” are the units that do the most damage for the faction, but they have a major vulnerability — their practicing of this forbidden, evil magic has consumed all their energy and so they have no melee attack at all. The Undead are a very aggressive faction and the most powerful <ref>text='Default Era' dst='..era_era_default'</ref> faction at nighttime."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,fighter,mixed fighter,archer,scout

View file

@ -11,7 +11,7 @@
# wmllint: markcheck off
description="<img>src='portraits/undead/spectre.png~BG()' align='middle'</img>
"+_"The <bold>text='Undead'</bold> are a faction of <ref>text='undead' dst='..race_undead'</ref> creatures and <ref>text='human' dst='..race_human'</ref> practitioners of dark arts that usually accompany them. Often, these “Dark Adepts” are the units that do the most damage for the faction, but they have a major vulnerability — their practicing of this forbidden, evil magic has consumed all their energy and so they have no melee attack at all. The Undead are a very aggressive faction and the most powerful <ref>text='Default Era' dst='..era_era_default'</ref> faction at nighttime."
"+_"The <b>Undead</b> are a faction of <ref>text='undead' dst='..race_undead'</ref> creatures and <ref>text='human' dst='..race_human'</ref> practitioners of dark arts that usually accompany them. Often, these “Dark Adepts” are the units that do the most damage for the faction, but they have a major vulnerability — their practicing of this forbidden, evil magic has consumed all their energy and so they have no melee attack at all. The Undead are a very aggressive faction and the most powerful <ref>text='Default Era' dst='..era_era_default'</ref> faction at nighttime."
# wmllint: markcheck on
[ai]
recruitment_pattern=fighter,fighter,archer,fighter,scout,archer

View file

@ -41,7 +41,6 @@ pathfind/astarsearch.cpp
pathutils.cpp
quit_confirmation.cpp
reports.cpp
show_dialog.cpp
sound.cpp
sound_music_track.cpp
soundsource.cpp
@ -54,10 +53,5 @@ tooltips.cpp
utils/make_enum.cpp
video.cpp
widgets/button.cpp
widgets/menu.cpp
widgets/menu_style.cpp
widgets/scrollarea.cpp
widgets/scrollbar.cpp
widgets/textbox.cpp
widgets/widget.cpp
wml_exception.cpp

View file

@ -103,7 +103,6 @@ editor/toolkit/editor_toolkit.cpp
fake_unit_manager.cpp
fake_unit_ptr.cpp
filesystem_sdl.cpp
floating_textbox.cpp
formula/callable_objects.cpp
formula/debugger.cpp
formula/debugger_fwd.cpp
@ -265,12 +264,15 @@ gui/widgets/viewport.cpp
gui/widgets/widget.cpp
gui/widgets/widget_helpers.cpp
halo.cpp
help/constants.cpp
help/help.cpp
help/help_browser.cpp
help/help_impl.cpp
help/help_menu.cpp
help/help_text_area.cpp
help/help_topic_generators.cpp
help/manager.cpp
help/section.cpp
help/section_generators.cpp
help/topic.cpp
help/topic_generators.cpp
help/topic_text_generators.cpp
help/utils.cpp
hotkey/hotkey_handler.cpp
hotkey/hotkey_handler_mp.cpp
hotkey/hotkey_handler_sp.cpp

View file

@ -29,6 +29,7 @@
#include "units/abilities.hpp"
#include "units/udisplay.hpp"
#include "units/map.hpp"
#include "units/unit.hpp"
#include <list>
#include <vector>

View file

@ -18,6 +18,8 @@
#include "display.hpp"
#include "events.hpp"
#include "game_config_manager.hpp"
#include "gui/core/event/handler.hpp" // gui2::is_in_dialog
#include "gui/dialogs/loading_screen.hpp"
#include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_command.hpp"
#include "log.hpp"
@ -25,8 +27,6 @@
#include "mouse_handler_base.hpp"
#include "preferences/game.hpp"
#include "scripting/plugins/context.hpp"
#include "show_dialog.hpp" //gui::in_dialog
#include "gui/core/event/handler.hpp" // gui2::is_in_dialog
#include "soundsource.hpp"
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
@ -50,7 +50,17 @@ controller_base::~controller_base()
void controller_base::handle_event(const SDL_Event& event)
{
if(gui::in_dialog()) {
/* TODO: since GUI2 and the main game are now part of the same event context, there is some conflict
* between the GUI2 and event handlers such as these. By design, the GUI2 sdl handler is always on top
* of the handler queue, so its events are handled last. This means events here have a chance to fire
* first. have_keyboard_focus currently returns false if a dialog open, but this is just as stopgap
* measure. We need to figure out a better way to filter out events.
*/
//if(gui2::is_in_dialog()) {
// return;
//}
if(gui2::dialogs::loading_screen::displaying()) {
return;
}
@ -162,7 +172,7 @@ void controller_base::keyup_listener::handle_event(const SDL_Event& event)
bool controller_base::have_keyboard_focus()
{
return true;
return !gui2::is_in_dialog();
}
bool controller_base::handle_scroll(int mousex, int mousey, int mouse_flags, double x_axis, double y_axis)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
/*
Copyright (C) 2008 - 2018 by Tomasz Sniatowski <kailoran@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -43,9 +43,10 @@
#include "desktop/clipboard.hpp"
#include "floating_label.hpp"
#include "game_board.hpp"
#include "help/help.hpp"
#include "preferences/game.hpp"
#include "gettext.hpp"
#include "picture.hpp"
#include "image.hpp"
#include "preferences/display.hpp"
#include "sound.hpp"
#include "units/unit.hpp"
@ -61,42 +62,38 @@ static std::vector<std::string> saved_windows_;
namespace editor {
editor_controller::editor_controller()
: controller_base()
editor_controller::editor_controller(const config &game_config)
: controller_base(game_config)
, mouse_handler_base()
, quit_confirmation(std::bind(&editor_controller::quit_confirm, this))
, active_menu_(editor::MAP)
, reports_(new reports())
, gui_(new editor_display(*this, *reports_, controller_base::get_theme(game_config_, "editor")))
, gui_(new editor_display(*this, controller_base::get_theme(game_config, "editor")))
, tods_()
, context_manager_(new context_manager(*gui_.get(), game_config_))
, toolkit_(nullptr)
, tooltip_manager_()
, floating_label_manager_(nullptr)
, help_manager_(nullptr)
, do_quit_(false)
, quit_mode_(EXIT_ERROR)
, music_tracks_()
{
init_gui();
toolkit_.reset(new editor_toolkit(*gui_.get(), key_, game_config_, *context_manager_.get()));
help_manager_.reset(new help::help_manager(&game_config_));
context_manager_->locs_ = toolkit_->get_palette_manager()->location_palette_.get();
context_manager_->switch_context(0, true);
init_tods(game_config_);
init_music(game_config_);
init_tods(game_config);
init_music(game_config);
get_current_map_context().set_starting_position_labels(gui());
cursor::set(cursor::NORMAL);
gui().create_buttons();
gui().redraw_everything();
join();
}
void editor_controller::init_gui()
{
gui_->change_display_context(&get_current_map_context());
preferences::set_preference_display_settings();
gui_->add_redraw_observer(std::bind(&editor_controller::display_redraw_callback, this, _1));
//gui_->add_redraw_observer(std::bind(&editor_controller::display_redraw_callback, this, _1));
floating_label_manager_.reset(new font::floating_label_context());
gui().set_draw_coordinates(preferences::editor::draw_hex_coordinates());
gui().set_draw_terrain_codes(preferences::editor::draw_terrain_codes());
@ -162,6 +159,8 @@ void editor_controller::init_music(const config& game_config)
editor_controller::~editor_controller()
{
help::reset();
resources::tod_manager = nullptr;
resources::filter_con = nullptr;
@ -174,10 +173,10 @@ EXIT_STATUS editor_controller::main_loop()
while (!do_quit_) {
play_slice();
}
} catch (const editor_exception& e) {
} catch (editor_exception& e) {
gui2::show_transient_message(_("Fatal error"), e.what());
return EXIT_ERROR;
} catch (const wml_exception& e) {
} catch (wml_exception& e) {
e.show();
}
return quit_mode_;
@ -190,10 +189,10 @@ void editor_controller::do_screenshot(const std::string& screenshot_filename /*
{
try {
surface screenshot = gui().screenshot(true);
if(screenshot.null() || image::save_image(screenshot, screenshot_filename) != image::save_result::success) {
if(screenshot.null() || !image::save_image(screenshot, screenshot_filename)) {
ERR_ED << "Screenshot creation failed!\n";
}
} catch (const wml_exception& e) {
} catch (wml_exception& e) {
e.show();
}
}
@ -650,10 +649,12 @@ bool editor_controller::do_execute_command(const hotkey::hotkey_command& cmd, in
//TODO mark the map as changed
sound::play_music_once(music_tracks_[index].id());
get_current_map_context().add_to_playlist(music_tracks_[index]);
#if 0
std::vector<config> items;
items.emplace_back("id", "editor-playlist");
std::shared_ptr<gui::button> b = gui_->find_menu_button("menu-playlist");
show_menu(items, b->location().x +1, b->location().y + b->height() +1, false, *gui_);
#endif
return true;
}
case SCHEDULE:
@ -927,10 +928,6 @@ bool editor_controller::do_execute_command(const hotkey::hotkey_command& cmd, in
// Side specific ones
case HOTKEY_EDITOR_SIDE_NEW:
if(get_current_map_context().teams().size() >= 9) {
size_t new_side_num = get_current_map_context().teams().size() + 1;
toolkit_->get_palette_manager()->location_palette_->add_item(std::to_string(new_side_num));
}
get_current_map_context().new_side();
gui_->init_flags();
return true;
@ -972,17 +969,14 @@ bool editor_controller::do_execute_command(const hotkey::hotkey_command& cmd, in
case HOTKEY_EDITOR_DRAW_COORDINATES:
gui().set_draw_coordinates(!gui().get_draw_coordinates());
preferences::editor::set_draw_hex_coordinates(gui().get_draw_coordinates());
gui().invalidate_all();
return true;
case HOTKEY_EDITOR_DRAW_TERRAIN_CODES:
gui().set_draw_terrain_codes(!gui().get_draw_terrain_codes());
preferences::editor::set_draw_terrain_codes(gui().get_draw_terrain_codes());
gui().invalidate_all();
return true;
case HOTKEY_EDITOR_DRAW_NUM_OF_BITMAPS:
gui().set_draw_num_of_bitmaps(!gui().get_draw_num_of_bitmaps());
preferences::editor::set_draw_num_of_bitmaps(gui().get_draw_num_of_bitmaps());
gui().invalidate_all();
return true;
case HOTKEY_EDITOR_REMOVE_LOCATION: {
location_palette* lp = dynamic_cast<location_palette*>(&toolkit_->get_palette_manager()->active_palette());
@ -1071,7 +1065,7 @@ void editor_controller::show_menu(const std::vector<config>& items_arg, int xloc
active_menu_ = editor::UNIT_FACING;
auto pos = items.erase(items.begin());
int dir = 0;
std::generate_n(std::inserter<std::vector<config>>(items, pos), static_cast<int>(map_location::NDIRECTIONS), [&dir]() -> config {
std::generate_n(std::inserter<std::vector<config>>(items, pos), int(map_location::NDIRECTIONS), [&dir]() -> config {
return config {"label", map_location::write_translated_direction(map_location::DIRECTION(dir++))};
});
}
@ -1107,14 +1101,11 @@ void editor_controller::preferences()
{
gui_->video().clear_all_help_strings();
gui2::dialogs::preferences_dialog::display(game_config_);
gui_->redraw_everything();
}
void editor_controller::toggle_grid()
{
preferences::set_grid(!preferences::grid());
gui_->invalidate_all();
}
void editor_controller::unit_description()
@ -1238,7 +1229,6 @@ void editor_controller::refresh_image_cache()
void editor_controller::display_redraw_callback(display&)
{
set_button_state();
toolkit_->adjust_size();
toolkit_->get_palette_manager()->draw_contents();
get_current_map_context().get_labels().recalculate_labels();
@ -1314,7 +1304,6 @@ bool editor_controller::left_click(int x, int y, const bool browse)
LOG_ED << "Left click action " << hex_clicked << "\n";
editor_action* a = get_mouse_action().click_left(*gui_, x, y);
perform_refresh_delete(a, true);
if (a) set_button_state();
return false;
}
@ -1329,7 +1318,6 @@ void editor_controller::left_mouse_up(int x, int y, const bool /*browse*/)
{
editor_action* a = get_mouse_action().up_left(*gui_, x, y);
perform_delete(a);
if (a) set_button_state();
toolkit_->set_mouseover_overlay();
context_manager_->refresh_after_action();
}
@ -1344,7 +1332,6 @@ bool editor_controller::right_click(int x, int y, const bool browse)
LOG_ED << "Right click action " << hex_clicked << "\n";
editor_action* a = get_mouse_action().click_right(*gui_, x, y);
perform_refresh_delete(a, true);
if (a) set_button_state();
return false;
}
@ -1361,7 +1348,6 @@ void editor_controller::right_mouse_up(int x, int y, const bool browse)
editor_action* a = get_mouse_action().up_right(*gui_, x, y);
perform_delete(a);
if (a) set_button_state();
toolkit_->set_mouseover_overlay();
context_manager_->refresh_after_action();
}

View file

@ -22,7 +22,6 @@
#include "editor/toolkit/editor_toolkit.hpp"
#include "controller_base.hpp"
#include "help/help.hpp"
#include "hotkey/command_executor.hpp"
#include "mouse_handler_base.hpp"
#include "tooltips.hpp"
@ -251,8 +250,6 @@ class editor_controller : public controller_base,
tooltips::manager tooltip_manager_;
std::unique_ptr<font::floating_label_context> floating_label_manager_;
std::unique_ptr<help::help_manager> help_manager_;
/** Quit main loop flag */
bool do_quit_;
EXIT_STATUS quit_mode_;

View file

@ -528,7 +528,7 @@ config map_context::to_config()
side["user_team_name"].write_if_not_empty(t->user_team_name());
// TODO
// side["allow_player"] = "yes";
// side["allow_player"] = true;
side["fog"] = t->uses_fog();
side["shroud"] = t->uses_shroud();

View file

@ -1,146 +0,0 @@
/*
Copyright (C) 2006 - 2018 by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
wesnoth playturn Copyright (C) 2003 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "floating_textbox.hpp"
#include "display_chat_manager.hpp"
#include "floating_label.hpp"
#include "font/standard_colors.hpp"
#include "game_display.hpp"
#include "preferences/game.hpp"
#include "log.hpp"
#include <ctime>
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
namespace gui{
floating_textbox::floating_textbox() :
box_(nullptr),
check_(nullptr),
mode_(TEXTBOX_NONE),
label_string_(),
label_(0)
{}
void floating_textbox::close(game_display& gui)
{
if(!active()) {
return;
}
if(check_ != nullptr) {
if(mode_ == TEXTBOX_MESSAGE) {
preferences::set_message_private(check_->checked());
}
}
box_.reset(nullptr);
check_.reset(nullptr);
font::remove_floating_label(label_);
mode_ = TEXTBOX_NONE;
gui.invalidate_all();
}
void floating_textbox::update_location(game_display& gui)
{
if (box_ == nullptr)
return;
const SDL_Rect& area = gui.map_outside_area();
const int border_size = 10;
const int ypos = area.y+area.h-30 - (check_ != nullptr ? check_->height() + border_size : 0);
if (label_ != 0)
font::remove_floating_label(label_);
font::floating_label flabel(label_string_);
flabel.set_color(font::YELLOW_COLOR);
flabel.set_position(area.x + border_size, ypos);
flabel.set_alignment(font::LEFT_ALIGN);
flabel.set_clip_rect(area);
label_ = font::add_floating_label(flabel);
if (label_ == 0)
return;
const SDL_Rect& label_area = font::get_floating_label_rect(label_);
const int textbox_width = area.w - label_area.w - border_size*3;
if(textbox_width <= 0) {
font::remove_floating_label(label_);
return;
}
if(box_ != nullptr) {
box_->set_volatile(true);
const SDL_Rect rect {
area.x + label_area.w + border_size * 2
, ypos
, textbox_width
, box_->height()
};
box_->set_location(rect);
}
if(check_ != nullptr) {
check_->set_volatile(true);
check_->set_location(box_->location().x,box_->location().y + box_->location().h + border_size);
}
}
void floating_textbox::show(gui::TEXTBOX_MODE mode, const std::string& label,
const std::string& check_label, bool checked, game_display& gui)
{
close(gui);
label_string_ = label;
mode_ = mode;
if(!check_label.empty()) {
check_.reset(new gui::button(gui.video(),check_label,gui::button::TYPE_CHECK));
check_->set_check(checked);
}
box_.reset(new gui::textbox(gui.video(),100,"",true,256,font::SIZE_PLUS,0.8,0.6));
update_location(gui);
}
void floating_textbox::tab(const std::set<std::string>& dictionary)
{
if(active() == false) {
return;
}
std::string text = box_->text();
std::vector<std::string> matches(dictionary.begin(), dictionary.end());
const bool line_start = utils::word_completion(text, matches);
if (matches.empty()) return;
if (matches.size() == 1 && mode_ == gui::TEXTBOX_MESSAGE) {
text.append(line_start ? ": " : " ");
} else if (matches.size() > 1) {
std::string completion_list = utils::join(matches, " ");
game_display::get_singleton()->get_chat_manager().add_chat_message(time(nullptr), "", 0, completion_list,
events::chat_handler::MESSAGE_PRIVATE, false);
}
box_->set_text(text);
}
}

View file

@ -1,57 +0,0 @@
/*
Copyright (C) 2006 - 2018 by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
wesnoth playturn Copyright (C) 2003 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
// Scoped_resource can't use a pointer to an incomplete pointer with MSVC.
#include "widgets/textbox.hpp"
#include <memory>
#include <set>
class game_display;
namespace gui{
class button;
enum TEXTBOX_MODE { TEXTBOX_NONE, TEXTBOX_SEARCH, TEXTBOX_MESSAGE,
TEXTBOX_COMMAND, TEXTBOX_AI };
class floating_textbox{
public:
floating_textbox();
TEXTBOX_MODE mode() const { return mode_; }
const std::unique_ptr<gui::button>& check() const { return check_; }
const std::unique_ptr<gui::textbox>& box() const { return box_; }
void close(game_display& gui);
void update_location(game_display& gui);
void show(gui::TEXTBOX_MODE mode, const std::string& label,
const std::string& check_label, bool checked, game_display& gui);
void tab(const std::set<std::string>& dictionary);
bool active() const { return box_.get() != nullptr; }
private:
std::unique_ptr<gui::textbox> box_;
std::unique_ptr<gui::button> check_;
TEXTBOX_MODE mode_;
std::string label_string_;
int label_;
};
}

View file

@ -16,92 +16,211 @@
#include "gui/dialogs/help_browser.hpp"
#include "game_config_manager.hpp"
#include "font/pango/escape.hpp"
#include "font/pango/hyperlink.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/button.hpp"
#include "gui/widgets/image.hpp"
#include "gui/widgets/multi_page.hpp"
#include "gui/widgets/scroll_label.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/settings.hpp"
#include "gui/widgets/text_box.hpp"
#include "gui/widgets/tree_view.hpp"
#include "gui/widgets/tree_view_node.hpp"
#include "gui/widgets/window.hpp"
#ifdef GUI2_EXPERIMENTAL_LISTBOX
#include "gui/widgets/list.hpp"
#else
#include "gui/widgets/listbox.hpp"
#endif
#include "help/help.hpp"
#include "help/section.hpp"
#include "help/topic.hpp"
#include "help/utils.hpp"
namespace gui2
{
namespace dialogs
{
REGISTER_DIALOG(help_browser)
help_browser::help_browser()
: initial_topic_("introduction")
, help_cfg_(game_config_manager::get()->game_config().child("help"))
help_browser::help_browser(const help::section& toplevel, const std::string& initial)
: initial_topic_(initial)
, toplevel_(toplevel)
{
}
void help_browser::pre_show(window& window)
{
tree_view& topic_tree = find_widget<tree_view>(&window, "topic_tree", false);
multi_page& topic_pages = find_widget<multi_page>(&window, "topic_text_pages", false);
button& back_button = find_widget<button>(&window, "back", false);
button& next_button = find_widget<button>(&window, "next", false);
next_button.set_visible(widget::visibility::hidden);
back_button.set_visible(widget::visibility::hidden);
connect_signal_mouse_left_click(back_button,
std::bind(&help_browser::on_history_navigate, this, std::ref(window), true));
connect_signal_mouse_left_click(next_button,
std::bind(&help_browser::on_history_navigate, this, std::ref(window), false));
topic_tree.set_selection_change_callback(std::bind(&help_browser::on_topic_select, this, std::ref(window)));
window.keyboard_capture(&topic_tree);
unsigned id = 0;
add_topics_for_section(toplevel_, topic_tree.get_root_node());
for(const auto& topic : help_cfg_.child_range("topic")) {
std::map<std::string, string_map> data;
string_map item;
item["label"] = topic["title"];
data.emplace("topic_name", item);
topic_tree.add_node("topic", data).set_id(std::to_string(id));
// FIXME: maybe using a multi page isn't a good idea here... :| it causes massive lag when opening.
item.clear();
data.clear();
item["label"] = topic["text"].empty() ? "" : topic["text"].str();
data.emplace("topic_text", item);
topic_pages.add_page(data);
++id;
}
tree_view_node& initial_node = find_widget<tree_view_node>(&topic_tree, initial_topic_, false);
initial_node.select_node(true);
on_topic_select(window);
}
void help_browser::add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node)
{
for(const auto& section : parent_section.sections()) {
tree_view_node& section_node = add_topic(section->id, section->title, true, parent_node);
add_topics_for_section(*section, section_node);
}
for(const help::topic& topic : parent_section.topics()) {
// TODO: we do actually need to generate nodes for hidden topics somewhere
// so they can be displayed...
if(topic.id.compare(0, 2, "..") != 0 && help::is_visible_id(topic.id)) {
add_topic(topic.id, topic.title, false, parent_node);
}
}
}
tree_view_node& help_browser::add_topic(
const std::string& topic_id, const std::string& topic_title, bool expands, tree_view_node& parent)
{
std::map<std::string, string_map> data;
string_map item;
item["label"] = topic_title;
data.emplace("topic_name", item);
tree_view_node& new_node = parent.add_child(expands ? "section" : "topic", data);
new_node.set_id(std::string(expands ? "+" : "-") + topic_id);
return new_node;
}
static std::string format_help_text(const config& cfg)
{
std::stringstream ss;
for(auto& item : cfg.all_children_range()) {
if(item.key == "text") {
//ss << font::escape_text(item.cfg["text"]);
ss << item.cfg["text"];
} else if(item.key == "ref") {
if(item.cfg["dst"].empty()) {
std::stringstream msg;
msg << "Ref markup must have dst attribute. Please submit a bug"
" report if you have not modified the game files yourself. Erroneous config: "
<< cfg;
throw help::parse_error(msg.str());
};
// TODO: Get the proper link shade from somewhere
ss << font::format_as_link(font::escape_text(item.cfg["text"]), color_t::from_hex_string("ffe100"));
} else if(item.key == "img") {
if(item.cfg["src"].empty()) {
throw help::parse_error("Img markup must have src attribute.");
}
// For now, just output a placeholder so we know an image is supposed to be there.
ss << "[img]" << font::escape_text(item.cfg["src"]) << "[/img]";
} else if(item.key == "jump") {
// This appears to be something akin to tab stops.
if(item.cfg["amount"].empty() && item.cfg["to"].empty()) {
throw help::parse_error("Jump markup must have either a to or an amount attribute.");
}
ss << '\t';
}
}
return ss.str();
}
void help_browser::on_topic_select(window& window)
{
tree_view& tree = find_widget<tree_view>(&window, "topic_tree", false);
multi_page& topic_pages = find_widget<multi_page>(&window, "topic_text_pages", false);
tree_view& topic_tree = find_widget<tree_view>(&window, "topic_tree", false);
if(tree.empty()) {
if(topic_tree.empty()) {
return;
}
assert(tree.selected_item());
tree_view_node* selected = topic_tree.selected_item();
assert(selected);
if(tree.selected_item()->id().empty()) {
std::string topic_id = selected->id();
if(topic_id.empty()) {
return;
}
const unsigned topic_i = lexical_cast<unsigned>(tree.selected_item()->id());
if(topic_id[0] == '+') {
topic_id.replace(topic_id.begin(), topic_id.begin() + 1, 2, '.');
} else {
topic_id.erase(topic_id.begin());
}
const help::section& sec = toplevel_;
auto iter = parsed_pages_.find(topic_id);
if(iter == parsed_pages_.end()) {
const help::topic* topic = sec.find_topic(topic_id);
if(topic == nullptr) {
return;
}
std::map<std::string, string_map> data;
string_map item;
item["label"] = format_help_text(topic->parsed_text());
data.emplace("topic_text", item);
item.clear();
item["label"] = topic->title;
data.emplace("topic_title", item);
parsed_pages_.emplace(topic_id, topic_pages.get_page_count());
topic_pages.add_page(data);
// TODO: refactor out
window.invalidate_layout();
}
if(!history_.empty()) {
history_.erase(std::next(history_pos_), history_.end());
}
history_.push_back(topic_id);
history_pos_ = std::prev(history_.end());
if(history_pos_ != history_.begin()) {
find_widget<button>(&window, "back", false).set_visible(widget::visibility::visible);
}
find_widget<button>(&window, "next", false).set_visible(widget::visibility::hidden);
const unsigned topic_i = parsed_pages_.at(topic_id);
topic_pages.select_page(topic_i);
}
void help_browser::on_history_navigate(window& window, bool backwards)
{
if(backwards) {
--history_pos_;
} else {
++history_pos_;
}
find_widget<button>(&window, "back", false).set_visible(history_pos_ == history_.begin()
? widget::visibility::hidden
: widget::visibility::visible);
find_widget<button>(&window, "next", false).set_visible(history_pos_ == std::prev(history_.end())
? widget::visibility::hidden
: widget::visibility::visible);
const unsigned topic_i = parsed_pages_.at(*history_pos_);
find_widget<multi_page>(&window, "topic_text_pages", false).select_page(topic_i);
}
} // namespace dialogs

View file

@ -15,9 +15,20 @@
#pragma once
#include "gui/dialogs/modal_dialog.hpp"
#include "help/help_impl.hpp"
#include <map>
#include <list>
class config;
namespace help {
class section;
}
namespace gui2
{
class tree_view_node;
namespace dialogs
{
@ -25,14 +36,18 @@ namespace dialogs
class help_browser : public modal_dialog
{
public:
help_browser();
help_browser(const help::section& toplevel, const std::string& initial = "");
DEFINE_SIMPLE_DISPLAY_WRAPPER(help_browser)
private:
std::string initial_topic_;
const help::section& toplevel_;
const config& help_cfg_;
std::map<std::string, int> parsed_pages_;
std::list<std::string> history_;
std::list<std::string>::const_iterator history_pos_;
/** Inherited from modal_dialog, implemented by REGISTER_DIALOG. */
virtual const std::string& window_id() const override;
@ -41,6 +56,11 @@ private:
virtual void pre_show(window& window) override;
void on_topic_select(window& window);
void on_history_navigate(window& window, bool backwards);
void add_topics_for_section(const help::section& parent_section, tree_view_node& parent_node);
tree_view_node& add_topic(const std::string& topic_id, const std::string& topic_title,
bool expands, tree_view_node& parent);
};
} // namespace dialogs

View file

@ -26,8 +26,8 @@
#include "gui/core/timer.hpp"
#include "gui/dialogs/core_selection.hpp"
#include "gui/dialogs/debug_clock.hpp"
#include "gui/dialogs/game_version_dialog.hpp"
#include "gui/dialogs/help_browser.hpp"
#include "gui/dialogs/game_version_dialog.hpp"
#include "gui/dialogs/language_selection.hpp"
#include "gui/dialogs/lua_interpreter.hpp"
#include "gui/dialogs/message.hpp"
@ -283,11 +283,6 @@ void title_screen::pre_show(window& win)
// Help
//
register_button(win, "help", hotkey::HOTKEY_HELP, []() {
if(gui2::new_widgets) {
gui2::dialogs::help_browser::display();
}
help::help_manager help_manager(&game_config_manager::get()->game_config());
help::show_help();
});
@ -338,9 +333,6 @@ void title_screen::pre_show(window& win)
// Addons
//
register_button(win, "addons", hotkey::TITLE_SCREEN__ADDONS, []() {
// NOTE: we need the help_manager to get access to the Add-ons section in the game help!
help::help_manager help_manager(&game_config_manager::get()->game_config());
if(manage_addons()) {
game_config_manager::get()->reload_changed_game_config();
}

View file

@ -33,7 +33,7 @@
#include "preferences/game.hpp"
#include "gettext.hpp"
#include "help/help.hpp"
#include "help/help_impl.hpp"
#include "help/utils.hpp"
#include "play_controller.hpp"
#include "resources.hpp"
#include "team.hpp"

36
src/help/constants.cpp Normal file
View file

@ -0,0 +1,36 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/constants.hpp"
namespace help
{
const unsigned max_section_recursion_level = 15;
const unsigned max_history = 100;
const std::string default_topic = "..introduction";
const std::string unknown_unit_topic = ".unknown_unit";
const std::string ability_prefix = "ability_";
const std::string era_prefix = "era_";
const std::string faction_prefix = "faction_";
const std::string race_prefix = "race_";
const std::string terrain_prefix = "terrain_";
const std::string tod_prefix = "time_of_day_";
const std::string trait_prefix = "traits_";
const std::string unit_prefix = "unit_";
const std::string variation_prefix = "variation_";
const std::string weapon_special_prefix = "weaponspecial_";
} // namespace help

49
src/help/constants.hpp Normal file
View file

@ -0,0 +1,49 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <string>
namespace help
{
extern const unsigned max_section_recursion_level;
extern const unsigned max_history;
//
// Constant topics IDs
//
/** Topic to open by default when opening the help browser. */
extern const std::string default_topic;
/** Topic to show when a unit hasn't been encountered in-game yet */
extern const std::string unknown_unit_topic;
//
// Standard topic ID prefixes
//
extern const std::string ability_prefix;
extern const std::string era_prefix;
extern const std::string faction_prefix;
extern const std::string race_prefix;
extern const std::string terrain_prefix;
extern const std::string tod_prefix;
extern const std::string trait_prefix;
extern const std::string unit_prefix;
extern const std::string variation_prefix;
extern const std::string weapon_special_prefix;
} // namespace help

View file

@ -17,228 +17,83 @@
* Routines for showing the help-dialog.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/help.hpp"
#include "config.hpp" // for config, etc
#include "events.hpp" // for raise_draw_event, pump, etc
#include "font/constants.hpp" // for relative_size
#include "preferences/game.hpp"
#include "gettext.hpp" // for _
#include "gui/dialogs/transient_message.hpp"
#include "help/help_browser.hpp" // for help_browser
#include "help/help_impl.hpp" // for hidden_symbol, toplevel, etc
#include "key.hpp" // for CKey
#include "log.hpp" // for LOG_STREAM, log_domain
#include "sdl/surface.hpp" // for surface
#include "show_dialog.hpp" // for dialog_frame, etc
#include "terrain/terrain.hpp" // for terrain_type
#include "units/unit.hpp" // for unit
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "video.hpp" // for CVideo, resize_lock
#include "widgets/button.hpp" // for button
#include "help/constants.hpp"
#include "help/manager.hpp"
#include "help/utils.hpp"
#include "terrain/terrain.hpp"
#include "units/types.hpp"
#include "units/unit.hpp"
#include <cassert> // for assert
#include <algorithm> // for min
#include <ostream> // for basic_ostream, operator<<, etc
#include <vector> // for vector, vector<>::iterator
#include <SDL.h>
#include <cassert>
static lg::log_domain log_display("display");
#define WRN_DP LOG_STREAM(warn, log_display)
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help {
void show_unit_description(const unit &u)
namespace help
{
help::show_unit_description(u.type());
/** The help manager. What else would it be? */
static help_manager manager;
void show_help(const std::string& show_topic)
{
manager.open_help_browser_to(show_topic);
}
void show_terrain_description(const terrain_type &t)
void reset()
{
help::show_terrain_help(t.id(), t.hide_in_editor() || t.is_combined());
manager.reset_contents();
}
void show_unit_description(const unit_type &t)
void show_unit_description(const unit& u)
{
show_unit_description(u.type());
}
void show_unit_description(const unit_type& t)
{
std::string var_id = t.get_cfg()["variation_id"].str();
if (var_id.empty())
if(var_id.empty()) {
var_id = t.get_cfg()["variation_name"].str();
}
bool hide_help = t.hide_help();
bool use_variation = false;
if (!var_id.empty()) {
const unit_type *parent = unit_types.find(t.id());
if(!var_id.empty()) {
const unit_type* parent = unit_types.find(t.id());
assert(parent);
if (hide_help) {
if(hide_help) {
hide_help = parent->hide_help();
} else {
use_variation = true;
}
}
if (use_variation)
help::show_variation_help(t.id(), var_id, hide_help);
else
help::show_unit_help(t.id(), t.show_variations_in_help(), hide_help);
}
extern config dummy_cfg;
help_manager::help_manager(const config *cfg) //, gamemap *_map)
{
game_cfg = cfg == nullptr ? &dummy_cfg : cfg;
// map = _map;
}
help_manager::~help_manager()
{
game_cfg = nullptr;
// map = nullptr;
default_toplevel.clear();
hidden_sections.clear();
// These last numbers must be reset so that the content is regenerated.
// Upon next start.
last_num_encountered_units = -1;
last_num_encountered_terrains = -1;
}
/**
* Open the help browser, show topic with id show_topic.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_help(const std::string& show_topic, int xloc, int yloc)
{
show_help(default_toplevel, show_topic, xloc, yloc);
}
/**
* Open the help browser, show unit with id unit_id.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_unit_help(const std::string& show_topic, bool has_variations, bool hidden, int xloc, int yloc)
{
show_help(default_toplevel,
hidden_symbol(hidden) + (has_variations ? ".." : "") + unit_prefix + show_topic, xloc, yloc);
}
/**
* Open the help browser, show terrain with id terrain_id.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_terrain_help(const std::string& show_topic, bool hidden, int xloc, int yloc)
{
show_help(default_toplevel, hidden_symbol(hidden) + terrain_prefix + show_topic, xloc, yloc);
}
/**
* Open the help browser, show the variation of the unit matching.
*/
void show_variation_help(const std::string& unit, const std::string &variation, bool hidden, int xloc, int yloc)
{
show_help(default_toplevel, hidden_symbol(hidden) + variation_prefix + unit + "_" + variation, xloc, yloc);
}
/**
* Open a help dialog using a toplevel other than the default.
*
* This allows for complete customization of the contents, although not in a
* very easy way.
*/
void show_help(const section &toplevel_sec,
const std::string& show_topic,
int xloc, int yloc)
{
CVideo& video = CVideo::get_singleton();
const events::event_context dialog_events_context;
const gui::dialog_manager manager;
SDL_Rect screen_area = video.screen_area();
const int width = std::min<int>(font::relative_size(1200), screen_area.w - font::relative_size(20));
const int height = std::min<int>(font::relative_size(850), screen_area.h - font::relative_size(150));
const int left_padding = font::relative_size(10);
const int right_padding = font::relative_size(10);
const int top_padding = font::relative_size(10);
const int bot_padding = font::relative_size(10);
// If not both locations were supplied, put the dialog in the middle
// of the screen.
if (yloc <= -1 || xloc <= -1) {
xloc = screen_area.w / 2 - width / 2;
yloc = screen_area.h / 2 - height / 2;
if(use_variation) {
show_variation_help(t.id(), var_id, hide_help);
} else {
show_unit_help(t.id(), t.show_variations_in_help(), hide_help);
}
std::vector<gui::button*> buttons_ptr;
gui::button close_button_(video, _("Close"));
buttons_ptr.push_back(&close_button_);
}
gui::dialog_frame f(video, _("The Battle for Wesnoth Help"), gui::dialog_frame::default_style,
true, &buttons_ptr);
f.layout(xloc, yloc, width, height);
f.draw();
void show_unit_help(const std::string& show_topic, bool has_variations, bool hidden)
{
show_help(hidden_symbol(hidden) + (has_variations ? ".." : "") + unit_prefix + show_topic);
}
// Find all unit_types that have not been constructed yet and fill in the information
// needed to create the help topics
unit_types.build_all(unit_type::HELP_INDEXED);
void show_variation_help(const std::string& unit, const std::string& variation, bool hidden)
{
show_help(hidden_symbol(hidden) + variation_prefix + unit + "_" + variation);
}
if (preferences::encountered_units().size() != size_t(last_num_encountered_units) ||
preferences::encountered_terrains().size() != size_t(last_num_encountered_terrains) ||
last_debug_state != game_config::debug ||
last_num_encountered_units < 0) {
// More units or terrains encountered, update the contents.
last_num_encountered_units = preferences::encountered_units().size();
last_num_encountered_terrains = preferences::encountered_terrains().size();
last_debug_state = game_config::debug;
generate_contents();
}
try {
help_browser hb(video, toplevel_sec);
hb.set_location(xloc + left_padding, yloc + top_padding);
hb.set_width(width - left_padding - right_padding);
hb.set_height(height - top_padding - bot_padding);
if (!show_topic.empty()) {
hb.show_topic(show_topic);
}
else {
hb.show_topic(default_show_topic);
}
hb.set_dirty(true);
events::raise_draw_event();
CKey key;
for (;;) {
events::pump();
events::raise_process_event();
f.draw();
events::raise_draw_event();
if (key[SDLK_ESCAPE]) {
// Escape quits from the dialog.
return;
}
for (std::vector<gui::button*>::iterator button_it = buttons_ptr.begin();
button_it != buttons_ptr.end(); ++button_it) {
if ((*button_it)->pressed()) {
// There is only one button, close.
return;
}
}
video.flip();
CVideo::delay(10);
}
}
catch (parse_error& e) {
std::stringstream msg;
msg << _("Parse error when parsing help text: ") << "'" << e.message << "'";
gui2::show_transient_message("", msg.str());
}
void show_terrain_description(const terrain_type& t)
{
show_terrain_help(t.id(), t.hide_in_editor() || t.is_combined());
}
void show_terrain_help(const std::string& show_topic, bool hidden)
{
show_help(hidden_symbol(hidden) + terrain_prefix + show_topic);
}
} // End namespace help.

View file

@ -14,46 +14,45 @@
#pragma once
class config;
#include <string>
class terrain_type;
class unit;
class unit_type;
class CVideo;
#include <string>
namespace help {
namespace help
{
/** Flags the help manager's contents for regeneration. */
void reset();
struct help_manager {
help_manager(const config *game_config);
~help_manager();
};
/**
* Open the help browser, show topic with id show_topic.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_help(const std::string& show_topic = "");
struct section;
/// Open a help dialog using a toplevel other than the default. This
/// allows for complete customization of the contents, although not in a
/// very easy way.
void show_help(const section &toplevel, const std::string& show_topic="",
int xloc=-1, int yloc=-1);
/**
* Open the help browser, show unit with id unit_id.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_unit_help(const std::string& unit_id, bool has_variations = false, bool hidden = false);
/// Open the help browser. The help browser will have the topic with id
/// show_topic open if it is not the empty string. The default topic
/// will be shown if show_topic is the empty string.
void show_help(const std::string& show_topic="", int xloc=-1, int yloc=-1);
/**
* Open the help browser, show the variation of the unit matching.
*/
void show_variation_help(const std::string& unit_id, const std::string& variation, bool hidden = false);
/// wrapper to add unit prefix and hiding symbol
void show_unit_help(const std::string& unit_id, bool has_variations=false,
bool hidden = false, int xloc=-1, int yloc=-1);
/**
* Open the help browser, show terrain with id terrain_id.
*
* If show_topic is the empty string, the default topic will be shown.
*/
void show_terrain_help(const std::string& unit_id, bool hidden = false);
/// wrapper to add variation prefix and hiding symbol
void show_variation_help(const std::string &unit_id, const std::string &variation,
bool hidden = false, int xloc=-1, int yloc=-1);
/// wrapper to add terrain prefix and hiding symbol
void show_terrain_help(const std::string& unit_id, bool hidden = false,
int xloc = -1, int yloc = -1);
void show_unit_description(const unit_type &t);
void show_unit_description(const unit &u);
void show_unit_description(const unit_type& t);
void show_unit_description(const unit& u);
void show_terrain_description(const terrain_type& t);
} // End namespace help.

View file

@ -1,242 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/help_browser.hpp"
#include <iostream> // for operator<<, basic_ostream, etc
#include <SDL_mouse.h> // for SDL_GetMouseState, etc
#include "cursor.hpp" // for set, CURSOR_TYPE::HYPERLINK, etc
#include "font/constants.hpp" // for relative_size
#include "gettext.hpp" // for _
#include "gui/dialogs/transient_message.hpp"
#include "help/help_text_area.hpp" // for help_text_area
#include "help/help_impl.hpp" // for find_topic, hidden_symbol, etc
#include "key.hpp" // for CKey
#include "log.hpp" // for log_scope
#include "sdl/rect.hpp"
class CVideo;
struct SDL_Rect;
namespace help {
help_browser::help_browser(CVideo& video, const section &toplevel) :
gui::widget(video),
menu_(video,
toplevel),
text_area_(video, toplevel), toplevel_(toplevel),
ref_cursor_(false),
back_topics_(),
forward_topics_(),
back_button_(video, "", gui::button::TYPE_PRESS, "button_normal/button_small_H22", gui::button::DEFAULT_SPACE, true, "icons/arrows/long_arrow_ornate_left"),
forward_button_(video, "", gui::button::TYPE_PRESS, "button_normal/button_small_H22", gui::button::DEFAULT_SPACE, true, "icons/arrows/long_arrow_ornate_right"),
shown_topic_(nullptr)
{
// Hide the buttons at first since we do not have any forward or
// back topics at this point. They will be unhidden when history
// appears.
back_button_.hide(true);
forward_button_.hide(true);
// Set sizes to some default values.
set_measurements(font::relative_size(400), font::relative_size(500));
}
void help_browser::adjust_layout()
{
const int menu_buttons_padding = font::relative_size(10);
const int menu_y = location().y;
const int menu_x = location().x;
const int menu_w = 250;
const int menu_h = height();
const int menu_text_area_padding = font::relative_size(10);
const int text_area_y = location().y;
const int text_area_x = menu_x + menu_w + menu_text_area_padding;
const int text_area_w = width() - menu_w - menu_text_area_padding;
const int text_area_h = height();
const int button_border_padding = 0;
const int button_button_padding = font::relative_size(10);
const int back_button_x = location().x + button_border_padding;
const int back_button_y = menu_y + menu_h + menu_buttons_padding;
const int forward_button_x = back_button_x + back_button_.width() + button_button_padding;
const int forward_button_y = back_button_y;
menu_.set_width(menu_w);
menu_.set_location(menu_x, menu_y);
menu_.set_max_height(menu_h);
menu_.set_max_width(menu_w);
text_area_.set_location(text_area_x, text_area_y);
text_area_.set_width(text_area_w);
text_area_.set_height(text_area_h);
back_button_.set_location(back_button_x, back_button_y);
forward_button_.set_location(forward_button_x, forward_button_y);
set_dirty(true);
}
void help_browser::update_location(const SDL_Rect&)
{
adjust_layout();
}
void help_browser::process_event()
{
CKey key;
int mousex, mousey;
SDL_GetMouseState(&mousex,&mousey);
/// Fake focus functionality for the menu, only process it if it has focus.
if (sdl::point_in_rect(mousex, mousey, menu_.location())) {
menu_.process();
const topic *chosen_topic = menu_.chosen_topic();
if (chosen_topic != nullptr && chosen_topic != shown_topic_) {
/// A new topic has been chosen in the menu, display it.
show_topic(*chosen_topic);
}
}
if (back_button_.pressed()) {
move_in_history(back_topics_, forward_topics_);
}
if (forward_button_.pressed()) {
move_in_history(forward_topics_, back_topics_);
}
back_button_.hide(back_topics_.empty());
forward_button_.hide(forward_topics_.empty());
}
void help_browser::move_in_history(std::deque<const topic *> &from,
std::deque<const topic *> &to)
{
if (!from.empty()) {
const topic *to_show = from.back();
from.pop_back();
if (shown_topic_ != nullptr) {
if (to.size() > max_history) {
to.pop_front();
}
to.push_back(shown_topic_);
}
show_topic(*to_show, false);
}
}
void help_browser::handle_event(const SDL_Event &event)
{
gui::widget::handle_event(event);
SDL_MouseButtonEvent mouse_event = event.button;
if (event.type == SDL_MOUSEBUTTONDOWN) {
if (mouse_event.button == SDL_BUTTON_LEFT) {
// Did the user click a cross-reference?
const int mousex = mouse_event.x;
const int mousey = mouse_event.y;
const std::string ref = text_area_.ref_at(mousex, mousey);
if (!ref.empty()) {
const topic *t = find_topic(toplevel_, ref);
if (t == nullptr) {
//
// HACK: there are difficult-to-solve issues with a GUI2 popup over the
// GUI1 help browser (see issue #2587). Simply disabling it for now.
// Should be reenabled once the help browser switches to GUI2.
//
// -- vultraz, 2018-03-05
//
#if 0
std::stringstream msg;
msg << _("Reference to unknown topic: ") << "'" << ref << "'.";
gui2::show_transient_message("", msg.str());
#endif
update_cursor();
}
else {
show_topic(*t);
update_cursor();
}
}
}
else {
const bool mouse_back = !back_button_.hidden() && mouse_event.button == SDL_BUTTON_X1;
const bool mouse_forward = !forward_button_.hidden() && mouse_event.button == SDL_BUTTON_X2;
if (mouse_back) {
move_in_history(back_topics_, forward_topics_);
}
if (mouse_forward) {
move_in_history(forward_topics_, back_topics_);
}
if (mouse_back || mouse_forward) {
back_button_.hide(back_topics_.empty());
forward_button_.hide(forward_topics_.empty());
}
}
}
else if (event.type == SDL_MOUSEMOTION) {
update_cursor();
}
}
void help_browser::update_cursor()
{
int mousex, mousey;
SDL_GetMouseState(&mousex,&mousey);
const std::string ref = text_area_.ref_at(mousex, mousey);
if (!ref.empty() && !ref_cursor_) {
cursor::set(cursor::HYPERLINK);
ref_cursor_ = true;
}
else if (ref.empty() && ref_cursor_) {
cursor::set(cursor::NORMAL);
ref_cursor_ = false;
}
}
void help_browser::show_topic(const std::string &topic_id)
{
const topic *t = find_topic(toplevel_, topic_id);
if (t != nullptr) {
show_topic(*t);
} else if (topic_id.find(unit_prefix)==0 || topic_id.find(hidden_symbol() + unit_prefix)==0) {
show_topic(unknown_unit_topic);
} else {
std::cerr << "Help browser tried to show topic with id '" << topic_id
<< "' but that topic could not be found." << std::endl;
}
}
void help_browser::show_topic(const topic &t, bool save_in_history)
{
log_scope("show_topic");
if (save_in_history) {
forward_topics_.clear();
if (shown_topic_ != nullptr) {
if (back_topics_.size() > max_history) {
back_topics_.pop_front();
}
back_topics_.push_back(shown_topic_);
}
}
shown_topic_ = &t;
text_area_.show_topic(t);
menu_.select_topic(t);
update_cursor();
}
} // end namespace help

View file

@ -1,65 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <deque> // for deque
#include <string> // for string
#include <SDL_events.h> // for SDL_Event
#include "help_menu.hpp" // for help_menu
#include "help_text_area.hpp" // for help_text_area
#include "widgets/button.hpp" // for button
#include "widgets/widget.hpp" // for widget
class CVideo; // lines 18-18
struct SDL_Rect;
namespace help {
/// A help browser widget.
class help_browser : public gui::widget
{
public:
help_browser(CVideo& video, const section &toplevel);
void adjust_layout();
/// Display the topic with the specified identifier. Open the menu
/// on the right location and display the topic in the text area.
void show_topic(const std::string &topic_id);
protected:
virtual void update_location(const SDL_Rect& rect);
virtual void process_event();
virtual void handle_event(const SDL_Event &event);
private:
/// Update the current cursor, set it to the reference cursor if
/// mousex, mousey is over a cross-reference, otherwise, set it to
/// the normal cursor.
void update_cursor();
void show_topic(const topic &t, bool save_in_history=true);
/// Move in the topic history. Pop an element from from and insert
/// it in to. Pop at the fronts if the maximum number of elements is
/// exceeded.
void move_in_history(std::deque<const topic *> &from, std::deque<const topic *> &to);
help_menu menu_;
help_text_area text_area_;
const section &toplevel_;
bool ref_cursor_; // If the cursor currently is the hyperlink cursor.
std::deque<const topic *> back_topics_, forward_topics_;
gui::button back_button_, forward_button_;
topic const *shown_topic_;
};
} // end namespace help

File diff suppressed because it is too large Load diff

View file

@ -1,398 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/**
* Note:
* Prior to the creation of this file, all the code associated to the help
* browser existed in a single file src/help.cpp, including all of the
* widgets, topic generators, and implementation details. This totaled
* ~4000 lines of code.
*
* I have split it all up now, so that the gui aspects are separated from
* the content, the "front facing" part which the rest of the code base
* interacts with is in src/help/help.?pp, and the topic generators are
* separated. The remaining "guts" are here. It is implemented in a static
* singleton pattern, using "extern"'d variables, simply for ease of translation
* from the previous state. It would probably be a good idea to rewrite this
* guy as a proper C++ object. Feel free to do so, or to adopt some other
* design pattern.
*/
#pragma once
#include "exceptions.hpp" // for error
#include "font/sdl_ttf.hpp" // for line_width, relative_size
#include "gettext.hpp"
#include <cstring>
#include <list> // for list
#include <memory>
#include <ostream> // for operator<<, stringstream, etc
#include <string> // for string, allocator, etc
#include <utility> // for pair, make_pair
#include <vector> // for vector, etc
#include <SDL.h> // for SDL_Surface
#include <boost/logic/tribool.hpp>
class config;
class unit_type;
class terrain_type_data;
typedef std::shared_ptr<terrain_type_data> ter_data_cache;
namespace help { struct section; } // lines 51-51
namespace help {
/// Generate the help contents from the configurations given to the
/// manager.
void generate_contents();
typedef std::vector<section *> section_list;
/// Generate a topic text on the fly.
class topic_generator
{
unsigned count;
friend class topic_text;
public:
topic_generator(): count(1) {}
virtual std::string operator()() const = 0;
virtual ~topic_generator() {}
};
class text_topic_generator: public topic_generator {
std::string text_;
public:
text_topic_generator(const std::string& t): text_(t) {}
virtual std::string operator()() const { return text_; }
};
/// The text displayed in a topic. It is generated on the fly with the information
/// contained in generator_.
class topic_text
{
mutable std::vector< std::string > parsed_text_;
mutable topic_generator *generator_;
public:
~topic_text();
topic_text():
parsed_text_(),
generator_(nullptr)
{
}
topic_text(const std::string& t):
parsed_text_(),
generator_(new text_topic_generator(t))
{
}
explicit topic_text(topic_generator *g):
parsed_text_(),
generator_(g)
{
}
topic_text &operator=(topic_generator *g);
topic_text(const topic_text& t);
const std::vector<std::string>& parsed_text() const;
};
/// A topic contains a title, an id and some text.
struct topic
{
topic() :
title(),
id(),
text()
{
}
topic(const std::string &_title, const std::string &_id) :
title(_title),
id(_id),
text()
{
}
topic(const std::string &_title, const std::string &_id, const std::string &_text)
: title(_title), id(_id), text(_text) {}
topic(const std::string &_title, const std::string &_id, topic_generator *g)
: title(_title), id(_id), text(g) {}
/// Two topics are equal if their IDs are equal.
bool operator==(const topic &) const;
bool operator!=(const topic &t) const { return !operator==(t); }
/// Comparison on the ID.
bool operator<(const topic &) const;
std::string title, id;
mutable topic_text text;
};
typedef std::list<topic> topic_list;
/// A section contains topics and sections along with title and ID.
struct section {
section() :
title(""),
id(""),
topics(),
sections(),
level()
{
}
section(const section&);
section& operator=(const section&);
~section();
/// Two sections are equal if their IDs are equal.
bool operator==(const section &) const;
/// Comparison on the ID.
bool operator<(const section &) const;
/// Allocate memory for and add the section.
void add_section(const section &s);
void clear();
std::string title, id;
topic_list topics;
section_list sections;
int level;
};
/// To be used as a function object to locate sections and topics
/// with a specified ID.
class has_id
{
public:
has_id(const std::string &id) : id_(id) {}
bool operator()(const topic &t) { return t.id == id_; }
bool operator()(const section &s) { return s.id == id_; }
bool operator()(const section *s) { return s != nullptr && s->id == id_; }
private:
const std::string id_;
};
/// To be used as a function object when sorting topic lists on the title.
class title_less
{
public:
bool operator()(const topic &t1, const topic &t2) {
return translation::compare(t1.title, t2.title) < 0; }
};
/// To be used as a function object when sorting section lists on the title.
class section_less
{
public:
bool operator()(const section* s1, const section* s2) {
return translation::compare(s1->title, s2->title) < 0; }
};
class string_less
{
public:
bool operator() (const std::string &s1, const std::string &s2) const {
return translation::compare(s1, s2) < 0;
}
};
struct delete_section
{
void operator()(section *s) { delete s; }
};
struct create_section
{
section *operator()(const section *s) { return new section(*s); }
section *operator()(const section &s) { return new section(s); }
};
/// Thrown when the help system fails to parse something.
struct parse_error : public game::error
{
parse_error(const std::string& msg) : game::error(msg) {}
};
// Generator stuff below. Maybe move to a separate file? This one is
// getting crowded. Dunno if much more is needed though so I'll wait and
// see.
/// Dispatch generators to their appropriate functions.
void generate_sections(const config *help_cfg, const std::string &generator, section &sec, int level);
std::vector<topic> generate_topics(const bool sort_topics,const std::string &generator);
std::string generate_topic_text(const std::string &generator, const config *help_cfg,
const section &sec, const std::vector<topic>& generated_topics);
std::string generate_contents_links(const std::string& section_name, config const *help_cfg);
std::string generate_contents_links(const section &sec, const std::vector<topic>& topics);
/// return a hyperlink with the unit's name and pointing to the unit page
/// return empty string if this unit is hidden. If not yet discovered add the (?) suffix
std::string make_unit_link(const std::string& type_id);
/// return a list of hyperlinks to unit's pages (ordered or not)
std::vector<std::string> make_unit_links_list(
const std::vector<std::string>& type_id_list, bool ordered = false);
void generate_races_sections(const config *help_cfg, section &sec, int level);
void generate_terrain_sections(const config* help_cfg, section &sec, int level);
std::vector<topic> generate_unit_topics(const bool, const std::string& race);
void generate_unit_sections(const config *help_cfg, section &sec, int level, const bool, const std::string& race);
enum UNIT_DESCRIPTION_TYPE {FULL_DESCRIPTION, NO_DESCRIPTION, NON_REVEALING_DESCRIPTION};
/// Return the type of description that should be shown for a unit of
/// the given kind. This method is intended to filter out information
/// about units that should not be shown, for example due to not being
/// encountered.
UNIT_DESCRIPTION_TYPE description_type(const unit_type &type);
std::vector<topic> generate_ability_topics(const bool);
std::vector<topic> generate_time_of_day_topics(const bool);
std::vector<topic> generate_weapon_special_topics(const bool);
void generate_era_sections(const config *help_cfg, section &sec, int level);
std::vector<topic> generate_faction_topics(const config &, const bool);
std::vector<topic> generate_era_topics(const bool, const std::string & era_id);
std::vector<topic> generate_trait_topics(const bool);
/// Parse a help config, return the top level section. Return an empty
/// section if cfg is nullptr.
section parse_config(const config *cfg);
/// Recursive function used by parse_config.
void parse_config_internal(const config *help_cfg, const config *section_cfg,
section &sec, int level=0);
/// Return true if the section with id section_id is referenced from
/// another section in the config, or the toplevel.
bool section_is_referenced(const std::string &section_id, const config &cfg);
/// Return true if the topic with id topic_id is referenced from
/// another section in the config, or the toplevel.
bool topic_is_referenced(const std::string &topic_id, const config &cfg);
/// Search for the topic with the specified identifier in the section
/// and its subsections. Return the found topic, or nullptr if none could
/// be found.
const topic *find_topic(const section &sec, const std::string &id);
/// Search for the section with the specified identifier in the section
/// and its subsections. Return the found section or nullptr if none could
/// be found.
const section *find_section(const section &sec, const std::string &id);
/// Parse a text string. Return a vector with the different parts of the
/// text. Each markup item is a separate part while the text between
/// markups are separate parts.
std::vector<std::string> parse_text(const std::string &text);
/// Convert the contents to wml attributes, surrounded within
/// [element_name]...[/element_name]. Return the resulting WML.
std::string convert_to_wml(const std::string &element_name, const std::string &contents);
/// Return the color the string represents. Return font::NORMAL_COLOR if
/// the string is empty or can't be matched against any other color.
color_t string_to_color(const std::string &s);
/// Make a best effort to word wrap s. All parts are less than width.
std::vector<std::string> split_in_width(const std::string &s, const int font_size, const unsigned width);
std::string remove_first_space(const std::string& text);
/// Prepend all chars with meaning inside attributes with a backslash.
std::string escape(const std::string &s);
/// Return the first word in s, not removing any spaces in the start of
/// it.
std::string get_first_word(const std::string &s);
/// Load the appropriate terrain types data to use
ter_data_cache load_terrain_types_data();
extern const config *game_cfg;
// The default toplevel.
extern help::section default_toplevel;
// All sections and topics not referenced from the default toplevel.
extern help::section hidden_sections;
extern int last_num_encountered_units;
extern int last_num_encountered_terrains;
extern boost::tribool last_debug_state;
extern std::vector<std::string> empty_string_vector;
extern const int max_section_level;
extern const int title_size;
extern const int title2_size;
extern const int box_width;
extern const int normal_font_size;
extern const unsigned max_history;
extern const std::string topic_img;
extern const std::string closed_section_img;
extern const std::string open_section_img;
// The topic to open by default when opening the help dialog.
extern const std::string default_show_topic;
extern const std::string unknown_unit_topic;
extern const std::string unit_prefix;
extern const std::string terrain_prefix;
extern const std::string race_prefix;
extern const std::string faction_prefix;
extern const std::string era_prefix;
extern const std::string variation_prefix;
extern const std::string ability_prefix;
// id starting with '.' are hidden
std::string hidden_symbol(bool hidden = true);
bool is_visible_id(const std::string &id);
/// Return true if the id is valid for user defined topics and
/// sections. Some IDs are special, such as toplevel and may not be
/// be defined in the config.
bool is_valid_id(const std::string &id);
// Helpers for making generation of topics easier.
inline std::string make_link(const std::string& text, const std::string& dst)
{
// some sorting done on list of links may rely on the fact that text is first
return "<ref>text='" + help::escape(text) + "' dst='" + help::escape(dst) + "'</ref>";
}
inline std::string jump_to(const unsigned pos)
{
std::stringstream ss;
ss << "<jump>to=" << pos << "</jump>";
return ss.str();
}
inline std::string jump(const unsigned amount)
{
std::stringstream ss;
ss << "<jump>amount=" << amount << "</jump>";
return ss.str();
}
inline std::string bold(const std::string &s)
{
std::stringstream ss;
ss << "<bold>text='" << help::escape(s) << "'</bold>";
return ss.str();
}
typedef std::vector<std::vector<std::pair<std::string, unsigned int >> > table_spec;
// Create a table using the table specs. Return markup with jumps
// that create a table. The table spec contains a vector with
// vectors with pairs. The pairs are the markup string that should
// be in a cell, and the width of that cell.
std::string generate_table(const table_spec &tab, const unsigned int spacing=font::relative_size(20));
// Return the width for the image with filename.
unsigned image_width(const std::string &filename);
void push_tab_pair(std::vector<std::pair<std::string, unsigned int>> &v, const std::string &s);
} // end namespace help

View file

@ -1,238 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/help_menu.hpp"
#include "game_config.hpp" // for menu_contract, menu_expand
#include "help/help_impl.hpp" // for section, topic, topic_list, etc
#include "sound.hpp" // for play_UI_sound
#include "wml_separators.hpp" // for IMG_TEXT_SEPARATOR, etc
#include <algorithm> // for find
#include <iostream> // for basic_ostream, operator<<, etc
#include <list> // for _List_const_iterator, etc
#include <utility> // for pair
#include <SDL.h>
class CVideo; // lines 56-56
namespace help {
help_menu::help_menu(CVideo &video, const section& toplevel, int max_height) :
gui::menu(video, empty_string_vector, true, max_height, -1, nullptr, &gui::menu::bluebg_style),
visible_items_(),
toplevel_(toplevel),
expanded_(),
restorer_(),
chosen_topic_(nullptr),
selected_item_(&toplevel, "")
{
silent_ = true; //silence the default menu sounds
update_visible_items(toplevel_);
display_visible_items();
if (!visible_items_.empty())
selected_item_ = visible_items_.front();
}
bool help_menu::expanded(const section &sec)
{
return expanded_.find(&sec) != expanded_.end();
}
void help_menu::expand(const section &sec)
{
if (sec.id != "toplevel" && expanded_.insert(&sec).second) {
sound::play_UI_sound(game_config::sounds::menu_expand);
}
}
void help_menu::contract(const section &sec)
{
if (expanded_.erase(&sec)) {
sound::play_UI_sound(game_config::sounds::menu_contract);
}
}
void help_menu::update_visible_items(const section &sec, unsigned level)
{
if (level == 0) {
// Clear if this is the top level, otherwise append items.
visible_items_.clear();
}
section_list::const_iterator sec_it;
for (sec_it = sec.sections.begin(); sec_it != sec.sections.end(); ++sec_it) {
if (is_visible_id((*sec_it)->id)) {
const std::string vis_string = get_string_to_show(*(*sec_it), level + 1);
visible_items_.emplace_back(*sec_it, vis_string);
if (expanded(*(*sec_it))) {
update_visible_items(*(*sec_it), level + 1);
}
}
}
topic_list::const_iterator topic_it;
for (topic_it = sec.topics.begin(); topic_it != sec.topics.end(); ++topic_it) {
if (is_visible_id(topic_it->id)) {
const std::string vis_string = get_string_to_show(*topic_it, level + 1);
visible_items_.emplace_back(&(*topic_it), vis_string);
}
}
}
std::string help_menu::indent_list(const std::string& icon, const unsigned level) {
std::stringstream to_show;
for (unsigned i = 1; i < level; ++i) {
to_show << " "; // Indent 4 spaces
}
to_show << IMG_TEXT_SEPARATOR << IMAGE_PREFIX << icon;
return to_show.str();
}
std::string help_menu::get_string_to_show(const section &sec, const unsigned level)
{
std::stringstream to_show;
to_show << indent_list(expanded(sec) ? open_section_img : closed_section_img, level)
<< IMG_TEXT_SEPARATOR << sec.title;
return to_show.str();
}
std::string help_menu::get_string_to_show(const topic &topic, const unsigned level)
{
std::stringstream to_show;
to_show << indent_list(topic_img, level)
<< IMG_TEXT_SEPARATOR << topic.title;
return to_show.str();
}
bool help_menu::select_topic_internal(const topic &t, const section &sec)
{
topic_list::const_iterator tit =
std::find(sec.topics.begin(), sec.topics.end(), t);
if (tit != sec.topics.end()) {
// topic starting with ".." are assumed as rooted in the parent section
// and so only expand the parent when selected
if (t.id.size()<2 || t.id[0] != '.' || t.id[1] != '.')
expand(sec);
return true;
}
section_list::const_iterator sit;
for (sit = sec.sections.begin(); sit != sec.sections.end(); ++sit) {
if (select_topic_internal(t, *(*sit))) {
expand(sec);
return true;
}
}
return false;
}
void help_menu::select_topic(const topic &t)
{
if (selected_item_ == t) {
// The requested topic is already selected.
return;
}
if (select_topic_internal(t, toplevel_)) {
update_visible_items(toplevel_);
for (std::vector<visible_item>::const_iterator it = visible_items_.begin();
it != visible_items_.end(); ++it) {
if (*it == t) {
selected_item_ = *it;
break;
}
}
display_visible_items();
}
}
int help_menu::process()
{
int res = menu::process();
int mousex, mousey;
SDL_GetMouseState(&mousex,&mousey);
if (!visible_items_.empty() &&
static_cast<std::size_t>(res) < visible_items_.size()) {
selected_item_ = visible_items_[res];
const section* sec = selected_item_.sec;
if (sec != nullptr) {
// Check how we click on the section
int x = mousex - menu::location().x;
const std::string icon_img = expanded(*sec) ? open_section_img : closed_section_img;
// we remove the right thickness (ne present between icon and text)
int text_start = style_->item_size(indent_list(icon_img, sec->level)).w - style_->get_thickness();
// NOTE: if you want to forbid click to the left of the icon
// also check x >= text_start-image_width(icon_img)
if (menu::double_clicked() || x < text_start) {
// Open or close a section if we double-click on it
// or do simple click on the icon.
expanded(*sec) ? contract(*sec) : expand(*sec);
update_visible_items(toplevel_);
display_visible_items();
} else if (x >= text_start){
// click on title open the topic associated to this section
chosen_topic_ = find_topic(default_toplevel, ".."+sec->id );
}
} else if (selected_item_.t != nullptr) {
/// Choose a topic if it is clicked.
chosen_topic_ = selected_item_.t;
}
}
return res;
}
const topic *help_menu::chosen_topic()
{
const topic *ret = chosen_topic_;
chosen_topic_ = nullptr;
return ret;
}
void help_menu::display_visible_items()
{
std::vector<std::string> menu_items;
for(std::vector<visible_item>::const_iterator items_it = visible_items_.begin(),
end = visible_items_.end(); items_it != end; ++items_it) {
std::string to_show = items_it->visible_string;
if (selected_item_ == *items_it)
to_show = std::string("*") + to_show;
menu_items.push_back(to_show);
}
set_items(menu_items, false, true);
}
help_menu::visible_item::visible_item(const section *_sec, const std::string &vis_string) :
t(nullptr), sec(_sec), visible_string(vis_string) {}
help_menu::visible_item::visible_item(const topic *_t, const std::string &vis_string) :
t(_t), sec(nullptr), visible_string(vis_string) {}
bool help_menu::visible_item::operator==(const section &_sec) const
{
return sec != nullptr && *sec == _sec;
}
bool help_menu::visible_item::operator==(const topic &_t) const
{
return t != nullptr && *t == _t;
}
bool help_menu::visible_item::operator==(const visible_item &vis_item) const
{
return t == vis_item.t && sec == vis_item.sec;
}
} // end namespace help

View file

@ -1,102 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <set> // for set
#include <string> // for string, basic_string
#include <vector> // for vector
#include "widgets/menu.hpp" // for menu
class CVideo;
struct surface_restorer;
namespace help { struct section; }
namespace help { struct topic; }
namespace help {
/// The menu to the left in the help browser, where topics can be
/// navigated through and chosen.
class help_menu : public gui::menu
{
public:
help_menu(CVideo &video, const section &toplevel, int max_height=-1);
int process();
/// Make the topic the currently selected one, and expand all
/// sections that need to be expanded to show it.
void select_topic(const topic &t);
/// If a topic has been chosen, return that topic, otherwise
/// nullptr. If one topic is returned, it will not be returned again,
/// if it is not re-chosen.
const topic *chosen_topic();
private:
/// Information about an item that is visible in the menu.
struct visible_item {
visible_item(const section *_sec, const std::string &visible_string);
visible_item(const topic *_t, const std::string &visible_string);
// Invariant, one if these should be nullptr. The constructors
// enforce it.
const topic *t;
const section *sec;
std::string visible_string;
bool operator==(const visible_item &vis_item) const;
bool operator==(const section &sec) const;
bool operator==(const topic &t) const;
};
/// Regenerate what items are visible by checking what sections are
/// expanded.
void update_visible_items(const section &top_level, unsigned starting_level=0);
/// Return true if the section is expanded.
bool expanded(const section &sec);
/// Mark a section as expanded. Do not update the visible items or
/// anything.
void expand(const section &sec);
/// Contract (close) a section. That is, mark it as not expanded,
/// visible items are not updated.
void contract(const section &sec);
/// Return the string to use as the prefix for the icon part of the
/// menu-string at the specified level.
std::string indent_list(const std::string &icon, const unsigned level);
/// Return the string to use as the menu-string for sections at the
/// specified level.
std::string get_string_to_show(const section &sec, const unsigned level);
/// Return the string to use as the menu-string for topics at the
/// specified level.
std::string get_string_to_show(const topic &topic, const unsigned level);
/// Draw the currently visible items.
void display_visible_items();
/// Internal recursive thingie. did_expand will be true if any
/// section was expanded, otherwise untouched.
bool select_topic_internal(const topic &t, const section &sec);
std::vector<visible_item> visible_items_;
const section &toplevel_;
std::set<const section*> expanded_;
surface_restorer restorer_;
topic const *chosen_topic_;
visible_item selected_item_;
};
} // end namespace help

View file

@ -1,588 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/help_text_area.hpp"
#include "config.hpp" // for config, etc
#include "game_config.hpp" // for debug
#include "help/help_impl.hpp" // for parse_error, box_width, etc
#include "picture.hpp" // for get_image
#include "log.hpp" // for LOG_STREAM, log_domain, etc
#include "preferences/general.hpp" // for font_scaled
#include "sdl/rect.hpp" // for draw_rectangle, etc
#include "serialization/parser.hpp" // for read, write
#include "video.hpp" // for CVideo
#include <algorithm> // for max, min, find_if
#include <ostream> // for operator<<, stringstream, etc
#include <vector> // for vector, etc
static lg::log_domain log_display("display");
#define WRN_DP LOG_STREAM(warn, log_display)
namespace help {
help_text_area::help_text_area(CVideo &video, const section &toplevel) :
gui::scrollarea(video),
items_(),
last_row_(),
toplevel_(toplevel),
shown_topic_(nullptr),
title_spacing_(16),
curr_loc_(0, 0),
min_row_height_(font::get_max_height(normal_font_size)),
curr_row_height_(min_row_height_),
contents_height_(0)
{
set_scroll_rate(40);
}
void help_text_area::set_inner_location(const SDL_Rect& rect)
{
bg_register(rect);
if (shown_topic_)
set_items();
}
void help_text_area::show_topic(const topic &t)
{
shown_topic_ = &t;
set_items();
set_dirty(true);
}
help_text_area::item::item(surface surface, int x, int y, const std::string& _text,
const std::string& reference_to, bool _floating,
bool _box, ALIGNMENT alignment) :
rect(),
surf(surface),
text(_text),
ref_to(reference_to),
floating(_floating), box(_box),
align(alignment)
{
rect.x = x;
rect.y = y;
rect.w = box ? surface->w + box_width * 2 : surface->w;
rect.h = box ? surface->h + box_width * 2 : surface->h;
}
help_text_area::item::item(surface surface, int x, int y, bool _floating,
bool _box, ALIGNMENT alignment) :
rect(),
surf(surface),
text(""),
ref_to(""),
floating(_floating),
box(_box), align(alignment)
{
rect.x = x;
rect.y = y;
rect.w = box ? surface->w + box_width * 2 : surface->w;
rect.h = box ? surface->h + box_width * 2 : surface->h;
}
void help_text_area::set_items()
{
last_row_.clear();
items_.clear();
curr_loc_.first = 0;
curr_loc_.second = 0;
curr_row_height_ = min_row_height_;
// Add the title item.
const std::string show_title =
font::make_text_ellipsis(shown_topic_->title, title_size, inner_location().w);
surface surf(font::get_rendered_text(show_title, title_size,
font::NORMAL_COLOR, TTF_STYLE_BOLD));
if (surf != nullptr) {
add_item(item(surf, 0, 0, show_title));
curr_loc_.second = title_spacing_;
contents_height_ = title_spacing_;
down_one_line();
}
// Parse and add the text.
const std::vector<std::string>& parsed_items = shown_topic_->text.parsed_text();
std::vector<std::string>::const_iterator it;
for (it = parsed_items.begin(); it != parsed_items.end(); ++it) {
if (!(*it).empty() && (*it)[0] == '[') {
// Should be parsed as WML.
try {
config cfg;
std::istringstream stream(*it);
read(cfg, stream);
#define TRY(name) do { \
if (config &child = cfg.child(#name)) \
handle_##name##_cfg(child); \
} while (0)
TRY(ref);
TRY(img);
TRY(bold);
TRY(italic);
TRY(header);
TRY(jump);
TRY(format);
#undef TRY
}
catch (config::error& e) {
std::stringstream msg;
msg << "Error when parsing help markup as WML: '" << e.message << "'";
throw parse_error(msg.str());
}
}
else {
add_text_item(*it);
}
}
down_one_line(); // End the last line.
int h = height();
set_position(0);
set_full_size(contents_height_);
set_shown_size(h);
}
void help_text_area::handle_ref_cfg(const config &cfg)
{
const std::string dst = cfg["dst"];
const std::string text = cfg["text"];
bool force = cfg["force"].to_bool();
if (dst.empty()) {
std::stringstream msg;
msg << "Ref markup must have dst attribute. Please submit a bug"
" report if you have not modified the game files yourself. Erroneous config: ";
write(msg, cfg);
throw parse_error(msg.str());
}
if (find_topic(toplevel_, dst) == nullptr && !force) {
// detect the broken link but quietly silence the hyperlink for normal user
add_text_item(text, game_config::debug ? dst : "", true);
// FIXME: workaround: if different campaigns define different
// terrains, some terrains available in one campaign will
// appear in the list of seen terrains, and be displayed in the
// help, even if the current campaign does not handle such
// terrains. This will lead to the unit page generator creating
// invalid references.
//
// Disabling this is a kludgey workaround until the
// encountered_terrains system is fixed
//
// -- Ayin apr 8 2005
#if 0
if (game_config::debug) {
std::stringstream msg;
msg << "Reference to non-existent topic '" << dst
<< "'. Please submit a bug report if you have not"
"modified the game files yourself. Erroneous config: ";
write(msg, cfg);
throw parse_error(msg.str());
}
#endif
} else {
add_text_item(text, dst);
}
}
void help_text_area::handle_img_cfg(const config &cfg)
{
const std::string src = cfg["src"];
const std::string align = cfg["align"];
bool floating = cfg["float"].to_bool();
bool box = cfg["box"].to_bool(true);
if (src.empty()) {
throw parse_error("Img markup must have src attribute.");
}
add_img_item(src, align, floating, box);
}
void help_text_area::handle_bold_cfg(const config &cfg)
{
const std::string text = cfg["text"];
if (text.empty()) {
throw parse_error("Bold markup must have text attribute.");
}
add_text_item(text, "", false, -1, true);
}
void help_text_area::handle_italic_cfg(const config &cfg)
{
const std::string text = cfg["text"];
if (text.empty()) {
throw parse_error("Italic markup must have text attribute.");
}
add_text_item(text, "", false, -1, false, true);
}
void help_text_area::handle_header_cfg(const config &cfg)
{
const std::string text = cfg["text"];
if (text.empty()) {
throw parse_error("Header markup must have text attribute.");
}
add_text_item(text, "", false, title2_size, true);
}
void help_text_area::handle_jump_cfg(const config &cfg)
{
const std::string amount_str = cfg["amount"];
const std::string to_str = cfg["to"];
if (amount_str.empty() && to_str.empty()) {
throw parse_error("Jump markup must have either a to or an amount attribute.");
}
unsigned jump_to = curr_loc_.first;
if (!amount_str.empty()) {
unsigned amount;
try {
amount = lexical_cast<unsigned, std::string>(amount_str);
}
catch (bad_lexical_cast&) {
throw parse_error("Invalid amount the amount attribute in jump markup.");
}
jump_to += amount;
}
if (!to_str.empty()) {
unsigned to;
try {
to = lexical_cast<unsigned, std::string>(to_str);
}
catch (bad_lexical_cast&) {
throw parse_error("Invalid amount in the to attribute in jump markup.");
}
if (to < jump_to) {
down_one_line();
}
jump_to = to;
}
if (jump_to != 0 && static_cast<int>(jump_to) <
get_max_x(curr_loc_.first, curr_row_height_)) {
curr_loc_.first = jump_to;
}
}
void help_text_area::handle_format_cfg(const config &cfg)
{
const std::string text = cfg["text"];
if (text.empty()) {
throw parse_error("Format markup must have text attribute.");
}
bool bold = cfg["bold"].to_bool();
bool italic = cfg["italic"].to_bool();
int font_size = cfg["font_size"].to_int(normal_font_size);
color_t color = help::string_to_color(cfg["color"]);
add_text_item(text, "", false, font_size, bold, italic, color);
}
void help_text_area::add_text_item(const std::string& text, const std::string& ref_dst,
bool broken_link, int _font_size, bool bold, bool italic,
color_t text_color
)
{
const int font_size = _font_size < 0 ? normal_font_size : _font_size;
// font::line_width(), font::get_rendered_text() are not use scaled font inside
const int scaled_font_size = preferences::font_scaled(font_size);
if (text.empty())
return;
const int remaining_width = get_remaining_width();
std::size_t first_word_start = text.find_first_not_of(" ");
if (first_word_start == std::string::npos) {
first_word_start = 0;
}
if (text[first_word_start] == '\n') {
down_one_line();
std::string rest_text = text;
rest_text.erase(0, first_word_start + 1);
add_text_item(rest_text, ref_dst, broken_link, _font_size, bold, italic, text_color);
return;
}
const std::string first_word = get_first_word(text);
int state = 0;
state |= bold ? TTF_STYLE_BOLD : 0;
state |= italic ? TTF_STYLE_ITALIC : 0;
if (curr_loc_.first != get_min_x(curr_loc_.second, curr_row_height_)
&& remaining_width < font::line_width(first_word, scaled_font_size, state)) {
// The first word does not fit, and we are not at the start of
// the line. Move down.
down_one_line();
std::string s = remove_first_space(text);
add_text_item(s, ref_dst, broken_link, _font_size, bold, italic, text_color);
}
else {
std::vector<std::string> parts = split_in_width(text, font_size, remaining_width);
std::string first_part = parts.front();
// Always override the color if we have a cross reference.
color_t color;
if(ref_dst.empty())
color = text_color;
else if(broken_link)
color = font::BAD_COLOR;
else
color = font::YELLOW_COLOR;
// In split_in_width(), no_break_after() and no_break_before() are used(see marked-up_text.cpp).
// Thus, even if there is enough remaining_width for the next word,
// sometimes empty string is returned from split_in_width().
if (first_part.empty()) {
down_one_line();
}
else {
surface surf(font::get_rendered_text(first_part, scaled_font_size, color, state));
if (!surf.null())
add_item(item(surf, curr_loc_.first, curr_loc_.second, first_part, ref_dst));
}
if (parts.size() > 1) {
std::string& s = parts.back();
const std::string first_word_before = get_first_word(s);
const std::string first_word_after = get_first_word(remove_first_space(s));
if (get_remaining_width() >= font::line_width(first_word_after, scaled_font_size, state)
&& get_remaining_width()
< font::line_width(first_word_before, scaled_font_size, state)) {
// If the removal of the space made this word fit, we
// must move down a line, otherwise it will be drawn
// without a space at the end of the line.
s = remove_first_space(s);
down_one_line();
}
else if (!(font::line_width(first_word_before, scaled_font_size, state)
< get_remaining_width())) {
s = remove_first_space(s);
}
add_text_item(s, ref_dst, broken_link, _font_size, bold, italic, text_color);
}
}
}
void help_text_area::add_img_item(const std::string& path, const std::string& alignment,
const bool floating, const bool box)
{
surface surf(image::get_image(path));
if (surf.null())
return;
ALIGNMENT align = str_to_align(alignment);
if (align == HERE && floating) {
WRN_DP << "Floating image with align HERE, aligning left." << std::endl;
align = LEFT;
}
const int width = surf->w + (box ? box_width * 2 : 0);
int xpos;
int ypos = curr_loc_.second;
int text_width = inner_location().w;
switch (align) {
case HERE:
xpos = curr_loc_.first;
break;
case LEFT:
default:
xpos = 0;
break;
case MIDDLE:
xpos = text_width / 2 - width / 2 - (box ? box_width : 0);
break;
case RIGHT:
xpos = text_width - width - (box ? box_width * 2 : 0);
break;
}
if (curr_loc_.first != get_min_x(curr_loc_.second, curr_row_height_)
&& (xpos < curr_loc_.first || xpos + width > text_width)) {
down_one_line();
add_img_item(path, alignment, floating, box);
}
else {
if (!floating) {
curr_loc_.first = xpos;
}
else {
ypos = get_y_for_floating_img(width, xpos, ypos);
}
add_item(item(surf, xpos, ypos, floating, box, align));
}
}
int help_text_area::get_y_for_floating_img(const int width, const int x, const int desired_y)
{
int min_y = desired_y;
for (std::list<item>::const_iterator it = items_.begin(); it != items_.end(); ++it) {
const item& itm = *it;
if (itm.floating) {
if ((itm.rect.x + itm.rect.w > x && itm.rect.x < x + width)
|| (itm.rect.x > x && itm.rect.x < x + width)) {
min_y = std::max<int>(min_y, itm.rect.y + itm.rect.h);
}
}
}
return min_y;
}
int help_text_area::get_min_x(const int y, const int height)
{
int min_x = 0;
for (std::list<item>::const_iterator it = items_.begin(); it != items_.end(); ++it) {
const item& itm = *it;
if (itm.floating) {
if (itm.rect.y < y + height && itm.rect.y + itm.rect.h > y && itm.align == LEFT) {
min_x = std::max<int>(min_x, itm.rect.w + 5);
}
}
}
return min_x;
}
int help_text_area::get_max_x(const int y, const int height)
{
int text_width = inner_location().w;
int max_x = text_width;
for (std::list<item>::const_iterator it = items_.begin(); it != items_.end(); ++it) {
const item& itm = *it;
if (itm.floating) {
if (itm.rect.y < y + height && itm.rect.y + itm.rect.h > y) {
if (itm.align == RIGHT) {
max_x = std::min<int>(max_x, text_width - itm.rect.w - 5);
} else if (itm.align == MIDDLE) {
max_x = std::min<int>(max_x, text_width / 2 - itm.rect.w / 2 - 5);
}
}
}
}
return max_x;
}
void help_text_area::add_item(const item &itm)
{
items_.push_back(itm);
if (!itm.floating) {
curr_loc_.first += itm.rect.w;
curr_row_height_ = std::max<int>(itm.rect.h, curr_row_height_);
contents_height_ = std::max<int>(contents_height_, curr_loc_.second + curr_row_height_);
last_row_.push_back(&items_.back());
}
else {
if (itm.align == LEFT) {
curr_loc_.first = itm.rect.w + 5;
}
contents_height_ = std::max<int>(contents_height_, itm.rect.y + itm.rect.h);
}
}
help_text_area::ALIGNMENT help_text_area::str_to_align(const std::string &cmp_str)
{
if (cmp_str == "left") {
return LEFT;
} else if (cmp_str == "middle") {
return MIDDLE;
} else if (cmp_str == "right") {
return RIGHT;
} else if (cmp_str == "here" || cmp_str.empty()) { // Make the empty string be "here" alignment.
return HERE;
}
std::stringstream msg;
msg << "Invalid alignment string: '" << cmp_str << "'";
throw parse_error(msg.str());
}
void help_text_area::down_one_line()
{
adjust_last_row();
last_row_.clear();
curr_loc_.second += curr_row_height_ + (curr_row_height_ == min_row_height_ ? 0 : 2);
curr_row_height_ = min_row_height_;
contents_height_ = std::max<int>(curr_loc_.second + curr_row_height_, contents_height_);
curr_loc_.first = get_min_x(curr_loc_.second, curr_row_height_);
}
void help_text_area::adjust_last_row()
{
for (std::list<item *>::iterator it = last_row_.begin(); it != last_row_.end(); ++it) {
item &itm = *(*it);
const int gap = curr_row_height_ - itm.rect.h;
itm.rect.y += gap / 2;
}
}
int help_text_area::get_remaining_width()
{
const int total_w = get_max_x(curr_loc_.second, curr_row_height_);
return total_w - curr_loc_.first;
}
void help_text_area::draw_contents()
{
const SDL_Rect& loc = inner_location();
bg_restore();
surface& screen = video().getSurface();
clip_rect_setter clip_rect_set(screen, &loc);
for(std::list<item>::const_iterator it = items_.begin(), end = items_.end(); it != end; ++it) {
SDL_Rect dst = it->rect;
dst.y -= get_position();
if (dst.y < static_cast<int>(loc.h) && dst.y + it->rect.h > 0) {
dst.x += loc.x;
dst.y += loc.y;
if (it->box) {
for (int i = 0; i < box_width; ++i) {
SDL_Rect draw_rect {
dst.x,
dst.y,
it->rect.w - i * 2,
it->rect.h - i * 2
};
sdl::draw_rectangle(draw_rect, {0, 0, 0, 0});
++dst.x;
++dst.y;
}
}
sdl_blit(it->surf, nullptr, screen, &dst);
}
}
}
void help_text_area::scroll(unsigned int)
{
// Nothing will be done on the actual scroll event. The scroll
// position is checked when drawing instead and things drawn
// accordingly.
set_dirty(true);
}
bool help_text_area::item_at::operator()(const item& item) const {
return sdl::point_in_rect(x_, y_, item.rect);
}
std::string help_text_area::ref_at(const int x, const int y)
{
const int local_x = x - location().x;
const int local_y = y - location().y;
if (local_y < height() && local_y > 0) {
const int cmp_y = local_y + get_position();
const std::list<item>::const_iterator it =
std::find_if(items_.begin(), items_.end(), item_at(local_x, cmp_y));
if (it != items_.end()) {
if (!(*it).ref_to.empty()) {
return ((*it).ref_to);
}
}
}
return "";
}
} // end namespace help

View file

@ -1,166 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <list> // for list
#include <string> // for string
#include <utility> // for pair
#include "font/standard_colors.hpp" // for NORMAL_COLOR
#include "sdl/surface.hpp" // for surface
#include "widgets/scrollarea.hpp" // for scrollarea
class CVideo;
class config;
namespace help { struct section; }
namespace help { struct topic; }
namespace help {
/// The area where the content is shown in the help browser.
class help_text_area : public gui::scrollarea
{
public:
help_text_area(CVideo &video, const section &toplevel);
/// Display the topic.
void show_topic(const topic &t);
/// Return the ID that is cross-referenced at the (screen)
/// coordinates x, y. If no cross-reference is there, return the
/// empty string.
std::string ref_at(const int x, const int y);
protected:
virtual void scroll(unsigned int pos);
virtual void set_inner_location(const SDL_Rect& rect);
private:
enum ALIGNMENT {LEFT, MIDDLE, RIGHT, HERE};
/// Convert a string to an alignment. Throw parse_error if
/// unsuccessful.
ALIGNMENT str_to_align(const std::string &s);
/// An item that is displayed in the text area. Contains the surface
/// that should be blitted along with some other information.
struct item {
item(surface surface, int x, int y, const std::string& text="",
const std::string& reference_to="", bool floating=false,
bool box=false, ALIGNMENT alignment=HERE);
item(surface surface, int x, int y,
bool floating, bool box=false, ALIGNMENT=HERE);
/// Relative coordinates of this item.
SDL_Rect rect;
surface surf;
// If this item contains text, this will contain that text.
std::string text;
// If this item contains a cross-reference, this is the id
// of the referenced topic.
std::string ref_to;
// If this item is floating, that is, if things should be filled
// around it.
bool floating;
bool box;
ALIGNMENT align;
};
/// Function object to find an item at the specified coordinates.
class item_at {
public:
item_at(const int x, const int y) : x_(x), y_(y) {}
bool operator()(const item&) const;
private:
const int x_, y_;
};
/// Update the vector with the items of the shown topic, creating
/// surfaces for everything and putting things where they belong.
void set_items();
// Create appropriate items from configs. Items will be added to the
// internal vector. These methods check that the necessary
// attributes are specified.
void handle_ref_cfg(const config &cfg);
void handle_img_cfg(const config &cfg);
void handle_bold_cfg(const config &cfg);
void handle_italic_cfg(const config &cfg);
void handle_header_cfg(const config &cfg);
void handle_jump_cfg(const config &cfg);
void handle_format_cfg(const config &cfg);
void draw_contents();
/// Add an item with text. If ref_dst is something else than the
/// empty string, the text item will be underlined to show that it
/// is a cross-reference. The item will also remember what the
/// reference points to. If font_size is below zero, the default
/// will be used.
void add_text_item(const std::string& text, const std::string& ref_dst="",
bool broken_link = false,
int font_size=-1, bool bold=false, bool italic=false,
color_t color=font::NORMAL_COLOR);
/// Add an image item with the specified attributes.
void add_img_item(const std::string& path, const std::string& alignment, const bool floating,
const bool box);
/// Move the current input point to the next line.
void down_one_line();
/// Adjust the heights of the items in the last row to make it look
/// good .
void adjust_last_row();
/// Return the width that remain on the line the current input point is at.
int get_remaining_width();
/// Return the least x coordinate at which something of the
/// specified height can be drawn at the specified y coordinate
/// without interfering with floating images.
int get_min_x(const int y, const int height=0);
/// Analogous with get_min_x but return the maximum X.
int get_max_x(const int y, const int height=0);
/// Find the lowest y coordinate where a floating img of the
/// specified width and at the specified x coordinate can be
/// placed. Start looking at desired_y and continue downwards. Only
/// check against other floating things, since text and inline
/// images only can be above this place if called correctly.
int get_y_for_floating_img(const int width, const int x, const int desired_y);
/// Add an item to the internal list, update the locations and row
/// height.
void add_item(const item& itm);
std::list<item> items_;
std::list<item *> last_row_;
const section &toplevel_;
topic const *shown_topic_;
const int title_spacing_;
// The current input location when creating items.
std::pair<int, int> curr_loc_;
const unsigned min_row_height_;
unsigned curr_row_height_;
/// The height of all items in total.
int contents_height_;
};
} // end namespace help

View file

@ -1,856 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/help_topic_generators.hpp"
#include "font/sdl_ttf.hpp" // for line_width
#include "game_config.hpp" // for debug, menu_contract, etc
#include "preferences/game.hpp" // for encountered_terrains, etc
#include "gettext.hpp" // for _, gettext, N_
#include "language.hpp" // for string_table, symbol_table
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "movetype.hpp" // for movetype, movetype::effects, etc
#include "units/race.hpp" // for unit_race, etc
#include "terrain/terrain.hpp" // for terrain_type
#include "terrain/translation.hpp" // for operator==, ter_list, etc
#include "terrain/type_data.hpp" // for terrain_type_data, etc
#include "tstring.hpp" // for t_string, operator<<
#include "units/helper.hpp" // for resistance_color
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "video.hpp" // fore current_resolution
#include <boost/optional.hpp> // for optional
#include <iostream> // for operator<<, basic_ostream, etc
#include <map> // for map, etc
#include <set>
#include <SDL.h>
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help {
struct terrain_movement_info
{
const t_string name;
const t_string id;
const int defense;
const int movement_cost;
const int vision_cost;
const int jamming_cost;
const bool defense_cap;
bool operator<(const terrain_movement_info& other) const
{
return translation::icompare(name, other.name) < 0;
}
};
static std::string best_str(bool best) {
std::string lang_policy = (best ? _("Best of") : _("Worst of"));
std::string color_policy = (best ? "green": "red");
return "<format>color='" + color_policy + "' text='" + lang_policy + "'</format>";
}
typedef t_translation::ter_list::const_iterator ter_iter;
// Gets an english description of a terrain ter_list alias behavior: "Best of cave, hills", "Worst of Swamp, Forest" etc.
static std::string print_behavior_description(ter_iter start, ter_iter end, const ter_data_cache & tdata, bool first_level = true, bool begin_best = true)
{
if (start == end) return "";
if (*start == t_translation::MINUS || *start == t_translation::PLUS) return print_behavior_description(start+1, end, tdata, first_level, *start == t_translation::PLUS); //absorb any leading mode changes by calling again, with a new default value begin_best.
boost::optional<ter_iter> last_change_pos;
bool best = begin_best;
for (ter_iter i = start; i != end; ++i) {
if ((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
best = !best;
last_change_pos = i;
}
}
std::stringstream ss;
if (!last_change_pos) {
std::vector<std::string> names;
for (ter_iter i = start; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i);
if (!tt.editor_name().empty())
names.push_back(tt.editor_name());
}
if (names.empty()) return "";
if (names.size() == 1) return names.at(0);
ss << best_str(best) << " ";
if (!first_level) ss << "( ";
ss << names.at(0);
for (std::size_t i = 1; i < names.size(); i++) {
ss << ", " << names.at(i);
}
if (!first_level) ss << " )";
} else {
std::vector<std::string> names;
for (ter_iter i = *last_change_pos+1; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i);
if (!tt.editor_name().empty())
names.push_back(tt.editor_name());
}
if (names.empty()) { //This alias list is apparently padded with junk at the end, so truncate it without adding more parens
return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
}
ss << best_str(best) << " ";
if (!first_level) ss << "( ";
ss << print_behavior_description(start, *last_change_pos-1, tdata, false, begin_best);
// Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
for (const std::string & s : names) {
ss << ", " << s;
}
if (!first_level) ss << " )";
}
return ss.str();
}
std::string terrain_topic_generator::operator()() const {
std::stringstream ss;
if (!type_.icon_image().empty())
ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id()
<< ")~BLIT("<< "terrain/" << type_.icon_image() << "_30.png)" << "'</img> ";
if (!type_.editor_image().empty())
ss << "<img>src='" << type_.editor_image() << "'</img> ";
if (!type_.help_topic_text().empty())
ss << "\n\n" << type_.help_topic_text().str() << "\n";
else
ss << "\n";
ter_data_cache tdata = load_terrain_types_data();
if (!tdata) {
WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data\n";
return ss.str();
}
if (!(type_.union_type().size() == 1 && type_.union_type()[0] == type_.number() && type_.is_nonnull())) {
const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
ss << "\n" << _("Base Terrain: ");
bool first = true;
for (const t_translation::terrain_code& underlying_terrain : underlying_mvt_terrains) {
const terrain_type& mvt_base = tdata->get_terrain_info(underlying_terrain);
if (mvt_base.editor_name().empty()) continue;
if (!first) {
ss << ", ";
} else {
first = false;
}
ss << make_link(mvt_base.editor_name(), ".." + terrain_prefix + mvt_base.id());
}
ss << "\n";
ss << "\n" << _("Movement properties: ");
ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
ss << "\n" << _("Defense properties: ");
ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
}
if (game_config::debug) {
ss << "\n";
ss << "ID: " << type_.id() << "\n";
ss << "Village: " << (type_.is_village() ? "Yes" : "No") << "\n";
ss << "Gives Healing: " << type_.gives_healing() << "\n";
ss << "Keep: " << (type_.is_keep() ? "Yes" : "No") << "\n";
ss << "Castle: " << (type_.is_castle() ? "Yes" : "No") << "\n";
ss << "Overlay: " << (type_.is_overlay() ? "Yes" : "No") << "\n";
ss << "Combined: " << (type_.is_combined() ? "Yes" : "No") << "\n";
ss << "Nonnull: " << (type_.is_nonnull() ? "Yes" : "No") << "\n";
ss << "Terrain string:" << type_.number() << "\n";
ss << "Hide in Editor: " << (type_.hide_in_editor() ? "Yes" : "No") << "\n";
ss << "Editor Group: " << type_.editor_group() << "\n";
ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
ss << type_.income_description();
if (type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
ss << "\nEditor Image: Empty\n";
} else {
ss << "\nEditor Image: " << type_.editor_image() << "\n";
}
const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
ss << "\nDebug Mvt Description String:";
for (const t_translation::terrain_code & t : underlying_mvt_terrains) {
ss << " " << t;
}
const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
ss << "\nDebug Def Description String:";
for (const t_translation::terrain_code & t : underlying_def_terrains) {
ss << " " << t;
}
}
return ss.str();
}
//Typedef to help with formatting list of traits
// Maps localized trait name to trait help topic ID
typedef std::pair<std::string, std::string> trait_data;
//Helper function for printing a list of trait data
static void print_trait_list(std::stringstream & ss, const std::vector<trait_data> & l)
{
std::size_t i = 0;
ss << make_link(l[i].first, l[i].second);
// This doesn't skip traits with empty names
for(i++; i < l.size(); i++) {
ss << ", " << make_link(l[i].first,l[i].second);
}
}
std::string unit_topic_generator::operator()() const {
// Force the lazy loading to build this unit.
unit_types.build_unit_type(type_, unit_type::FULL);
std::stringstream ss;
std::string clear_stringstream;
const std::string detailed_description = type_.unit_description();
const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE);
const unit_type& male_type = type_.get_gender_unit_type(unit_race::MALE);
const int screen_width = CVideo::get_singleton().get_width();
ss << _("Level") << " " << type_.level();
ss << "\n\n";
ss << "<img>src='" << male_type.image();
ss << "~RC(" << male_type.flag_rgb() << ">red)";
if (screen_width >= 1200) ss << "~XBRZ(2)";
ss << "' box='no'</img> ";
if (&female_type != &male_type) {
ss << "<img>src='" << female_type.image();
ss << "~RC(" << female_type.flag_rgb() << ">red)";
if (screen_width >= 1200) ss << "~XBRZ(2)";
ss << "' box='no'</img> ";
}
const std::string &male_portrait = male_type.small_profile().empty() ?
male_type.big_profile() : male_type.small_profile();
const std::string &female_portrait = female_type.small_profile().empty() ?
female_type.big_profile() : female_type.small_profile();
const bool has_male_portrait = !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait && female_portrait != female_type.image() && female_portrait != "unit_image";
int sz = (has_male_portrait && has_female_portrait ? 300 : 400);
if (screen_width <= 1366) {
sz = (has_male_portrait && has_female_portrait ? 200 : 300);
} else if (screen_width >= 1920) {
sz = 400;
}
// TODO: figure out why the second checks don't match but the last does
if (has_male_portrait) {
ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
}
if (has_female_portrait) {
ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz << ")' box='no' align='right' float='yes'</img> ";
}
ss << "\n\n\n";
// Print cross-references to units that this unit advances from/to.
// Cross reference to the topics containing information about those units.
const bool first_reverse_value = true;
bool reverse = first_reverse_value;
if (variation_.empty()) {
do {
std::vector<std::string> adv_units =
reverse ? type_.advances_from() : type_.advances_to();
bool first = true;
for (const std::string &adv : adv_units) {
const unit_type *type = unit_types.find(adv, unit_type::HELP_INDEXED);
if (!type || type->hide_help()) {
continue;
}
if (first) {
if (reverse) {
ss << _("Advances from: ");
} else {
ss << _("Advances to: ");
}
first = false;
} else {
ss << ", ";
}
std::string lang_unit = type->type_name();
std::string ref_id;
if (description_type(*type) == FULL_DESCRIPTION) {
const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
ref_id = section_prefix + unit_prefix + type->id();
} else {
ref_id = unknown_unit_topic;
lang_unit += " (?)";
}
ss << make_link(lang_unit, ref_id);
}
if (!first) {
ss << "\n";
}
reverse = !reverse; //switch direction
} while(reverse != first_reverse_value); // don't restart
}
const unit_type* parent = variation_.empty() ? &type_ :
unit_types.find(type_.id(), unit_type::HELP_INDEXED);
if (!variation_.empty()) {
ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
} else {
bool first = true;
for (const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
if (first) {
ss << _("Base units: ");
first = false;
}
const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
ss << make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
}
}
bool first = true;
for (const std::string &var_id : parent->variations()) {
const unit_type &type = parent->get_variation(var_id);
if(type.hide_help()) {
continue;
}
if (first) {
ss << _("Variations: ");
first = false;
} else {
ss << ", ";
}
std::string ref_id;
std::string var_name = type.variation_name();
if (description_type(type) == FULL_DESCRIPTION) {
ref_id = variation_prefix + type.id() + "_" + var_id;
} else {
ref_id = unknown_unit_topic;
var_name += " (?)";
}
ss << make_link(var_name, ref_id);
}
ss << "\n"; //added even if empty, to avoid shifting
// Print the race of the unit, cross-reference it to the respective topic.
const std::string race_id = type_.race_id();
std::string race_name = type_.race()->plural_name();
if (race_name.empty()) {
race_name = _ ("race^Miscellaneous");
}
ss << _("Race: ");
ss << make_link(race_name, "..race_" + race_id);
ss << "\n\n";
// Print the possible traits of the unit, cross-reference them
// to their respective topics.
if (config::const_child_itors traits = type_.possible_traits()) {
std::vector<trait_data> must_have_traits;
std::vector<trait_data> random_traits;
int must_have_nameless_traits = 0;
for(const config& trait : traits) {
const std::string& male_name = trait["male_name"].str();
const std::string& female_name = trait["female_name"].str();
std::string trait_name;
if (type_.has_gender_variation(unit_race::MALE) && ! male_name.empty())
trait_name = male_name;
else if (type_.has_gender_variation(unit_race::FEMALE) && ! female_name.empty())
trait_name = female_name;
else if (! trait["name"].str().empty())
trait_name = trait["name"].str();
else
continue; // Hidden trait
std::string lang_trait_name = translation::gettext(trait_name.c_str());
if (lang_trait_name.empty() && trait["availability"].str() == "musthave") {
++must_have_nameless_traits;
continue;
}
const std::string ref_id = "traits_"+trait["id"].str();
((trait["availability"].str() == "musthave") ? must_have_traits : random_traits).emplace_back(lang_trait_name, ref_id);
}
bool line1 = !must_have_traits.empty();
bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
if (line1) {
std::string traits_label = _("Traits");
ss << traits_label;
if (line2) {
std::stringstream must_have_count;
must_have_count << " (" << must_have_traits.size() << ") : ";
std::stringstream random_count;
random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits) << ") : ";
int second_line_whitespace = font::line_width(traits_label+must_have_count.str(), normal_font_size)
- font::line_width(random_count.str(), normal_font_size);
// This ensures that the second line is justified so that the ':' characters are aligned.
ss << must_have_count.str();
print_trait_list(ss, must_have_traits);
ss << "\n" << jump(second_line_whitespace) << random_count.str();
print_trait_list(ss, random_traits);
} else {
ss << ": ";
print_trait_list(ss, must_have_traits);
}
ss << "\n\n";
} else {
if (line2) {
ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
print_trait_list(ss, random_traits);
ss << "\n\n";
}
}
}
// Print the abilities the units has, cross-reference them
// to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
if(!type_.abilities_metadata().empty()) {
ss << _("Abilities: ");
for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
std::string lang_ability = translation::gettext(iter->name.c_str());
ss << make_link(lang_ability, ref_id);
if(std::next(iter) != type_.abilities_metadata().end()) {
ss << ", ";
}
}
ss << "\n\n";
}
// Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
if(!type_.adv_abilities_metadata().empty()) {
ss << _("Ability Upgrades: ");
for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
std::string lang_ability = translation::gettext(iter->name.c_str());
ss << make_link(lang_ability, ref_id);
if(std::next(iter) != type_.adv_abilities_metadata().end()) {
ss << ", ";
}
}
ss << "\n\n";
}
// Print some basic information such as HP and movement points.
// TODO: Make this info update according to musthave traits, similar to movetype below.
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
ss << _("HP:") << font::nbsp << type_.hitpoints() << jump(30)
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
<< _("Moves:") << font::nbsp << type_.movement() << jump(30);
if (type_.vision() != type_.movement()) {
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
ss << _("Vision:") << font::nbsp << type_.vision() << jump(30);
}
if (type_.jamming() > 0) {
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
ss << _("Jamming:") << font::nbsp << type_.jamming() << jump(30);
}
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
ss << _("Cost:") << font::nbsp << type_.cost() << jump(30)
// TRANSLATORS: This string is used in the help page of a single unit. If the translation
// uses spaces, use non-breaking spaces as appropriate for the target language to prevent
// unpleasant line breaks (issue #3256).
<< _("Alignment:") << font::nbsp
<< make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day")
<< jump(30);
if (type_.can_advance()) {
// TRANSLATORS: This string is used in the help page of a single unit. It uses
// non-breaking spaces to prevent unpleasant line breaks (issue #3256). In the
// translation use non-breaking spaces as appropriate for the target language.
ss << _("Required\u00a0XP:") << font::nbsp << type_.experience_needed();
}
// Print the detailed description about the unit.
ss << "\n\n" << detailed_description;
// Print the different attacks a unit has, if it has any.
if (!type_.attacks().empty()) {
// Print headers for the table.
ss << "\n\n<header>text='" << escape(_("unit help^Attacks"))
<< "'</header>\n\n";
table_spec table;
std::vector<item> first_row;
// Dummy element, icons are below.
first_row.push_back(item("", 0));
push_header(first_row, _("unit help^Name"));
push_header(first_row, _("Type"));
push_header(first_row, _("Strikes"));
push_header(first_row, _("Range"));
push_header(first_row, _("Special"));
table.push_back(first_row);
// Print information about every attack.
for(const attack_type& attack : type_.attacks()) {
std::string lang_weapon = attack.name();
std::string lang_type = string_table["type_" + attack.type()];
std::vector<item> row;
std::stringstream attack_ss;
attack_ss << "<img>src='" << attack.icon() << "'</img>";
row.emplace_back(attack_ss.str(),image_width(attack.icon()));
push_tab_pair(row, lang_weapon);
push_tab_pair(row, lang_type);
attack_ss.str(clear_stringstream);
attack_ss << attack.damage() << font::weapon_numbers_sep << attack.num_attacks()
<< " " << attack.accuracy_parry_description();
push_tab_pair(row, attack_ss.str());
attack_ss.str(clear_stringstream);
if (attack.min_range() > 1 || attack.max_range() > 1) {
attack_ss << attack.min_range() << "-" << attack.max_range() << ' ';
}
attack_ss << string_table["range_" + attack.range()];
push_tab_pair(row, attack_ss.str());
attack_ss.str(clear_stringstream);
// Show this attack's special, if it has any. Cross
// reference it to the section describing the special.
std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
if (!specials.empty()) {
std::string lang_special = "";
const std::size_t specials_size = specials.size();
for (std::size_t i = 0; i != specials_size; ++i) {
const std::string ref_id = std::string("weaponspecial_")
+ specials[i].first.base_str();
lang_special = (specials[i].first);
attack_ss << make_link(lang_special, ref_id);
if (i+1 != specials_size) {
attack_ss << ", "; //comma placed before next special
}
}
row.emplace_back(attack_ss.str(), font::line_width(lang_special, normal_font_size));
}
table.push_back(row);
}
ss << generate_table(table);
}
// Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated according to any 'musthave' traits which always apply
movetype movement_type = type_.movement_type();
config::const_child_itors traits = type_.possible_traits();
if (!traits.empty() && type_.num_traits() > 0) {
for (const config & t : traits) {
if (t["availability"].str() == "musthave") {
for (const config & effect : t.child_range("effect")) {
if (!effect.child("filter") // If this is musthave but has a unit filter, it might not always apply, so don't apply it in the help.
&& movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
movement_type.merge(effect, effect["replace"].to_bool());
}
}
}
}
}
// Print the resistance table of the unit.
ss << "\n\n<header>text='" << escape(_("Resistances"))
<< "'</header>\n\n";
table_spec resistance_table;
std::vector<item> first_res_row;
push_header(first_res_row, _("Attack Type"));
push_header(first_res_row, _("Resistance"));
resistance_table.push_back(first_res_row);
utils::string_map dam_tab = movement_type.damage_table();
for(std::pair<std::string, std::string> dam_it : dam_tab) {
std::vector<item> row;
int resistance = 100;
try {
resistance -= std::stoi(dam_it.second);
} catch(std::invalid_argument&) {}
std::string resist = std::to_string(resistance) + '%';
const std::size_t pos = resist.find('-');
if (pos != std::string::npos) {
resist.replace(pos, 1, font::unicode_minus);
}
std::string color = unit_helper::resistance_color(resistance);
std::string lang_weapon = string_table["type_" + dam_it.first];
push_tab_pair(row, lang_weapon);
std::stringstream str;
str << "<format>color=\"" << color << "\" text='"<< resist << "'</format>";
const std::string markup = str.str();
str.str(clear_stringstream);
str << resist;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
resistance_table.push_back(row);
}
ss << generate_table(resistance_table);
if (ter_data_cache tdata = load_terrain_types_data()) {
// Print the terrain modifier table of the unit.
ss << "\n\n<header>text='" << escape(_("Terrain Modifiers"))
<< "'</header>\n\n";
std::vector<item> first_row;
table_spec table;
push_header(first_row, _("Terrain"));
push_header(first_row, _("Defense"));
push_header(first_row, _("Movement Cost"));
const bool has_terrain_defense_caps = movement_type.has_terrain_defense_caps(preferences::encountered_terrains());
if (has_terrain_defense_caps) {
push_header(first_row, _("Defense Cap"));
}
const bool has_vision = type_.movement_type().has_vision_data();
if (has_vision) {
push_header(first_row, _("Vision Cost"));
}
const bool has_jamming = type_.movement_type().has_jamming_data();
if (has_jamming) {
push_header(first_row, _("Jamming Cost"));
}
table.push_back(first_row);
std::set<terrain_movement_info> terrain_moves;
for (t_translation::terrain_code terrain : preferences::encountered_terrains()) {
if (terrain == t_translation::FOGGED || terrain == t_translation::VOID_TERRAIN || t_translation::terrain_matches(terrain, t_translation::ALL_OFF_MAP)) {
continue;
}
const terrain_type& info = tdata->get_terrain_info(terrain);
const int moves = movement_type.movement_cost(terrain);
const bool cannot_move = moves > type_.movement();
if (cannot_move && info.hide_if_impassable()) {
continue;
}
if (info.union_type().size() == 1 && info.union_type()[0] == info.number() && info.is_nonnull()) {
terrain_movement_info movement_info =
{
info.name(),
info.id(),
100 - movement_type.defense_modifier(terrain),
moves,
movement_type.vision_cost(terrain),
movement_type.jamming_cost(terrain),
movement_type.get_defense().capped(terrain)
};
terrain_moves.insert(movement_info);
}
}
for(const terrain_movement_info &m : terrain_moves)
{
std::vector<item> row;
bool high_res = false;
const std::string tc_base = high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
const std::string terrain_image = "icons/terrain/terrain_type_" + m.id + (high_res ? "_30.png" : ".png");
const std::string final_image = tc_base + "~RC(magenta>" + m.id + ")~BLIT(" + terrain_image + ")";
row.emplace_back("<img>src='" + final_image + "'</img> " +
make_link(m.name, "..terrain_" + m.id),
font::line_width(m.name, normal_font_size) + (high_res ? 32 : 16) );
//defense - range: +10 % .. +70 %
std::string color;
if (m.defense <= 10) {
color = "red";
} else if (m.defense <= 30) {
color = "yellow";
} else if (m.defense <= 50) {
color = "white";
} else {
color = "green";
}
std::stringstream str;
str << "<format>color=" << color << " text='"<< m.defense << "%'</format>";
std::string markup = str.str();
str.str(clear_stringstream);
str << m.defense << "%";
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
//movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
str.str(clear_stringstream);
bool cannot_move = m.movement_cost > type_.movement();
if (cannot_move) { // cannot move in this terrain
color = "red";
} else if (m.movement_cost > 1) {
color = "yellow";
} else {
color = "white";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the movement costs go above
// the unit's max moves + 5, we replace it with dashes.
if(cannot_move && (m.movement_cost > type_.movement() + 5)) {
str << font::unicode_figure_dash;
} else {
str << m.movement_cost;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << m.movement_cost;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
//defense cap
if (has_terrain_defense_caps) {
str.str(clear_stringstream);
if (m.defense_cap) {
str << "<format>color='"<< color <<"' text='" << m.defense << "%'</format>";
} else {
str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>";
}
markup = str.str();
str.str(clear_stringstream);
if (m.defense_cap) {
str << m.defense << '%';
} else {
str << font::unicode_figure_dash;
}
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
}
//vision
if (has_vision) {
str.str(clear_stringstream);
const bool cannot_view = m.vision_cost > type_.vision();
if (cannot_view) { // cannot view in this terrain
color = "red";
} else if (m.vision_cost > m.movement_cost) {
color = "yellow";
} else if (m.vision_cost == m.movement_cost) {
color = "white";
} else {
color = "green";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the vision costs go above
// the unit's vision + 5, we replace it with dashes.
if(cannot_view && (m.vision_cost > type_.vision() + 5)) {
str << font::unicode_figure_dash;
} else {
str << m.vision_cost;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << m.vision_cost;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
}
//jamming
if (has_jamming) {
str.str(clear_stringstream);
const bool cannot_jam = m.jamming_cost > type_.jamming();
if (cannot_jam) { // cannot jamm in this terrain
color = "red";
} else if (m.jamming_cost > m.vision_cost) {
color = "yellow";
} else if (m.jamming_cost == m.vision_cost) {
color = "white";
} else {
color = "green";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the jamming costs go above
// the unit's jamming + 5, we replace it with dashes.
if (cannot_jam && m.jamming_cost > type_.jamming() + 5) {
str << font::unicode_figure_dash;
} else {
str << m.jamming_cost;
}
str << "'</format>";
push_tab_pair(row, str.str());
}
table.push_back(row);
}
ss << generate_table(table);
} else {
WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we need.\n";
}
return ss.str();
}
void unit_topic_generator::push_header(std::vector< item > &row, const std::string& name) const {
row.emplace_back(bold(name), font::line_width(name, normal_font_size, TTF_STYLE_BOLD));
}
} // end namespace help

View file

@ -1,51 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "help_impl.hpp"
#include <string> // for string
#include <utility> // for pair
#include <vector> // for vector
class terrain_type; // lines 20-20
class unit_type;
namespace help {
class terrain_topic_generator: public topic_generator
{
const terrain_type& type_;
public:
terrain_topic_generator(const terrain_type& type) : type_(type) {}
virtual std::string operator()() const;
};
class unit_topic_generator: public topic_generator
{
const unit_type& type_;
const std::string variation_;
typedef std::pair< std::string, unsigned > item;
void push_header(std::vector< item > &row, const std::string& name) const;
public:
unit_topic_generator(const unit_type &t, std::string variation="") : type_(t), variation_(variation) {}
virtual std::string operator()() const;
};
} // end namespace help/

170
src/help/manager.cpp Normal file
View file

@ -0,0 +1,170 @@
/*
Copyright (C) 2018 by the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/manager.hpp"
#include "config.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "gui/dialogs/help_browser.hpp"
#include "help/constants.hpp"
#include "help/utils.hpp"
#include "preferences/game.hpp"
#include "units/types.hpp"
namespace help
{
help_manager::help_manager()
: help_cfg_(nullptr)
, toplevel_section_(nullptr)
, hidden_sections_(nullptr)
, num_last_encountered_units_(-1)
, num_last_encountered_terrains_(-1)
, last_debug_state_(boost::indeterminate)
{
}
void help_manager::open_help_browser_to(std::string show_topic)
{
// Find all unit_types that have not been constructed yet and fill in the information
// needed to create the help topics
unit_types.build_all(unit_type::HELP_INDEXED);
// Build the actual stuff to display, if we need to.
if(content_update_needed()) {
num_last_encountered_units_ = preferences::encountered_units().size();
num_last_encountered_terrains_ = preferences::encountered_terrains().size();
last_debug_state_ = game_config::debug;
build_topic_tree();
}
if(show_topic.empty()) {
show_topic = help::default_topic;
}
// TODO: what's this for?
if(show_topic.compare(0, 2, "..") == 0) {
show_topic.replace(0, 2, "+");
} else {
show_topic.insert(0, "-");
}
assert(toplevel_section_);
gui2::dialogs::help_browser::display(*toplevel_section_, show_topic);
}
bool help_manager::content_update_needed() const
{
return
toplevel_section_ == nullptr ||
preferences::encountered_units().size() != static_cast<std::size_t>(num_last_encountered_units_) ||
preferences::encountered_terrains().size() != static_cast<std::size_t>(num_last_encountered_terrains_) ||
last_debug_state_ != game_config::debug ||
num_last_encountered_units_ < 0;
}
void help_manager::reset_contents()
{
toplevel_section_.reset(nullptr);
hidden_sections_.reset(nullptr);
num_last_encountered_units_ = -1;
num_last_encountered_terrains_ = -1;
}
void help_manager::update_config_pointer()
{
help_cfg_ = &game_config_manager::get()->game_config().child_or_empty("help");
assert(help_cfg_);
}
void help_manager::build_topic_tree()
{
// We probaby don't need to update the pointers every time, but it's the
// simplest way to ensure these are always valid.
update_config_pointer();
try {
// Start by parsing [toplevel]. It cascades down and parses all referenced sections and topics.
toplevel_section_ = std::make_unique<section>(help_cfg_->child_or_empty("toplevel"), this);
#if 0
//
// TODO: REIMPLEMENT
//
// Create a config object that contains everything that is not referenced from the
// toplevel element. Read this config and save these sections and topics so that they
// can be referenced later on when showing help about specified things, but that
// should not be shown when opening the help browser in the default manner.
config hidden_toplevel;
std::ostringstream ss;
for(const config& section : help_config.child_range("section")) {
const std::string id = section["id"];
if(find_section(default_toplevel, id) == nullptr) {
// This section is not referenced from the toplevel.
// Hence, add it to the hidden ones if it is not referenced from another section.
if(!section_is_referenced(id, help_config)) {
if(!ss.str().empty()) {
ss << ",";
}
ss << id;
}
}
}
hidden_toplevel["sections"] = ss.str();
ss.str("");
for(const config &topic : help_config.child_range("topic")) {
const std::string id = topic["id"];
if(find_topic(default_toplevel, id) == nullptr) {
if(!topic_is_referenced(id, help_config)) {
if(!ss.str().empty()) {
ss << ",";
}
ss << id;
}
}
}
hidden_toplevel["topics"] = ss.str();
config hidden_cfg = help_config;
// Change the toplevel to our new, custom built one.
hidden_cfg.clear_children("toplevel");
hidden_cfg.add_child("toplevel", std::move(hidden_toplevel));
hidden_sections = parse_config(&hidden_cfg);
#endif
} catch(const parse_error& e) {
std::cerr << "Error while parsing help text: '" << e.message << "'" << std::endl;
}
}
const config& help_manager::get_section_config(const std::string& id) const
{
return help_cfg_->find_child("section", "id", id);
}
const config& help_manager::get_topic_config(const std::string& id) const
{
return help_cfg_->find_child("topic", "id", id);
}
} // namespace help

82
src/help/manager.hpp Normal file
View file

@ -0,0 +1,82 @@
/*
Copyright (C) 2018 by the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "config.hpp"
#include "help/section.hpp"
#include "help/topic.hpp"
#include <boost/logic/tribool.hpp>
#include <memory>
#include <string>
class config;
namespace help
{
class help_manager
{
public:
help_manager();
/**
* Displays the help browser.
*
* @param show_topic The initial topic to open to.
*
* The topic tree will be regenerated as needed beforehand.
*
* @todo should we allow passing an arbitrary toplevel section here?
* The old code did expose a public interface to open to any topic, but it wasn't
* used in any user-facing code, only as an implementation helper for the other
* help display functions.
*/
void open_help_browser_to(std::string show_topic);
/** Clears the generated section data. */
void reset_contents();
/***** ***** ***** ***** Data getters ***** ***** ****** *****/
/** Returns the [section] child with the given id. */
const config& get_section_config(const std::string& id) const;
/** Returns the [topic] child with the given id. */
const config& get_topic_config(const std::string& id) const;
private:
/** Returns true if we need to regenerate the toplevel and hidden sections. */
bool content_update_needed() const;
void update_config_pointer();
/** Fills in both toplevel_section_ and hidden_sections_. */
void build_topic_tree();
const config* help_cfg_;
/** The default toplevel section node. */
section::section_ptr toplevel_section_;
/** All sections and topics not referenced from the default toplevel node. */
section::section_ptr hidden_sections_;
int num_last_encountered_units_;
int num_last_encountered_terrains_;
boost::tribool last_debug_state_;
};
} // namespace help

322
src/help/section.cpp Normal file
View file

@ -0,0 +1,322 @@
/*
Copyright (C) 2018 by the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/section.hpp"
#include "config.hpp"
#include "formatter.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/manager.hpp"
#include "help/section_generators.hpp"
#include "help/topic_generators.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "serialization/string_utils.hpp"
#include <iterator>
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help
{
section::section(const config& section_cfg, const help_manager* manager)
: id("toplevel")
, title()
, topics_()
, sections_()
, recursion_level_(0)
, manager_(manager)
{
assert(manager_ && "Y U NO provide help manager!");
initialize(section_cfg);
}
section::section(const config& section_cfg, const section& parent)
: id(section_cfg["id"])
, title(section_cfg["title"])
, topics_()
, sections_()
, recursion_level_(parent.recursion_level_ + 1)
, manager_(parent.manager_)
{
assert(manager_);
initialize(section_cfg);
}
void section::initialize(const config& section_cfg)
{
// If we're in too deep, exit.
if(recursion_level_ > max_section_recursion_level) {
throw max_recursion_reached("Maximum section depth has been reached. Possible circular dependency?");
}
if(section_cfg.empty()) {
return; // TODO: throw something?
}
//
// Parse static sub-sections.
//
for(const std::string& section_id : utils::quoted_split(section_cfg["sections"])) {
if(const config& child_cfg = manager_->get_section_config(section_id)) {
if(!is_valid_id(section_id)) {
throw parse_error(formatter() << "Invalid ID, used for internal purposes: '" << id << "'");
}
add_section(child_cfg);
} else {
throw parse_error(formatter()
<< "Help section '" << section_id << "' referenced from '" << id << "' but could not be found.");
}
}
//
// Generate dynamic sub-sections.
//
generate_sections(section_cfg["sections_generator"].str());
if(section_cfg["sort_sections"] == "yes") {
std::sort(sections_.begin(), sections_.end(), [](const section_ptr& lhs, const section_ptr& rhs) {
return translation::compare(lhs->title, rhs->title) < 0; }
);
}
const config::attribute_value& st = section_cfg["sort_topics"];
bool sort_topics = false;
bool sort_generated = false;
if(st == "yes") {
sort_topics = true;
sort_generated = false;
} else if(st == "no") {
sort_topics = false;
sort_generated = false;
} else if(st == "generated") {
sort_topics = false;
sort_generated = true;
} else if(!st.empty()) {
throw parse_error(formatter() << "Invalid sort option: '" << st << "'");
}
//
// Parse static section topics
//
for(const std::string& topic_id : utils::quoted_split(section_cfg["topics"])) {
if(const config& topic_cfg = manager_->get_topic_config(topic_id)) {
if(!is_valid_id(topic_id)) {
throw parse_error(formatter() << "Invalid ID, used for internal purposes: '" << id << "'");
}
std::ostringstream text;
text << topic_cfg["text"];
text << generate_table_of_contents(topic_cfg["generator"]);
// We don't need to use add_topic here since we don't need a special text generator.
topics_.emplace_back(topic_id, topic_cfg["title"], text.str());
} else {
throw parse_error(formatter()
<< "Help-topic '" << topic_id << "' referenced from '" << id << "' but could not be found.");
}
}
//
// Generate dynamic topics.
//
generate_topics(section_cfg["generator"], sort_generated);
if(sort_topics) {
this->sort_topics();
}
}
void section::generate_sections(const std::string& generator_type)
{
if(generator_type == "races") {
generate_races_sections(*this);
} else if(generator_type == "terrains") {
generate_terrain_sections(*this);
} else if(generator_type == "eras") {
DBG_HP << "Generating eras...\n";
generate_era_sections(*this);
} else {
std::vector<std::string> parts = utils::split(generator_type, ':', utils::STRIP_SPACES);
if(parts.size() > 1 && parts[0] == "units") {
generate_unit_sections(*this, parts[1]);
} else if(!generator_type.empty()) {
WRN_HP << "Unknown section generator: " << generator_type << "\n";
}
}
}
void section::generate_topics(const std::string& generator_type, const bool sort_generated)
{
if(generator_type.empty()) {
return;
}
topic_list res;
if(generator_type == "abilities") {
res = generate_ability_topics(sort_generated);
} else if(generator_type == "weapon_specials") {
res = generate_weapon_special_topics(sort_generated);
} else if(generator_type == "time_of_days") {
res = generate_time_of_day_topics(sort_generated);
} else if(generator_type == "traits") {
res = generate_trait_topics(sort_generated);
} else {
std::vector<std::string> parts = utils::split(generator_type, ':', utils::STRIP_SPACES);
if(parts.size() > 1 && parts[0] == "units") {
res = generate_unit_topics(sort_generated, parts[1]);
} else if (parts[0] == "era" && parts.size()>1) {
res = generate_era_topics(sort_generated, parts[1]);
} else {
WRN_HP << "Unknown topic generator: " << generator_type << "\n";
}
}
// Add the generated topics to this section.
topics_.splice(topics_.end(), res);
}
std::string section::generate_table_of_contents(const std::string& generator)
{
if(generator.empty()) {
return "";
}
std::vector<std::string> parts = utils::split(generator, ':');
if(parts.size() > 1 && parts[0] == "contents") {
if(parts[1] == "generated") {
return print_table_of_contents();
} else {
return print_table_of_contents_for(parts[1]);
}
}
return "";
}
std::string section::print_table_of_contents_for(const std::string& section_id) const
{
const config& section_cfg = manager_->get_section_config(section_id);
if(!section_cfg) {
return "";
}
// We use an intermediate structure to allow a conditional sorting
std::vector<std::pair<std::string, std::string>> topics_links;
// Find all topics in this section.
for(const std::string& topic_id : utils::quoted_split(section_cfg["topics"])) {
if(const config& topic_cfg = manager_->get_topic_config(topic_id)) {
if(is_visible_id(topic_id)) {
topics_links.emplace_back(topic_cfg["title"], topic_id);
}
}
}
if(section_cfg["sort_topics"].to_bool()) {
std::sort(topics_links.begin(), topics_links.end());
}
std::ostringstream res;
for(const auto& link : topics_links) {
res << font::unicode_bullet << ' ' << make_link(link.first, link.second) << '\n';
}
return res.str();
}
std::string section::print_table_of_contents() const
{
std::ostringstream res;
for(const section_ptr& s : sections_) {
if(is_visible_id(s->id)) {
res << font::unicode_bullet << ' ' << make_link(s->title, ".." + s->id) << '\n';
}
}
for(const topic& t : topics_) {
if(is_visible_id(t.id)) {
res << font::unicode_bullet << ' ' << make_link(t.title, t.id) << '\n';
}
}
return res.str();
}
section* section::add_section(const config& new_section_cfg)
{
try {
sections_.emplace_back(std::make_unique<section>(new_section_cfg, *this));
return sections_.back().get();
} catch(const parse_error& e) {
std::cerr << "Error while parsing help text: " << e.message << std::endl;
} catch(const max_recursion_reached& e) {
std::cerr << e.message << std::endl;
}
return nullptr;
}
const section* section::find_section(const std::string& s_id) const
{
// First, check this section's sub-sections...
auto sec_iter = std::find_if(sections_.begin(), sections_.end(),
[&s_id](const section_ptr& s) { return s && s_id == s->id; });
if(sec_iter != sections_.end()) {
return sec_iter->get();
}
// ...then recursively check this section's sub-section's sub-sections.
for(const section_ptr& s : sections_) {
if(const section* ss = s->find_section(s_id)) {
return ss;
}
}
return nullptr;
}
const topic* section::find_topic(const std::string& t_id) const
{
// First, check this section's topics...
auto topic_iter = std::find_if(topics_.begin(), topics_.end(),
[&t_id](const topic& t) { return t_id == t.id; });
if(topic_iter != topics_.end()) {
return &(*topic_iter);
}
// ...then recursively check this section's sub-section's topics.
for(const section::section_ptr& s : sections_) {
if(const topic* t = s->find_topic(t_id)) {
return t;
}
}
return nullptr;
}
} // namespace help

151
src/help/section.hpp Normal file
View file

@ -0,0 +1,151 @@
/*
Copyright (C) 2018 by the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "help/topic.hpp"
#include <memory>
#include <string>
#include <vector>
namespace help
{
class help_manager;
/**
* A help section.
*
* Sections serve as the branches of the topic tree. Each section has a set of topics,
* and can have additional sub-sections.
*
* A section on its own doesn't have any text. That's the exclusive domain of topics.
*/
class section
{
public:
using section_ptr = std::unique_ptr<section>;
using section_list = std::vector<section_ptr>;
/**
* Creates a section owned by the given manager. This is considered the toplevel node.
*
* The manager argument is mandatory; it's only a pointer since this class keeps a
* pointer to the manager and there's no reason to go ptr->ref->ptr.
*/
section(const config& section_cfg, const help_manager* manager);
/**
* Creates a new sub-section owned by the given parent section.
*/
section(const config& section_cfg, const section& parent);
section(const section&) = delete;
section& operator=(const section&) = delete;
bool operator==(const section& s) const
{
return id == s.id;
}
/**
* Adds a new sub-section to this section.
*
* @param new_section_cfg The config data for the new section.
*
* @returns A pointer to the newly-added section.
*/
section* add_section(const config& new_section_cfg);
/**
* Adds a new topic to this section.
*
* @tparam T The text generator type.
* @tparam Args The values to forward to the text generator's constructor.
*
* @param id The id of the new section.
* @param title The title of the new section.
*
* @todo Might make sense for this to return a pointer-to-topic like @ref add_section
* does, but we don't need that right now.
*/
template<typename T, typename... Args>
void add_topic(const std::string& id, const std::string& title, Args&&... generator_args)
{
topics_.emplace_back(id, title, std::make_unique<T>(std::forward<Args>(generator_args)...));
}
/**
* Returns a pointer to the section with the given id, or nullptr if none is found.
* Sub-sections' sub-sections are also searched.
*/
const section* find_section(const std::string& s_id) const;
/**
* Returns a pointer to the topic with the given id, or nullptr if none is found.
* Sub-sections' topics are also searched.
*/
const topic* find_topic(const std::string& t_id) const;
/** Deletes this section's sub-sections and topics. */
void clear()
{
topics_.clear();
sections_.clear();
}
void sort_topics()
{
topics_.sort();
}
const topic_list& topics() const
{
return topics_;
}
const section_list& sections() const
{
return sections_;
}
std::string id;
std::string title;
private:
/** Constructor implementation detail. */
void initialize(const config& section_cfg);
void generate_sections(const std::string& generator_type);
void generate_topics(const std::string& generator_type, const bool sort_generated);
std::string generate_table_of_contents(const std::string& generator);
std::string print_table_of_contents() const;
std::string print_table_of_contents_for(const std::string& section_id) const;
/** All topics this section owns. */
topic_list topics_;
/** All sub-sections. */
section_list sections_;
/** Tracks how many levels deep this section is in a topic tree. */
unsigned recursion_level_;
/** The manager that owns the toplevel node. This the same for all sections in a tree */
const help_manager* manager_;
};
} // namespace help

View file

@ -0,0 +1,196 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/section_generators.hpp"
#include "formatter.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/manager.hpp"
#include "help/constants.hpp"
#include "help/section.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "preferences/game.hpp"
#include "terrain/terrain.hpp"
#include "terrain/translation.hpp"
#include "terrain/type_data.hpp"
#include "units/race.hpp"
#include "units/types.hpp"
#include <set>
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help
{
void generate_races_sections(section& sec)
{
std::set<std::string, string_less> races;
std::set<std::string, string_less> visible_races;
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(description_type(type) == FULL_DESCRIPTION) {
races.insert(type.race_id());
if(!type.hide_help()) {
visible_races.insert(type.race_id());
}
}
}
for(const auto& race : races) {
config section_cfg;
const bool hidden = (visible_races.count(race) == 0);
std::string title;
if(const unit_race* r = unit_types.find_race(race)) {
title = r->plural_name();
} else {
title = _("race^Miscellaneous");
}
section_cfg["id"] = hidden_symbol(hidden) + race_prefix + race;
section_cfg["title"] = title;
section_cfg["sections_generator"] = "units:" + race;
section_cfg["generator"] = "units:" + race;
sec.add_section(section_cfg);
}
}
void generate_terrain_sections(section& sec)
{
ter_data_cache tdata = load_terrain_types_data();
if(!tdata) {
WRN_HP << "When building terrain help sections, couldn't acquire terrain types data, aborting.\n";
return;
}
std::map<std::string, section*> base_map;
for(const t_translation::terrain_code& t : tdata->list()) {
const terrain_type& info = tdata->get_terrain_info(t);
bool hidden = info.is_combined() || info.hide_help();
if(preferences::encountered_terrains().find(t) == preferences::encountered_terrains().end()
&& !info.is_overlay()
) {
hidden = true;
}
std::string topic_id = hidden_symbol(hidden) + terrain_prefix + info.id();
for(const t_translation::terrain_code& base : tdata->underlying_union_terrain(t)) {
const terrain_type& base_info = tdata->get_terrain_info(base);
const std::string& base_id = base_info.id();
if(!base_info.is_nonnull() || base_info.hide_help()) {
continue;
}
if(base_id == info.id()) {
topic_id = ".." + terrain_prefix + info.id();
}
section* base_section = nullptr;
try {
base_section = base_map.at(base_id);
} catch(const std::out_of_range&) {
config base_section_config;
base_section_config["id"] = terrain_prefix + base_id;
base_section_config["title"] = base_info.editor_name();
base_section = base_map[base_id] = sec.add_section(base_section_config);
}
// May be null if a parse/recursion depth error ocurred during construction.
if(base_section) {
base_section->add_topic<terrain_topic_generator>(topic_id, info.editor_name(), info);
}
}
}
for(auto& t_sec : base_map) {
if(section* s = t_sec.second) {
s->sort_topics();
}
}
}
void generate_era_sections(section& sec)
{
// TODO: should we be using the gcm here?
for(const config& era : game_config_manager::get()->game_config().child_range("era")) {
if(era["hide_help"].to_bool()) {
continue;
}
DBG_HP << "Adding help section: " << era["id"].str() << "\n";
config section_cfg;
section_cfg["id"] = era_prefix + era["id"].str();
section_cfg["title"] = era["name"];
section_cfg["generator"] = "era:" + era["id"].str();
DBG_HP << section_cfg.debug() << "\n";
sec.add_section(section_cfg);
}
}
void generate_unit_sections(section& sec, const std::string& race)
{
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(type.race_id() != race || !type.show_variations_in_help()) {
continue;
}
config base_unit_config;
base_unit_config["id"] = hidden_symbol(type.hide_help()) + unit_prefix + type.id();
base_unit_config["title"] = type.type_name();
section* base_unit = sec.add_section(base_unit_config);
// May be null if a parse/recursion depth error ocurred during construction.
// In that case, no need to parse variations since we have no section to which to
// append them.
if(!base_unit) {
continue;
}
// Add topics for each of the unit's variations.
for(const std::string& variation_id : type.variations()) {
// TODO: Do we apply encountered stuff to variations?
const unit_type& var_type = type.get_variation(variation_id);
const std::string topic_name = var_type.type_name() + "\n" + var_type.variation_name();
const std::string topic_id = formatter()
<< hidden_symbol(var_type.hide_help()) << variation_prefix << var_type.id() << "_" << variation_id;
base_unit->add_topic<unit_topic_generator>(topic_id, topic_name, var_type, variation_id);
}
}
}
} // namespace help

View file

@ -0,0 +1,28 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <string>
namespace help
{
class section;
void generate_races_sections(section& sec);
void generate_terrain_sections(section& sec);
void generate_era_sections(section& sec);
void generate_unit_sections(section& sec, const std::string& race);
} // namespace help

204
src/help/topic.cpp Normal file
View file

@ -0,0 +1,204 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/topic.hpp"
#include "config.hpp"
#include "formatter.hpp"
#include "gettext.hpp"
#include "help/utils.hpp"
#include "serialization/parser.hpp"
#include <iostream>
namespace help
{
namespace
{
std::string convert_to_wml(const std::string& element_name, const std::string& contents)
{
std::stringstream ss;
bool in_quotes = false;
bool last_char_escape = false;
const char escape_char = '\\';
std::vector<std::string> attributes;
// Find the different attributes.
// No checks are made for the equal sign or something like that.
// Attributes are just separated by spaces or newlines.
// Attributes that contain spaces must be in single quotes.
for(std::size_t pos = 0; pos < contents.size(); ++pos) {
const char c = contents[pos];
if(c == escape_char && !last_char_escape) {
last_char_escape = true;
} else {
if(c == '\'' && !last_char_escape) {
ss << '"';
in_quotes = !in_quotes;
} else if((c == ' ' || c == '\n') && !last_char_escape && !in_quotes) {
// Space or newline, end of attribute.
attributes.push_back(ss.str());
ss.str("");
} else {
ss << c;
}
last_char_escape = false;
}
}
if(in_quotes) {
throw parse_error(formatter() << "Unterminated single quote after: '" << ss.str() << "'");
}
if(!ss.str().empty()) {
attributes.push_back(ss.str());
}
ss.str("");
// Create the WML.
ss << "[" << element_name << "]\n";
for(const std::string& attr : attributes) {
ss << attr << "\n";
}
ss << "[/" << element_name << "]\n";
return ss.str();
}
config parse_text(const std::string& text)
{
config res;
const auto add_text_child =
[&res](const std::string& text) { res.add_child("text", config {"text", text}); };
bool last_char_escape = false;
const char escape_char = '\\';
std::stringstream ss;
std::size_t pos;
enum { ELEMENT_NAME, OTHER } state = OTHER;
for(pos = 0; pos < text.size(); ++pos) {
const char c = text[pos];
if(c == escape_char && !last_char_escape) {
last_char_escape = true;
} else {
if(state == OTHER) {
if(c == '<') {
if(last_char_escape) {
ss << c;
} else {
add_text_child(ss.str());
ss.str("");
state = ELEMENT_NAME;
}
} else {
ss << c;
}
} else if(state == ELEMENT_NAME) {
if(c == '/') {
throw parse_error("Erroneous / in element name.");
} else if(c == '>') {
const std::string element_name = ss.str();
ss.str("");
// End of this name.
// For markup such as "<span color=''>", we only want the first word. If the name
// has no spaces, the entirety will be used.
std::stringstream s;
s << "</" << element_name.substr(0, element_name.find_first_of(' ')) << ">";
const std::string end_element_name = s.str();
std::size_t end_pos = text.find(end_element_name, pos);
if(end_pos == std::string::npos) {
throw parse_error(formatter() << "Unterminated element: " << element_name);
}
const std::string contents = text.substr(pos + 1, end_pos - pos - 1);
// If we find no '=' character, we assume we're dealing with Pango markup.
if(contents.find('=') != std::string::npos) {
s.str(convert_to_wml(element_name, contents));
s.seekg(0);
try {
config cfg;
read(cfg, s);
res.append_children(cfg);
} catch(const config::error& e) {
throw parse_error(formatter() << "Error when parsing help markup as WML: " << e.message);
}
} else {
add_text_child(formatter() << "<" << element_name << ">" << contents << end_element_name);
}
pos = end_pos + end_element_name.size() - 1;
state = OTHER;
} else {
ss << c;
}
}
last_char_escape = false;
}
}
if(state == ELEMENT_NAME) {
throw parse_error(formatter() << "Element '" << ss.str() << "' continues through end of string.");
}
// Add the last string.
if(!ss.str().empty()) {
res.add_child("text", config{"text", ss.str()});
}
return res;
}
} // end anon namespace
const config& topic::parsed_text() const
{
if(text_generator_) {
try {
parsed_text_ = parse_text(text_generator_->generate());
} catch(const parse_error& e) {
std::cerr << e.message << std::endl;
}
text_generator_.reset(nullptr);
}
return parsed_text_;
}
bool topic::operator<(const topic& t) const
{
return translation::compare(title, t.title) < 0;
}
} // namespace help

80
src/help/topic.hpp Normal file
View file

@ -0,0 +1,80 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "help/topic_text_generators.hpp"
#include <list>
#include <memory>
#include <string>
namespace help
{
/** A topic contains a title, an id and some text. */
struct topic
{
using text_gen_ptr_t = std::unique_ptr<topic_text_generator>;
topic(const std::string& t_id, const std::string& t_title)
: id(t_id)
, title(t_title)
, text_generator_(nullptr)
, parsed_text_()
{
}
topic(const std::string& t_id, const std::string& t_title, const std::string& t_text)
: id(t_id)
, title(t_title)
, text_generator_(std::make_unique<plain_text_topic_generator>(t_text))
, parsed_text_()
{
}
topic(const std::string& t_id, const std::string& t_title, text_gen_ptr_t g)
: id(t_id)
, title(t_title)
, text_generator_(std::move(g))
, parsed_text_()
{
}
bool operator==(const topic& t) const
{
return id == t.id;
}
bool operator!=(const topic& t) const
{
return !operator==(t);
}
/** Case-sensitive, locale-dependent sort by title. */
bool operator<(const topic& t) const;
std::string id;
std::string title;
const config& parsed_text() const;
private:
mutable std::unique_ptr<topic_text_generator> text_generator_;
mutable config parsed_text_;
};
using topic_list = std::list<topic>;
} // namespace help

View file

@ -0,0 +1,532 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/topic_generators.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/topic.hpp"
#include "help/topic_text_generators.hpp"
#include "help/utils.hpp"
#include "log.hpp"
#include "resources.hpp"
#include "serialization/string_utils.hpp"
#include "tod_manager.hpp"
#include "units/types.hpp"
#include <map>
#include <set>
#include <string>
static lg::log_domain log_help("help");
#define DBG_HP LOG_STREAM(debug, log_help)
namespace help
{
namespace
{
std::string make_unit_link(const std::string& type_id)
{
std::string link;
const unit_type* type = unit_types.find(type_id, unit_type::HELP_INDEXED);
if(!type) {
std::cerr << "Unknown unit type : " << type_id << "\n";
// Don't return a hyperlink (no page). Instead show the id (as hint)
link = type_id;
} else if(!type->hide_help()) {
std::string name = type->type_name();
std::string ref_id;
if(description_type(*type) == FULL_DESCRIPTION) {
const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
ref_id = section_prefix + unit_prefix + type->id();
} else {
ref_id = unknown_unit_topic;
name += " (?)";
}
link = make_link(name, ref_id);
} // if hide_help then link is an empty string
return link;
}
std::vector<std::string> make_unit_links_list(const std::vector<std::string>& type_id_list, bool ordered)
{
std::vector<std::string> links_list;
for(const std::string& type_id : type_id_list) {
std::string unit_link = make_unit_link(type_id);
if(!unit_link.empty()) {
links_list.push_back(std::move(unit_link));
}
}
if(ordered) {
std::sort(links_list.begin(), links_list.end());
}
return links_list;
}
} // end anon namespace
topic_list generate_ability_topics(const bool sort_generated)
{
topic_list topics;
std::map<std::string, const unit_type::ability_metadata*> ability_topic_data;
std::map<std::string, std::set<std::string, string_less>> ability_units;
const auto parse = [&](const unit_type& type, const unit_type::ability_metadata& ability) {
// NOTE: neither ability names nor ability ids are necessarily unique. Creating
// topics for either each unique name or each unique id means certain abilities
// will be excluded from help. So... the ability topic ref id is a combination
// of id and (untranslated) name. It's rather ugly, but it works.
const std::string topic_ref = ability.id + ability.name.base_str();
ability_topic_data.emplace(topic_ref, &ability);
if(!type.hide_help()) {
// Add a link in the list of units with this ability
// We put the translated name at the beginning of the hyperlink,
// so the automatic alphabetic sorting of std::set can use it
const std::string link = make_link(type.type_name(), unit_prefix + type.id());
ability_units[topic_ref].insert(link);
}
};
// Look through all the unit types. If a unit of that type would have a full
// description, add its abilities to the potential topic list. We don't want
// to show abilities that the user has not encountered yet.
for(const auto& type_mapping : unit_types.types()) {
const unit_type& type = type_mapping.second;
if(description_type(type) != FULL_DESCRIPTION) {
continue;
}
for(const unit_type::ability_metadata& ability : type.abilities_metadata()) {
parse(type, ability);
}
for(const unit_type::ability_metadata& ability : type.adv_abilities_metadata()) {
parse(type, ability);
}
}
for(const auto& a : ability_topic_data) {
std::ostringstream text;
text << a.second->description;
text << "\n\n" << "<big>" << _("Units with this ability") << "</big>"<< "\n";
for(const auto& u : ability_units[a.first]) { // first is the topic ref id
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(ability_prefix + a.first, a.second->name, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_weapon_special_topics(const bool sort_generated)
{
topic_list topics;
std::map<t_string, std::string> special_description;
std::map<t_string, std::set<std::string, string_less>> special_units;
// FIXME: make this use the same id + name ref method as the ability topics.
const auto parse = [&](const unit_type& type, const std::pair<t_string, t_string>& data) {
special_description.emplace(data.first, data.second);
if(!type.hide_help()) {
// Check for variations (walking corpse/soulless etc)
const std::string section_prefix = type.show_variations_in_help() ? ".." : "";
const std::string ref_id = section_prefix + unit_prefix + type.id();
// We put the translated name at the beginning of the hyperlink,
// so the automatic alphabetic sorting of std::set can use it
special_units[data.first].insert(make_link(type.type_name(), ref_id));
}
};
const auto parse_config = [&parse](const unit_type& type, const config& specials) {
for(const config::any_child& spec : specials.all_children_range()) {
if(!spec.cfg["name"].empty()) {
parse(type, std::make_pair(spec.cfg["name"].t_str(), spec.cfg["description"].t_str()));
}
}
};
for(const auto& type_mapping : unit_types.types()) {
const unit_type& type = type_mapping.second;
// Only show the weapon special if we find it on a unit that
// detailed description should be shown about.
if(description_type(type) != FULL_DESCRIPTION) {
continue;
}
for(const attack_type& atk : type.attacks()) {
for(const auto& s_tooltip : atk.special_tooltips()) {
parse(type, s_tooltip);
}
}
for(const config& adv : type.modification_advancements()) {
for(const config& effect : adv.child_range("effect")) {
if(effect["apply_to"] == "new_attack" && effect.has_child("specials")) {
parse_config(type, effect.child("specials"));
} else if(effect["apply_to"] == "attack" && effect.has_child("set_specials")) {
parse_config(type, effect.child("set_specials"));
}
}
}
}
for(auto& s : special_description) {
// use untranslated name to have universal topic id
std::string id = weapon_special_prefix + s.first.base_str();
std::ostringstream text;
text << s.second;
text << "\n\n" << "<big>" << _("Units with this special attack") << "</big>" << "\n";
for(const std::string& u : special_units[s.first]) {
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(id, s.first, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_time_of_day_topics(const bool /*sort_generated*/)
{
topic_list topics;
std::ostringstream toplevel;
if(!resources::tod_manager) {
toplevel << _("Only available during a scenario.");
topics.emplace_back("..schedule", _("Time of Day Schedule"), toplevel.str());
return topics;
}
for(const time_of_day& time : resources::tod_manager->times()) {
const std::string id = tod_prefix + time.id;
const std::string image = "<img>src='" + time.image + "'</img>";
toplevel << make_link(time.name.str(), id) << jump_to(160) << image << jump(30) << time.lawful_bonus << '\n';
std::ostringstream text;
text << image << '\n'
<< time.description.str() << '\n'
<< _("Lawful Bonus:") << ' ' << time.lawful_bonus << '\n'
<< '\n'
<< make_link(_("Schedule"), "..schedule");
topics.emplace_back(id, time.name.str(), text.str());
}
topics.emplace_back("..schedule", _("Time of Day Schedule"), toplevel.str());
return topics;
}
topic_list generate_trait_topics(const bool sort_generated)
{
topic_list topics;
std::map<t_string, const config*> trait_list;
for(const config& trait : unit_types.traits()) {
trait_list.emplace(trait["id"].t_str(), &trait);
}
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(description_type(type) == FULL_DESCRIPTION) {
if(auto traits = type.possible_traits()) {
for(const config& trait : traits) {
trait_list.emplace(trait["id"].t_str(), &trait);
}
}
if(const unit_race* r = unit_types.find_race(type.race_id())) {
for(const config& trait : r->additional_traits()) {
trait_list.emplace(trait["id"].t_str(), &trait);
}
}
}
}
for(auto& a : trait_list) {
const std::string id = trait_prefix + a.first;
const config& trait = *a.second;
std::string name = trait["male_name"].str();
if(name.empty()) {
name = trait["female_name"].str();
}
if(name.empty()) {
name = trait["name"].str();
}
if(name.empty()) {
continue; // Hidden trait
}
std::ostringstream text;
if(!trait["help_text"].empty()) {
text << trait["help_text"];
} else if(!trait["description"].empty()) {
text << trait["description"];
} else {
text << _("No description available.");
}
text << "\n\n";
if(trait["availability"] == "musthave") {
text << _("Availability: Must-have") << "\n";
} else if(trait["availability"] == "none") {
text << _("Availability: Unavailable") << "\n";
}
topics.emplace_back(id, name, text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_unit_topics(const bool sort_generated, const std::string& race)
{
topic_list topics;
std::set<std::string, string_less> race_units;
std::set<std::string, string_less> race_topics;
std::set<std::string> alignments;
for(const auto& i : unit_types.types()) {
const unit_type& type = i.second;
if(type.race_id() != race) {
continue;
}
if(description_type(type) != FULL_DESCRIPTION) {
continue;
}
const std::string type_name = type.type_name();
const std::string real_prefix = type.show_variations_in_help() ? ".." : "";
const std::string ref_id = hidden_symbol(type.hide_help()) + real_prefix + unit_prefix + type.id();
topics.emplace_back(ref_id, type_name, std::make_unique<unit_topic_generator>(type));
if(!type.hide_help()) {
// we also record an hyperlink of this unit in the list used for the race topic/
race_units.insert(make_link(type_name, ref_id));
alignments.insert(
make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
}
}
// generate the hidden race description topic
std::string race_id = ".." + race_prefix + race;
std::string race_name;
std::string race_description;
if(const unit_race* r = unit_types.find_race(race)) {
race_name = r->plural_name();
race_description = r->description();
for(const config& additional_topic : r->additional_topics()) {
std::string id = additional_topic["id"];
std::string title = additional_topic["title"];
std::string text = additional_topic["text"];
topics.emplace_back(id, title, text);
race_topics.insert(make_link(title, id));
}
} else {
race_name = _("race^Miscellaneous");
}
std::ostringstream text;
if(!race_description.empty()) {
text << race_description << "\n\n";
}
if(!alignments.empty()) {
text << (alignments.size() > 1 ? _("Alignments:") : _("Alignment:")) << ' ';
text << utils::join(alignments, ", ") << "\n\n";
}
text << "<big>" << _("Units of this race") << "</big>\n";
for(const std::string& u : race_units) {
text << font::unicode_bullet << " " << u << "\n";
}
topics.emplace_back(race_id, race_name, text.str());
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_faction_topics(const bool sort_generated, const config& era)
{
topic_list topics;
for(const config& f : era.child_range("multiplayer_side")) {
const std::string& id = f["id"];
if(id == "Random") {
continue;
}
std::ostringstream text;
const config::attribute_value& description = f["description"];
if(!description.empty()) {
text << description.t_str() << "\n";
text << "\n";
}
const std::vector<std::string> recruit_ids = utils::split(f["recruit"]);
std::set<std::string> races;
std::set<std::string> alignments;
for(const std::string& u_id : recruit_ids) {
if(const unit_type* t = unit_types.find(u_id, unit_type::HELP_INDEXED)) {
assert(t);
const unit_type& type = *t;
if(const unit_race* r = unit_types.find_race(type.race_id())) {
races.insert(make_link(r->plural_name(), std::string("..") + race_prefix + r->id()));
}
DBG_HP << type.alignment() << " -> "
<< type.alignment_description(type.alignment(), type.genders().front()) << "\n";
alignments.insert(
make_link(type.alignment_description(type.alignment(), type.genders().front()), "time_of_day"));
}
}
if(!races.empty()) {
text << _("Races:") << ' ' << utils::join(races, ", ");
text << "\n\n";
}
if(!alignments.empty()) {
text << _("Alignments:") << ' ' << utils::join(alignments, ", ");
text << "\n\n";
}
text << "<big>" << _("Leaders") << "</big>";
text << "\n";
for(const std::string& link : make_unit_links_list(utils::split(f["leader"]), true)) {
text << font::unicode_bullet << " " << link << "\n";
}
text << "\n";
text << "<big>" << _("Recruits") << "</big>";
text << "\n";
for(const std::string& link : make_unit_links_list(recruit_ids, true)) {
text << font::unicode_bullet << " " << link << "\n";
}
const std::string ref_id = faction_prefix + era["id"] + "_" + id;
topics.emplace_back(ref_id, f["name"], text.str());
}
if(sort_generated) {
topics.sort();
}
return topics;
}
topic_list generate_era_topics(const bool sort_generated, const std::string& era_id)
{
topic_list topics;
// TODO: should we be using the gcm here?
const config& era = game_config_manager::get()->game_config().find_child("era", "id", era_id);
if(era && !era["hide_help"].to_bool()) {
topics = generate_faction_topics(sort_generated, era);
std::vector<std::string> faction_links;
for(const topic& t : topics) {
faction_links.push_back(make_link(t.title, t.id));
}
std::ostringstream text;
text << "<big>" << _("Era:") << " " << era["name"] << "</big>";
text << "\n\n";
const config::attribute_value& description = era["description"];
if(!description.empty()) {
text << description.t_str() << "\n\n";
}
text << "<big>" << _("Factions") << "</big>";
text << "\n";
std::sort(faction_links.begin(), faction_links.end());
for(const std::string& link : faction_links) {
text << font::unicode_bullet << " " << link << "\n";
}
const std::string ref_id = ".." + era_prefix + era["id"].str();
topics.emplace_back(ref_id, era["name"], text.str());
}
return topics;
}
} // namespace help

View file

@ -0,0 +1,31 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "help/topic.hpp"
class config;
namespace help
{
topic_list generate_ability_topics(bool sort_generated);
topic_list generate_weapon_special_topics(bool sort_generated);
topic_list generate_time_of_day_topics(bool sort_generated);
topic_list generate_trait_topics(bool sort_generated);
topic_list generate_unit_topics(bool sort_generated, const std::string& race);
topic_list generate_faction_topics(bool sort_generated, const config& era);
topic_list generate_era_topics(bool sort_generated, const std::string& era_id);
} // namespace help

View file

@ -0,0 +1,866 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-help"
#include "help/topic_text_generators.hpp"
#include "font/text_formatting.hpp"
#include "formatter.hpp"
#include "game_config.hpp" // for debug, menu_contract, etc
#include "gettext.hpp" // for _, gettext, N_
#include "language.hpp" // for string_table, symbol_table
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "movetype.hpp" // for movetype, movetype::effects, etc
#include "preferences/game.hpp" // for encountered_terrains, etc
#include "terrain/terrain.hpp" // for terrain_type
#include "terrain/translation.hpp" // for operator==, ter_list, etc
#include "terrain/type_data.hpp" // for terrain_type_data, etc
#include "tstring.hpp" // for t_string, operator<<
#include "units/helper.hpp" // for resistance_color
#include "units/race.hpp" // for unit_race, etc
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "video.hpp" // fore current_resolution
#include "help/constants.hpp"
#include "help/utils.hpp"
#include <boost/optional.hpp> // for optional
#include <iostream> // for operator<<, basic_ostream, etc
#include <map> // for map, etc
#include <set>
static lg::log_domain log_help("help");
#define WRN_HP LOG_STREAM(warn, log_help)
namespace help
{
namespace
{
std::string yes_no_str(bool value)
{
// TODO: do we want these translated?
#if 0
return value
? translation::dgettext("wesnoth", "Yes")
: translation::dgettext("wesnoth", "No");
#endif
return value ? "Yes" : "No";
}
std::string best_str(bool best)
{
if(best) {
return formatter() << font::span_color(font::GOOD_COLOR) << _("Best of") << "</span>";
} else {
return formatter() << font::span_color(font::BAD_COLOR) << _("Worst of") << "</span>";
}
}
using ter_iter = t_translation::ter_list::const_iterator;
// Gets an english description of a terrain ter_list alias behavior:
// "Best of cave, hills", "Worst of Swamp, Forest" etc.
std::string print_behavior_description(
ter_iter start, ter_iter end, const ter_data_cache& tdata, bool first_level = true, bool begin_best = true)
{
if(start == end) {
return "";
}
// absorb any leading mode changes by calling again, with a new default value begin_best.
if(*start == t_translation::MINUS || *start == t_translation::PLUS) {
return print_behavior_description(start + 1, end, tdata, first_level, *start == t_translation::PLUS);
}
boost::optional<ter_iter> last_change_pos;
bool best = begin_best;
for(ter_iter i = start; i != end; ++i) {
if((best && *i == t_translation::MINUS) || (!best && *i == t_translation::PLUS)) {
best = !best;
last_change_pos = i;
}
}
std::ostringstream ss;
if(!last_change_pos) {
std::vector<std::string> names;
for(ter_iter i = start; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i);
if(!tt.editor_name().empty()) {
names.push_back(tt.editor_name());
}
}
if(names.empty()) {
return "";
}
if(names.size() == 1) {
return names.at(0);
}
ss << best_str(best) << " ";
if(!first_level) {
ss << "( ";
}
ss << utils::join(names, ", ");
if(!first_level) {
ss << " )";
}
} else {
std::vector<std::string> names;
for(ter_iter i = *last_change_pos + 1; i != end; ++i) {
const terrain_type tt = tdata->get_terrain_info(*i);
if(!tt.editor_name().empty()) {
names.push_back(tt.editor_name());
}
}
// This alias list is apparently padded with junk at the end, so truncate it without adding more parens.
if(names.empty()) {
return print_behavior_description(start, *last_change_pos, tdata, first_level, begin_best);
}
ss << best_str(best) << " ";
if(!first_level) {
ss << "( ";
}
ss << print_behavior_description(start, *last_change_pos - 1, tdata, false, begin_best);
// Printed the (parenthesized) leading part from before the change, now print the remaining names in this group.
for(const std::string& s : names) {
ss << ", " << s;
}
if(!first_level) {
ss << " )";
}
}
return ss.str();
}
} // end anon namespace
//
// TERRAIN TOPIC GENERATOR =========================================================================
//
std::string terrain_topic_generator::generate() const
{
std::ostringstream ss;
if(!type_.icon_image().empty()) {
ss << "<img>src='images/buttons/icon-base-32.png~RC(magenta>" << type_.id() << ")~BLIT("
<< "terrain/" << type_.icon_image() << "_30.png)"
<< "'</img> ";
}
if(!type_.editor_image().empty()) {
ss << "<img>src='" << type_.editor_image() << "'</img> ";
}
if(!type_.help_topic_text().empty()) {
ss << "\n\n" << type_.help_topic_text().str() << "\n";
} else {
ss << "\n";
}
ter_data_cache tdata = load_terrain_types_data();
if(!tdata) {
WRN_HP << "When building terrain help topics, we couldn't acquire any terrain types data\n";
return ss.str();
}
if(!(type_.union_type().size() == 1 && type_.union_type()[0] == type_.number() && type_.is_nonnull())) {
const t_translation::ter_list& underlying_mvt_terrains = tdata->underlying_mvt_terrain(type_.number());
ss << "\n" << _("Base Terrain: ");
bool first = true;
for(const t_translation::terrain_code& underlying_terrain : underlying_mvt_terrains) {
const terrain_type& mvt_base = tdata->get_terrain_info(underlying_terrain);
if(mvt_base.editor_name().empty()) {
continue;
}
if(!first) {
ss << ", ";
} else {
first = false;
}
ss << make_link(mvt_base.editor_name(), ".." + terrain_prefix + mvt_base.id());
}
ss << "\n";
ss << "\n" << _("Movement properties: ");
ss << print_behavior_description(underlying_mvt_terrains.begin(), underlying_mvt_terrains.end(), tdata) << "\n";
const t_translation::ter_list& underlying_def_terrains = tdata->underlying_def_terrain(type_.number());
ss << "\n" << _("Defense properties: ");
ss << print_behavior_description(underlying_def_terrains.begin(), underlying_def_terrains.end(), tdata) << "\n";
}
if(game_config::debug) {
ss << "\n";
ss << "ID: " << type_.id() << "\n";
ss << "Village: " << yes_no_str(type_.is_village()) << "\n";
ss << "Gives Healing: " << type_.gives_healing() << "\n";
ss << "Keep: " << yes_no_str(type_.is_keep()) << "\n";
ss << "Castle: " << yes_no_str(type_.is_castle()) << "\n";
ss << "Overlay: " << yes_no_str(type_.is_overlay()) << "\n";
ss << "Combined: " << yes_no_str(type_.is_combined()) << "\n";
ss << "Terrain string: " << type_.number() << "\n";
ss << "Hide in Editor: " << yes_no_str(type_.hide_in_editor()) << "\n";
ss << "Hide Help: " << yes_no_str(type_.hide_help()) << "\n";
ss << "Editor Group: " << type_.editor_group() << "\n";
ss << "Light Bonus: " << type_.light_bonus(0) << "\n";
ss << type_.income_description();
if(type_.editor_image().empty()) { // Note: this is purely temporary to help make a different help entry
ss << "\nEditor Image: Empty\n";
} else {
ss << "\nEditor Image: " << type_.editor_image() << "\n";
}
ss << "\nDebug Mvt Description String:";
for(const t_translation::terrain_code& t : tdata->underlying_mvt_terrain(type_.number())) {
ss << " " << t;
}
ss << "\nDebug Def Description String:";
for(const t_translation::terrain_code& t : tdata->underlying_def_terrain(type_.number())) {
ss << " " << t;
}
}
return ss.str();
}
//
// UNIT TOPIC GENERATOR =========================================================================
//
// Typedef to help with formatting list of traits
using trait_data = std::pair<std::string, std::string>;
#if 0
//Helper function for printing a list of trait data
static void print_trait_list(std::ostringstream & ss, const std::vector<trait_data> & l)
{
std::size_t i = 0;
ss << make_link(l[i].first, l[i].second);
// This doesn't skip traits with empty names
for(i++; i < l.size(); i++) {
ss << ", " << make_link(l[i].first,l[i].second);
}
}
#endif
std::string unit_topic_generator::generate() const
{
#if 0
// Force the lazy loading to build this unit.
unit_types.build_unit_type(type_, unit_type::FULL);
std::ostringstream ss;
std::string clear_stringstream;
const std::string detailed_description = type_.unit_description();
const unit_type& female_type = type_.get_gender_unit_type(unit_race::FEMALE);
const unit_type& male_type = type_.get_gender_unit_type(unit_race::MALE);
const int screen_width = CVideo::get_singleton().get_width();
ss << _("Level") << " " << type_.level();
ss << "\n\n";
ss << "<img>src='" << male_type.image();
ss << "~RC(" << male_type.flag_rgb() << ">red)";
if(screen_width >= 1200)
ss << "~XBRZ(2)";
ss << "' box='no'</img> ";
if(&female_type != &male_type) {
ss << "<img>src='" << female_type.image();
ss << "~RC(" << female_type.flag_rgb() << ">red)";
if(screen_width >= 1200)
ss << "~XBRZ(2)";
ss << "' box='no'</img> ";
}
const std::string& male_portrait
= male_type.small_profile().empty() ? male_type.big_profile() : male_type.small_profile();
const std::string& female_portrait
= female_type.small_profile().empty() ? female_type.big_profile() : female_type.small_profile();
const bool has_male_portrait
= !male_portrait.empty() && male_portrait != male_type.image() && male_portrait != "unit_image";
const bool has_female_portrait = !female_portrait.empty() && female_portrait != male_portrait
&& female_portrait != female_type.image() && female_portrait != "unit_image";
int sz = (has_male_portrait && has_female_portrait ? 300 : 400);
if(screen_width <= 1366) {
sz = (has_male_portrait && has_female_portrait ? 200 : 300);
} else if(screen_width >= 1920) {
sz = 400;
}
// TODO: figure out why the second checks don't match but the last does
if(has_male_portrait) {
ss << "<img>src='" << male_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz
<< ")' box='no' align='right' float='yes'</img> ";
}
if(has_female_portrait) {
ss << "<img>src='" << female_portrait << "~FL(horiz)~SCALE_INTO(" << sz << ',' << sz
<< ")' box='no' align='right' float='yes'</img> ";
}
ss << "\n\n\n";
// Print cross-references to units that this unit advances from/to.
// Cross reference to the topics containing information about those units.
const bool first_reverse_value = true;
bool reverse = first_reverse_value;
if(variation_.empty()) {
do {
std::vector<std::string> adv_units = reverse ? type_.advances_from() : type_.advances_to();
bool first = true;
for(const std::string& adv : adv_units) {
const unit_type* type = unit_types.find(adv, unit_type::HELP_INDEXED);
if(!type || type->hide_help()) {
continue;
}
if(first) {
if(reverse) {
ss << _("Advances from: ");
} else {
ss << _("Advances to: ");
}
first = false;
} else {
ss << ", ";
}
std::string lang_unit = type->type_name();
std::string ref_id;
if(description_type(*type) == FULL_DESCRIPTION) {
const std::string section_prefix = type->show_variations_in_help() ? ".." : "";
ref_id = section_prefix + unit_prefix + type->id();
} else {
ref_id = unknown_unit_topic;
lang_unit += " (?)";
}
ss << make_link(lang_unit, ref_id);
}
if(!first) {
ss << "\n";
}
reverse = !reverse; // switch direction
} while(reverse != first_reverse_value); // don't restart
}
const unit_type* parent = variation_.empty() ? &type_ : unit_types.find(type_.id(), unit_type::HELP_INDEXED);
if(!variation_.empty()) {
ss << _("Base unit: ") << make_link(parent->type_name(), ".." + unit_prefix + type_.id()) << "\n";
} else {
bool first = true;
for(const std::string& base_id : utils::split(type_.get_cfg()["base_ids"])) {
if(first) {
ss << _("Base units: ");
first = false;
}
const unit_type* base_type = unit_types.find(base_id, unit_type::HELP_INDEXED);
const std::string section_prefix = base_type->show_variations_in_help() ? ".." : "";
ss << make_link(base_type->type_name(), section_prefix + unit_prefix + base_id) << "\n";
}
}
bool first = true;
for(const std::string& var_id : parent->variations()) {
const unit_type& type = parent->get_variation(var_id);
if(type.hide_help()) {
continue;
}
if(first) {
ss << _("Variations: ");
first = false;
} else {
ss << ", ";
}
std::string ref_id;
std::string var_name = type.variation_name();
if(description_type(type) == FULL_DESCRIPTION) {
ref_id = variation_prefix + type.id() + "_" + var_id;
} else {
ref_id = unknown_unit_topic;
var_name += " (?)";
}
ss << make_link(var_name, ref_id);
}
ss << "\n"; // added even if empty, to avoid shifting
// Print the race of the unit, cross-reference it to the respective topic.
const std::string race_id = type_.race_id();
std::string race_name = type_.race()->plural_name();
if(race_name.empty()) {
race_name = _("race^Miscellaneous");
}
ss << _("Race: ");
ss << make_link(race_name, "..race_" + race_id);
ss << "\n\n";
// Print the possible traits of the unit, cross-reference them
// to their respective topics.
if(config::const_child_itors traits = type_.possible_traits()) {
std::vector<trait_data> must_have_traits;
std::vector<trait_data> random_traits;
int must_have_nameless_traits = 0;
for(const config& trait : traits) {
const std::string trait_name = trait["male_name"];
std::string lang_trait_name = translation::gettext(trait_name.c_str());
if(lang_trait_name.empty() && trait["availability"].str() == "musthave") {
++must_have_nameless_traits;
continue;
}
const std::string ref_id = "traits_" + trait["id"].str();
((trait["availability"].str() == "musthave") ? must_have_traits : random_traits)
.emplace_back(lang_trait_name, ref_id);
}
bool line1 = !must_have_traits.empty();
bool line2 = !random_traits.empty() && type_.num_traits() > must_have_traits.size();
if(line1) {
std::string traits_label = _("Traits");
ss << traits_label;
if(line2) {
std::ostringstream must_have_count;
must_have_count << " (" << must_have_traits.size() << ") : ";
std::ostringstream random_count;
random_count << " (" << (type_.num_traits() - must_have_traits.size() - must_have_nameless_traits)
<< ") : ";
int second_line_whitespace = font::line_width(traits_label + must_have_count.str(), normal_font_size)
- font::line_width(random_count.str(), normal_font_size);
// This ensures that the second line is justified so that the ':' characters are aligned.
ss << must_have_count.str();
print_trait_list(ss, must_have_traits);
ss << "\n" << jump(second_line_whitespace) << random_count.str();
print_trait_list(ss, random_traits);
} else {
ss << ": ";
print_trait_list(ss, must_have_traits);
}
ss << "\n\n";
} else {
if(line2) {
ss << _("Traits") << " (" << (type_.num_traits() - must_have_nameless_traits) << ") : ";
print_trait_list(ss, random_traits);
ss << "\n\n";
}
}
}
// Print the abilities the units has, cross-reference them
// to their respective topics. TODO: Update this according to musthave trait effects, similar to movetype below
if(!type_.abilities_metadata().empty()) {
ss << _("Abilities: ");
for(auto iter = type_.abilities_metadata().begin(); iter != type_.abilities_metadata().end(); ++iter) {
const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
std::string lang_ability = translation::gettext(iter->name.c_str());
ss << make_link(lang_ability, ref_id);
if(std::next(iter) != type_.abilities_metadata().end()) {
ss << ", ";
}
}
ss << "\n\n";
}
// Print the extra AMLA upgrade abilities, cross-reference them to their respective topics.
if(!type_.adv_abilities_metadata().empty()) {
ss << _("Ability Upgrades: ");
for(auto iter = type_.adv_abilities_metadata().begin(); iter != type_.adv_abilities_metadata().end(); ++iter) {
const std::string ref_id = ability_prefix + iter->id + iter->name.base_str();
std::string lang_ability = translation::gettext(iter->name.c_str());
ss << make_link(lang_ability, ref_id);
if(std::next(iter) != type_.adv_abilities_metadata().end()) {
ss << ", ";
}
}
ss << "\n\n";
}
// Print some basic information such as HP and movement points.
// TODO: Make this info update according to musthave traits, similar to movetype below.
ss << _("HP: ") << type_.hitpoints() << jump(30) << _("Moves: ") << type_.movement() << jump(30);
if(type_.vision() != type_.movement()) {
ss << _("Vision: ") << type_.vision() << jump(30);
}
if(type_.jamming() > 0) {
ss << _("Jamming: ") << type_.jamming() << jump(30);
}
ss << _("Cost: ") << type_.cost() << jump(30) << _("Alignment: ")
<< make_link(type_.alignment_description(type_.alignment(), type_.genders().front()), "time_of_day") << jump(30);
if(type_.can_advance()) {
ss << _("Required XP: ") << type_.experience_needed();
}
// Print the detailed description about the unit.
ss << "\n\n" << detailed_description;
// Print the different attacks a unit has, if it has any.
if(!type_.attacks().empty()) {
// Print headers for the table.
ss << "\n\n<header>text='" << escape(_("unit help^Attacks")) << "'</header>\n\n";
table_spec table;
std::vector<item> first_row;
// Dummy element, icons are below.
first_row.push_back(item("", 0));
push_header(first_row, _("unit help^Name"));
push_header(first_row, _("Type"));
push_header(first_row, _("Strikes"));
push_header(first_row, _("Range"));
push_header(first_row, _("Special"));
table.push_back(first_row);
// Print information about every attack.
for(const attack_type& attack : type_.attacks()) {
std::string lang_weapon = attack.name();
std::string lang_type = string_table["type_" + attack.type()];
std::vector<item> row;
std::ostringstream attack_ss;
attack_ss << "<img>src='" << attack.icon() << "'</img>";
row.emplace_back(attack_ss.str(), image_width(attack.icon()));
push_tab_pair(row, lang_weapon);
push_tab_pair(row, lang_type);
attack_ss.str(clear_stringstream);
attack_ss << attack.damage() << font::unicode_en_dash << attack.num_attacks() << " "
<< attack.accuracy_parry_description();
push_tab_pair(row, attack_ss.str());
attack_ss.str(clear_stringstream);
if(attack.min_range() > 1 || attack.max_range() > 1) {
attack_ss << attack.min_range() << "-" << attack.max_range() << ' ';
}
attack_ss << string_table["range_" + attack.range()];
push_tab_pair(row, attack_ss.str());
attack_ss.str(clear_stringstream);
// Show this attack's special, if it has any. Cross
// reference it to the section describing the special.
std::vector<std::pair<t_string, t_string>> specials = attack.special_tooltips();
if(!specials.empty()) {
std::string lang_special = "";
const std::size_t specials_size = specials.size();
for(std::size_t i = 0; i != specials_size; ++i) {
const std::string ref_id = std::string("weaponspecial_") + specials[i].first.base_str();
lang_special = (specials[i].first);
attack_ss << make_link(lang_special, ref_id);
if(i + 1 != specials_size) {
attack_ss << ", "; // comma placed before next special
}
}
row.emplace_back(attack_ss.str(), font::line_width(lang_special, normal_font_size));
}
table.push_back(row);
}
ss << generate_table(table);
}
// Generate the movement type of the unit, with resistance, defense, movement, jamming and vision data updated
// according to any 'musthave' traits which always apply
movetype movement_type = type_.movement_type();
config::const_child_itors traits = type_.possible_traits();
if(!traits.empty() && type_.num_traits() > 0) {
for(const config& t : traits) {
if(t["availability"].str() == "musthave") {
for(const config& effect : t.child_range("effect")) {
if(!effect.child("filter") // If this is musthave but has a unit filter, it might not always apply,
// so don't apply it in the help.
&& movetype::effects.find(effect["apply_to"].str()) != movetype::effects.end()) {
movement_type.merge(effect, effect["replace"].to_bool());
}
}
}
}
}
// Print the resistance table of the unit.
ss << "\n\n<header>text='" << escape(_("Resistances")) << "'</header>\n\n";
table_spec resistance_table;
std::vector<item> first_res_row;
push_header(first_res_row, _("Attack Type"));
push_header(first_res_row, _("Resistance"));
resistance_table.push_back(first_res_row);
utils::string_map dam_tab = movement_type.damage_table();
for(std::pair<std::string, std::string> dam_it : dam_tab) {
std::vector<item> row;
int resistance = 100;
try {
resistance -= std::stoi(dam_it.second);
} catch(std::invalid_argument&) {
}
std::string resist = std::to_string(resistance) + '%';
const std::size_t pos = resist.find('-');
if(pos != std::string::npos) {
resist.replace(pos, 1, font::unicode_minus);
}
std::string color = unit_helper::resistance_color(resistance);
std::string lang_weapon = string_table["type_" + dam_it.first];
push_tab_pair(row, lang_weapon);
std::ostringstream str;
str << "<format>color=\"" << color << "\" text='" << resist << "'</format>";
const std::string markup = str.str();
str.str(clear_stringstream);
str << resist;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
resistance_table.push_back(row);
}
ss << generate_table(resistance_table);
if(ter_data_cache tdata = load_terrain_types_data()) {
// Print the terrain modifier table of the unit.
ss << "\n\n<header>text='" << escape(_("Terrain Modifiers")) << "'</header>\n\n";
std::vector<item> first_row;
table_spec table;
push_header(first_row, _("Terrain"));
push_header(first_row, _("Defense"));
push_header(first_row, _("Movement Cost"));
const bool has_terrain_defense_caps
= movement_type.has_terrain_defense_caps(preferences::encountered_terrains());
if(has_terrain_defense_caps) {
push_header(first_row, _("Defense Cap"));
}
const bool has_vision = type_.movement_type().has_vision_data();
if(has_vision) {
push_header(first_row, _("Vision Cost"));
}
const bool has_jamming = type_.movement_type().has_jamming_data();
if(has_jamming) {
push_header(first_row, _("Jamming Cost"));
}
table.push_back(first_row);
std::set<t_translation::terrain_code>::const_iterator terrain_it = preferences::encountered_terrains().begin();
for(; terrain_it != preferences::encountered_terrains().end(); ++terrain_it) {
const t_translation::terrain_code terrain = *terrain_it;
if(terrain == t_translation::FOGGED || terrain == t_translation::VOID_TERRAIN
|| t_translation::terrain_matches(terrain, t_translation::ALL_OFF_MAP)) {
continue;
}
const terrain_type& info = tdata->get_terrain_info(terrain);
const int moves = movement_type.movement_cost(terrain);
const bool cannot_move = moves > type_.movement();
if(cannot_move && info.hide_if_impassable()) {
continue;
}
if(info.union_type().size() == 1 && info.union_type()[0] == info.number() && info.is_nonnull()) {
std::vector<item> row;
const std::string& name = info.name();
const std::string& id = info.id();
const int views = movement_type.vision_cost(terrain);
const int jams = movement_type.jamming_cost(terrain);
bool high_res = false;
const std::string tc_base
= high_res ? "images/buttons/icon-base-32.png" : "images/buttons/icon-base-16.png";
const std::string terrain_image = "icons/terrain/terrain_type_" + id + (high_res ? "_30.png" : ".png");
const std::string final_image = tc_base + "~RC(magenta>" + id + ")~BLIT(" + terrain_image + ")";
row.emplace_back("<img>src='" + final_image + "'</img> " + make_link(name, "..terrain_" + id),
font::line_width(name, normal_font_size) + (high_res ? 32 : 16));
// defense - range: +10 % .. +70 %
const int defense = 100 - movement_type.defense_modifier(terrain);
std::string color;
if(defense <= 10) {
color = "red";
} else if(defense <= 30) {
color = "yellow";
} else if(defense <= 50) {
color = "white";
} else {
color = "green";
}
std::ostringstream str;
str << "<format>color=" << color << " text='" << defense << "%'</format>";
std::string markup = str.str();
str.str(clear_stringstream);
str << defense << "%";
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
// movement - range: 1 .. 5, movetype::UNREACHABLE=impassable
str.str(clear_stringstream);
if(cannot_move) { // cannot move in this terrain
color = "red";
} else if(moves > 1) {
color = "yellow";
} else {
color = "white";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the movement costs go above
// the unit's max moves + 5, we replace it with dashes.
if(cannot_move && (moves > type_.movement() + 5)) {
str << font::unicode_figure_dash;
} else {
str << moves;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << moves;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
// defense cap
if(has_terrain_defense_caps) {
str.str(clear_stringstream);
const bool has_cap = movement_type.get_defense().capped(terrain);
if(has_cap) {
str << "<format>color='" << color << "' text='" << defense << "%'</format>";
} else {
str << "<format>color=white text='" << font::unicode_figure_dash << "'</format>";
}
markup = str.str();
str.str(clear_stringstream);
if(has_cap) {
str << defense << '%';
} else {
str << font::unicode_figure_dash;
}
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
}
// vision
if(has_vision) {
str.str(clear_stringstream);
const bool cannot_view = views > type_.vision();
if(cannot_view) { // cannot view in this terrain
color = "red";
} else if(views > moves) {
color = "yellow";
} else if(views == moves) {
color = "white";
} else {
color = "green";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the vision costs go above
// the unit's vision + 5, we replace it with dashes.
if(cannot_view && (views > type_.vision() + 5)) {
str << font::unicode_figure_dash;
} else {
str << views;
}
str << "'</format>";
markup = str.str();
str.str(clear_stringstream);
str << views;
row.emplace_back(markup, font::line_width(str.str(), normal_font_size));
}
// jamming
if(has_jamming) {
str.str(clear_stringstream);
const bool cannot_jam = jams > type_.jamming();
if(cannot_jam) { // cannot jamm in this terrain
color = "red";
} else if(jams > views) {
color = "yellow";
} else if(jams == views) {
color = "white";
} else {
color = "green";
}
str << "<format>color=" << color << " text='";
// A 5 MP margin; if the jamming costs go above
// the unit's jamming + 5, we replace it with dashes.
if(cannot_jam && jams > type_.jamming() + 5) {
str << font::unicode_figure_dash;
} else {
str << jams;
}
str << "'</format>";
push_tab_pair(row, str.str());
}
table.push_back(row);
}
}
ss << generate_table(table);
} else {
WRN_HP << "When building unit help topics, the display object was null and we couldn't get the terrain info we "
"need.\n";
}
return ss.str();
#endif
return "";
}
void unit_topic_generator::push_header(std::vector<item>& /*row*/, const std::string& /*name*/) const
{
// row.emplace_back(bold(name), font::line_width(name, normal_font_size, TTF_STYLE_BOLD));
}
} // end namespace help

View file

@ -0,0 +1,93 @@
/*
Copyright (C) 2018 by the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "config.hpp"
#include <memory>
#include <string>
#include <utility>
#include <vector>
class terrain_type;
class unit_type;
namespace help
{
/**
* Abstract topic generator base class.
*/
class topic_text_generator
{
public:
virtual ~topic_text_generator() {}
virtual std::string generate() const = 0;
};
class plain_text_topic_generator : public topic_text_generator
{
public:
explicit plain_text_topic_generator(const std::string& t)
: text_(t)
{
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override
{
return text_;
}
private:
std::string text_;
};
class terrain_topic_generator : public topic_text_generator
{
public:
explicit terrain_topic_generator(const terrain_type& type)
: type_(type)
{
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override;
private:
const terrain_type& type_;
};
class unit_topic_generator : public topic_text_generator
{
public:
unit_topic_generator(const unit_type& t, std::string variation = "")
: type_(t)
, variation_(variation)
{
UNUSED(type_);
}
/** Inherited from @ref topic_text_generator. */
virtual std::string generate() const override;
private:
const unit_type& type_;
const std::string variation_;
using item = std::pair<std::string, unsigned>;
void push_header(std::vector<item>& row, const std::string& name) const;
};
} // namespace help

110
src/help/utils.cpp Normal file
View file

@ -0,0 +1,110 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "help/utils.hpp"
#include "display.hpp"
#include "game_config.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "help/constants.hpp"
#include "help/section.hpp"
#include "help/topic.hpp"
#include "hotkey/hotkey_command.hpp"
#include "map/map.hpp"
#include "preferences/game.hpp"
#include "terrain/type_data.hpp"
#include "units/types.hpp"
#include <iostream>
namespace help
{
/** Implementation helpers. */
namespace
{
} // end anon namespace
bool string_less::operator()(const std::string& s1, const std::string& s2) const
{
return translation::compare(s1, s2) < 0;
}
std::string hidden_symbol(bool hidden)
{
return hidden ? "." : "";
}
bool is_visible_id(const std::string& id)
{
return id.empty() || id[0] != '.';
}
bool is_valid_id(const std::string& id)
{
if(id == "toplevel") {
return false;
}
if(id.compare(0, unit_prefix.length(), unit_prefix) == 0
|| id.compare(hidden_symbol().length(), unit_prefix.length(), unit_prefix) == 0) {
return false;
}
if(id.compare(0, ability_prefix.length(), ability_prefix) == 0) {
return false;
}
if(id.compare(0, weapon_special_prefix.length(), weapon_special_prefix) == 0) {
return false;
}
if(id == "hidden") {
return false;
}
return true;
}
UNIT_DESCRIPTION_TYPE description_type(const unit_type& type)
{
if(game_config::debug || preferences::show_all_units_in_help() || hotkey::is_scope_active(hotkey::SCOPE_EDITOR)) {
return FULL_DESCRIPTION;
}
const std::set<std::string>& encountered_units = preferences::encountered_units();
if(encountered_units.find(type.id()) != encountered_units.end()) {
return FULL_DESCRIPTION;
}
return NO_DESCRIPTION;
}
std::string escape(const std::string& s)
{
return utils::escape(s, "'\\");
}
ter_data_cache load_terrain_types_data()
{
if(display* d = display::get_singleton()) {
return d->get_disp_context().map().tdata();
} else if(game_config_manager* g = game_config_manager::get()) {
return g->terrain_types();
} else {
return ter_data_cache();
}
}
} // namespace help

84
src/help/utils.hpp Normal file
View file

@ -0,0 +1,84 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "exceptions.hpp"
#include "formatter.hpp"
#include <memory>
#include <string>
#include <vector>
class terrain_type_data;
class unit_type;
namespace help
{
/** Thrown when the help manager fails to parse something. */
struct parse_error : public game::error
{
parse_error(const std::string& msg) : game::error(msg) {}
};
/** Thrown when trying to create a sub-section deeper than the max allowed depth. */
struct max_recursion_reached : public game::error
{
max_recursion_reached(const std::string& msg) : game::error(msg) {}
};
struct string_less
{
bool operator()(const std::string& s1, const std::string& s2) const;
};
enum UNIT_DESCRIPTION_TYPE {FULL_DESCRIPTION, NO_DESCRIPTION, NON_REVEALING_DESCRIPTION};
UNIT_DESCRIPTION_TYPE description_type(const unit_type& type);
std::string hidden_symbol(bool hidden = true);
/** An ID beginning with '.' denotes a hidden section or topic. */
bool is_visible_id(const std::string& id);
/// Return true if the id is valid for user defined topics and
/// sections. Some IDs are special, such as toplevel and may not be
/// be defined in the config.
bool is_valid_id(const std::string& id);
/// Prepend all chars with meaning inside attributes with a backslash.
std::string escape(const std::string &s);
inline std::string make_link(const std::string& text, const std::string& dst)
{
// some sorting done on list of links may rely on the fact that text is first
return formatter() << "<ref>text='" << help::escape(text) << "' dst='" << help::escape(dst) << "'</ref>";
}
inline std::string jump_to(const unsigned pos)
{
return formatter() << "<jump>to=" << pos << "</jump>";
}
inline std::string jump(const unsigned amount)
{
return formatter() << "<jump>amount=" << amount << "</jump>";
}
using ter_data_cache = std::shared_ptr<terrain_type_data>;
/// Load the appropriate terrain types data to use
ter_data_cache load_terrain_types_data();
} // namespace help

View file

@ -29,7 +29,6 @@
#include "display.hpp"
#include "quit_confirmation.hpp"
#include "sdl/surface.hpp"
#include "show_dialog.hpp"
#include "../resources.hpp"
#include "../playmp_controller.hpp"

View file

@ -151,8 +151,8 @@ bool teleport_group::allow_vision() const {
config teleport_group::to_config() const {
config retval = cfg_;
retval["saved"] = "yes";
retval["reversed"] = reversed_ ? "yes" : "no";
retval["saved"] = true;
retval["reversed"] = reversed_;
retval["id"] = id_;
return retval;
}

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
/*
Copyright (C) 2006 - 2018 by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
wesnoth playlevel Copyright (C) 2003 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -18,14 +18,13 @@
#include "controller_base.hpp"
#include "floating_label.hpp"
#include "game_end_exceptions.hpp"
#include "help/help.hpp"
#include "game_state.hpp"
#include "hotkey/command_executor.hpp"
#include "menu_events.hpp"
#include "mouse_events.hpp"
#include "persist_manager.hpp"
#include "terrain/type_data.hpp"
#include "tod_manager.hpp"
#include "game_state.hpp"
#include <set>
@ -39,49 +38,73 @@ struct mp_game_settings;
class game_classification;
struct unit_experience_accelerator;
namespace actions {
class undo_list;
namespace actions
{
class undo_list;
}
namespace game_events {
class manager;
class wml_event_pump;
class wml_menu_item;
namespace game_events
{
class manager;
class wml_event_pump;
class wml_menu_item;
} // namespace game_events
namespace soundsource {
class manager;
namespace soundsource
{
class manager;
} // namespace soundsource
namespace statistics {
struct scenario_context;
namespace statistics
{
struct scenario_context;
} // namespace statistics
namespace pathfind {
class manager;
namespace pathfind
{
class manager;
}
namespace tooltips {
struct manager;
namespace tooltips
{
struct manager;
} // namespace tooltips
namespace wb {
class manager; // whiteboard manager
namespace wb
{
class manager; // whiteboard manager
} // namespace wb
namespace gui2
{
namespace dialogs
{
class game_ui;
}
}
// Holds gamestate related objects
class game_state;
class play_controller : public controller_base, public events::observer, public quit_confirmation
{
public:
play_controller(const config& level, saved_game& state_of_game,
const ter_data_cache& tdata, bool skip_replay);
/** Declaration for opaque pointer paradigm. */
class hotkey_handler;
play_controller(const config& level,
saved_game& state_of_game,
const config& game_config,
const ter_data_cache& tdata,
bool skip_replay);
virtual ~play_controller();
//event handler, overridden from observer
//there is nothing to handle in this class actually but that might change in the future
virtual void handle_generic_event(const std::string& /*name*/) override {}
// event handler, overridden from observer
// there is nothing to handle in this class actually but that might change in the future
virtual void handle_generic_event(const std::string& /*name*/) override
{
}
bool can_undo() const;
bool can_redo() const;
@ -96,6 +119,7 @@ public:
void save_replay();
void save_replay_auto(const std::string& filename);
void save_map();
replay& get_replay();
void init_side_begin();
@ -114,7 +138,6 @@ public:
virtual void force_end_turn() = 0;
virtual void check_objectives() = 0;
virtual void on_not_observer() = 0;
/**
@ -124,49 +147,58 @@ public:
*/
virtual void process_oos(const std::string& msg) const;
void set_end_level_data(const end_level_data& data) {
void set_end_level_data(const end_level_data& data)
{
gamestate().end_level_data_ = data;
}
void reset_end_level_data() {
void reset_end_level_data()
{
gamestate().end_level_data_ = boost::none;
}
bool is_regular_game_end() const {
bool is_regular_game_end() const
{
return gamestate().end_level_data_.get_ptr() != nullptr;
}
const end_level_data& get_end_level_data_const() const {
const end_level_data& get_end_level_data_const() const
{
return *gamestate().end_level_data_;
}
const std::vector<team>& get_teams_const() const {
const std::vector<team>& get_teams_const() const
{
return gamestate().board_.teams_;
}
const unit_map& get_units_const() const {
const unit_map& get_units_const() const
{
return gamestate().board_.units();
}
const gamemap& get_map_const() const{
const gamemap& get_map_const() const
{
return gamestate().board_.map();
}
const tod_manager& get_tod_manager_const() const{
return gamestate().tod_manager_;
}
bool is_observer() const {
const tod_manager& get_tod_manager_const() const
{
return gamestate().tod_manager_;
}
bool is_observer() const
{
return gamestate().board_.is_observer();
}
bool do_healing() const {
return gamestate().do_healing_;
}
void set_do_healing(bool do_healing) {
gamestate().do_healing_ = do_healing;
}
game_state& gamestate() {
game_state& gamestate()
{
return *gamestate_;
}
const game_state& gamestate() const {
const game_state& gamestate() const
{
return *gamestate_;
}
@ -177,37 +209,64 @@ public:
*/
void check_victory();
std::size_t turn() const {return gamestate().tod_manager_.turn();}
std::size_t turn() const
{
return gamestate().tod_manager_.turn();
}
/**
* Returns the number of the side whose turn it is.
*
* Numbering starts at one.
*/
int current_side() const { return gamestate_->player_number_; }
int current_side() const
{
return gamestate_->player_number_;
}
/**
* Builds the snapshot config from members and their respective configs.
*/
config to_config() const;
bool is_skipping_replay() const { return skip_replay_; }
void toggle_skipping_replay();
bool is_linger_mode() const { return linger_; }
void do_autosave();
bool is_skipping_replay() const
{
return skip_replay_;
}
bool is_skipping_story() const { return skip_story_; }
void toggle_skipping_replay();
bool is_linger_mode() const
{
return linger_;
}
void do_autosave();
void do_consolesave(const std::string& filename);
events::mouse_handler& get_mouse_handler_base() override;
events::menu_handler& get_menu_handler() { return menu_handler_; }
events::menu_handler& get_menu_handler()
{
return menu_handler_;
}
std::shared_ptr<wb::manager> get_whiteboard() const;
const mp_game_settings& get_mp_settings();
game_classification& get_classification();
int get_server_request_number() const { return gamestate().server_request_number_; }
void increase_server_request_number() { ++gamestate().server_request_number_; }
int get_server_request_number() const
{
return gamestate().server_request_number_;
}
void increase_server_request_number()
{
++gamestate().server_request_number_;
}
game_events::wml_event_pump& pump();
@ -215,16 +274,24 @@ public:
virtual soundsource::manager* get_soundsource_man() override;
virtual plugins_context* get_plugins_context() override;
hotkey::command_executor* get_hotkey_command_executor() override;
actions::undo_list& get_undo_stack() { return undo_stack(); }
actions::undo_list& get_undo_stack()
{
return undo_stack();
}
bool is_browsing() const override;
bool is_lingering() const { return linger_; }
bool is_lingering() const
{
return linger_;
}
class hotkey_handler;
virtual bool is_replay() { return false; }
virtual bool is_replay()
{
return false;
}
t_string get_scenario_name() const
{
@ -248,12 +315,14 @@ public:
void maybe_throw_return_to_play_side() const
{
if(should_return_to_play_side() && !linger_ ) {
if(should_return_to_play_side() && !linger_) {
throw return_to_play_side_exception();
}
}
virtual void play_side_impl() {}
virtual void play_side_impl()
{
}
void play_side();
@ -261,27 +330,45 @@ public:
const team& current_team() const;
bool can_use_synced_wml_menu() const;
std::set<std::string> all_players() const;
int ticks() const { return ticks_; }
int ticks() const
{
return ticks_;
}
game_display& get_display() override;
virtual void initialize_and_show_ui() override;
void update_savegame_snapshot() const;
/**
* Changes the UI for this client to the passed side index.
*/
void update_gui_to_player(const int team_index, const bool observe = false);
virtual bool is_networked_mp() const { return false; }
virtual void send_to_wesnothd(const config&, const std::string& = "unknown") const { }
virtual bool receive_from_wesnothd(config&) const { return false; }
/// Reevaluate [show_if] conditions and build a new objectives string.
void refresh_objectives() const;
virtual bool is_networked_mp() const
{
return false;
}
virtual void send_to_wesnothd(const config&, const std::string& = "unknown") const
{
}
virtual bool receive_from_wesnothd(config&) const
{
return false;
}
void show_objectives() const;
struct scoped_savegame_snapshot
{
scoped_savegame_snapshot(const play_controller& controller);
~scoped_savegame_snapshot();
const play_controller& controller_;
};
@ -289,28 +376,30 @@ public:
protected:
friend struct scoped_savegame_snapshot;
void play_slice_catch();
bool have_keyboard_focus() override;
void process_focus_keydown_event(const SDL_Event& event) override;
void process_keydown_event(const SDL_Event& event) override;
void process_keyup_event(const SDL_Event& event) override;
void init_managers();
///preload events cannot be synced
/// preload events cannot be synced
void fire_preload();
void fire_prestart();
void fire_start();
void start_game();
virtual void init_gui();
void finish_side_turn();
void finish_turn(); //this should not throw an end turn or end level exception
void finish_turn(); // this should not throw an end turn or end level exception
bool enemies_visible() const;
void enter_textbox();
void tab();
bool is_team_visible(int team_num, bool observer) const;
/// returns 0 if no such team was found.
int find_last_visible_team() const;
@ -318,58 +407,70 @@ private:
const int ticks_;
protected:
//gamestate
// gamestate
const ter_data_cache& tdata_;
std::unique_ptr<game_state> gamestate_;
config level_;
saved_game& saved_game_;
//managers
// managers
std::unique_ptr<tooltips::manager> tooltips_manager_;
//whiteboard manager
// whiteboard manager
std::shared_ptr<wb::manager> whiteboard_manager_;
//plugins context
// plugins context
std::unique_ptr<plugins_context> plugins_context_;
//more managers
// more managers
font::floating_label_context labels_manager_;
help::help_manager help_manager_;
events::mouse_handler mouse_handler_;
events::menu_handler menu_handler_;
std::unique_ptr<hotkey_handler> hotkey_handler_;
std::unique_ptr<soundsource::manager> soundsources_manager_;
persist_manager persist_;
//other objects
// other objects
std::unique_ptr<game_display> gui_;
std::unique_ptr<gui2::dialogs::game_ui> ui_; // TODO: better name?
const std::unique_ptr<unit_experience_accelerator> xp_mod_;
const std::unique_ptr<const statistics::scenario_context> statistics_context_;
actions::undo_list& undo_stack() { return *gamestate().undo_stack_; }
const actions::undo_list& undo_stack() const { return *gamestate().undo_stack_; }
actions::undo_list& undo_stack()
{
return *gamestate().undo_stack_;
}
const actions::undo_list& undo_stack() const
{
return *gamestate().undo_stack_;
}
std::unique_ptr<replay> replay_;
bool skip_replay_;
bool skip_story_;
bool linger_;
/**
* Whether we did init sides in this session
* (false = we did init sides before we reloaded the game).
*/
bool init_side_done_now_;
//the displayed location when we load a game.
// the displayed location when we load a game.
map_location map_start_;
const std::string& select_music(bool victory) const;
void reset_gamestate(const config& level, int replay_pos);
private:
void init(const config& level);
bool victory_when_enemies_defeated_;
bool remove_from_carryover_on_defeat_;
std::vector<std::string> victory_music_;
std::vector<std::string> defeat_music_;
@ -377,9 +478,16 @@ private:
protected:
mutable bool ignore_replay_errors_;
bool player_type_changed_;
virtual void sync_end_turn() {}
virtual void sync_end_turn()
{
}
virtual void check_time_over();
virtual void update_viewing_player() = 0;
void play_turn();
};

View file

@ -1,367 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "show_dialog.hpp"
#include "floating_label.hpp"
#include "font/sdl_ttf.hpp"
#include "picture.hpp"
#include "gettext.hpp"
#include "gui/core/event/handler.hpp"
#include "help/help.hpp"
#include "hotkey/command_executor.hpp"
#include "log.hpp"
#include "font/marked-up_text.hpp"
#include "font/standard_colors.hpp"
#include "sdl/rect.hpp"
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
#define ERR_G LOG_STREAM(err, lg::general)
namespace {
bool is_in_dialog = false;
}
namespace gui {
//static initialization
const int ButtonHPadding = 10;
const int ButtonVPadding = 10;
//note: style names are directly related to the panel image file names
const dialog_frame::style dialog_frame::default_style("opaque", 0);
const dialog_frame::style dialog_frame::message_style("translucent65", 3);
const dialog_frame::style dialog_frame::preview_style("../dialogs/selection", 0);
const dialog_frame::style dialog_frame::titlescreen_style("translucent54", 1);
const int dialog_frame::title_border_w = 10;
const int dialog_frame::title_border_h = 5;
bool in_dialog()
{
return is_in_dialog || gui2::is_in_dialog();
}
dialog_manager::dialog_manager() : cursor::setter(cursor::NORMAL), reset_to(is_in_dialog)
{
is_in_dialog = true;
}
dialog_manager::~dialog_manager()
{
is_in_dialog = reset_to;
int mousex, mousey;
SDL_GetMouseState(&mousex, &mousey);
SDL_Event pb_event;
pb_event.type = SDL_MOUSEMOTION;
pb_event.motion.state = 0;
pb_event.motion.x = mousex;
pb_event.motion.y = mousey;
pb_event.motion.xrel = 0;
pb_event.motion.yrel = 0;
SDL_PushEvent(&pb_event);
}
dialog_frame::dialog_frame(CVideo& video, const std::string& title,
const style& style, bool auto_restore,
std::vector<button*>* buttons, button* help_button) :
title_(title),
video_(video),
dialog_style_(style),
buttons_(buttons),
help_button_(help_button),
restorer_(nullptr),
auto_restore_(auto_restore),
dim_(),
top_(image::get_image("dialogs/" + dialog_style_.panel + "-border-top.png")),
bot_(image::get_image("dialogs/" + dialog_style_.panel + "-border-bottom.png")),
left_(image::get_image("dialogs/" + dialog_style_.panel + "-border-left.png")),
right_(image::get_image("dialogs/" + dialog_style_.panel + "-border-right.png")),
top_left_(image::get_image("dialogs/" + dialog_style_.panel + "-border-topleft.png")),
bot_left_(image::get_image("dialogs/" + dialog_style_.panel + "-border-botleft.png")),
top_right_(image::get_image("dialogs/" + dialog_style_.panel + "-border-topright.png")),
bot_right_(image::get_image("dialogs/" + dialog_style_.panel + "-border-botright.png")),
bg_(image::get_image("dialogs/" + dialog_style_.panel + "-background.png")),
have_border_(top_ != nullptr && bot_ != nullptr && left_ != nullptr && right_ != nullptr),
dirty_(true)
{
}
dialog_frame::~dialog_frame()
{
delete restorer_;
}
dialog_frame::dimension_measurements::dimension_measurements() :
interior(sdl::empty_rect), exterior(sdl::empty_rect), title(sdl::empty_rect), button_row(sdl::empty_rect)
{}
dialog_frame::dimension_measurements dialog_frame::layout(const SDL_Rect& rect) {
return layout(rect.x, rect.y, rect.w, rect.h);
}
int dialog_frame::top_padding() const {
int padding = 0;
if(have_border_) {
padding += top_->h;
}
if(!title_.empty()) {
padding += font::get_max_height(font::SIZE_TITLE) + 2*dialog_frame::title_border_h;
}
return padding;
}
void dialog_frame::set_dirty(bool dirty) {
dirty_ = dirty;
}
void dialog_frame::handle_window_event(const SDL_Event& event) {
if (event.type == SDL_WINDOWEVENT) {
switch (event.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_RESTORED:
case SDL_WINDOWEVENT_SHOWN:
case SDL_WINDOWEVENT_EXPOSED:
set_dirty();
}
}
}
void dialog_frame::handle_event(const SDL_Event& event) {
if (event.type == DRAW_ALL_EVENT) {
set_dirty();
if (buttons_) {
for(std::vector<button *>::iterator it = buttons_->begin(); it != buttons_->end(); ++it) {
(*it)->set_dirty(true);
}
}
}
if (event.type == DRAW_EVENT || event.type == DRAW_ALL_EVENT) {
draw();
}
}
int dialog_frame::bottom_padding() const {
int padding = 0;
if(buttons_ != nullptr) {
for(std::vector<button*>::const_iterator b = buttons_->begin(); b != buttons_->end(); ++b) {
padding = std::max<int>((**b).height() + ButtonVPadding, padding);
}
}
if(have_border_) {
padding += bot_->h;
}
return padding;
}
dialog_frame::dimension_measurements dialog_frame::layout(int x, int y, int w, int h) {
dim_ = dimension_measurements();
if(!title_.empty()) {
dim_.title = draw_title(nullptr);
dim_.title.w += title_border_w;
}
if(buttons_ != nullptr) {
for(std::vector<button*>::const_iterator b = buttons_->begin(); b != buttons_->end(); ++b) {
dim_.button_row.w += (**b).width() + ButtonHPadding;
dim_.button_row.h = std::max<int>((**b).height() + ButtonVPadding,dim_.button_row.h);
}
dim_.button_row.x = -dim_.button_row.w;
dim_.button_row.y = y + h;
dim_.button_row.w += ButtonHPadding;
}
std::size_t buttons_width = dim_.button_row.w;
if(help_button_ != nullptr) {
buttons_width += help_button_->width() + ButtonHPadding*2;
dim_.button_row.y = y + h;
}
y -= dim_.title.h;
w = std::max(w, std::max(dim_.title.w, static_cast<int>(buttons_width)));
h += dim_.title.h + dim_.button_row.h;
dim_.button_row.x += x + w;
SDL_Rect bounds = video_.screen_area();
if(have_border_) {
bounds.x += left_->w;
bounds.y += top_->h;
bounds.w -= left_->w;
bounds.h -= top_->h;
}
if(x < bounds.x) {
w += x;
x = bounds.x;
}
if(y < bounds.y) {
h += y;
y = bounds.y;
}
if(x > bounds.w) {
w = 0;
} else if(x + w > bounds.w) {
w = bounds.w - x;
}
if(y > bounds.h) {
h = 0;
} else if(y + h > bounds.h) {
h = bounds.h - y;
}
dim_.interior.x = x;
dim_.interior.y = y;
dim_.interior.w = w;
dim_.interior.h = h;
if(have_border_) {
dim_.exterior.x = dim_.interior.x - left_->w;
dim_.exterior.y = dim_.interior.y - top_->h;
dim_.exterior.w = dim_.interior.w + left_->w + right_->w;
dim_.exterior.h = dim_.interior.h + top_->h + bot_->h;
} else {
dim_.exterior = dim_.interior;
}
dim_.title.x = dim_.interior.x + title_border_w;
dim_.title.y = dim_.interior.y + title_border_h;
return dim_;
}
void dialog_frame::draw_border()
{
if(have_border_ == false) {
return;
}
surface top_image(scale_surface(top_, dim_.interior.w, top_->h));
if(top_image != nullptr) {
video_.blit_surface(dim_.interior.x, dim_.exterior.y, top_image);
}
surface bot_image(scale_surface(bot_, dim_.interior.w, bot_->h));
if(bot_image != nullptr) {
video_.blit_surface(dim_.interior.x, dim_.interior.y + dim_.interior.h, bot_image);
}
surface left_image(scale_surface(left_, left_->w, dim_.interior.h));
if(left_image != nullptr) {
video_.blit_surface(dim_.exterior.x, dim_.interior.y, left_image);
}
surface right_image(scale_surface(right_, right_->w, dim_.interior.h));
if(right_image != nullptr) {
video_.blit_surface(dim_.interior.x + dim_.interior.w, dim_.interior.y, right_image);
}
if(top_left_ == nullptr || bot_left_ == nullptr || top_right_ == nullptr || bot_right_ == nullptr) {
return;
}
video_.blit_surface(dim_.interior.x - left_->w, dim_.interior.y - top_->h, top_left_);
video_.blit_surface(dim_.interior.x - left_->w, dim_.interior.y + dim_.interior.h + bot_->h - bot_left_->h, bot_left_);
video_.blit_surface(dim_.interior.x + dim_.interior.w + right_->w - top_right_->w, dim_.interior.y - top_->h, top_right_);
video_.blit_surface(dim_.interior.x + dim_.interior.w + right_->w - bot_right_->w, dim_.interior.y + dim_.interior.h + bot_->h - bot_right_->h, bot_right_);
}
void dialog_frame::clear_background()
{
delete restorer_;
restorer_ = nullptr;
}
void dialog_frame::draw_background()
{
if(auto_restore_) {
clear_background();
restorer_ = new surface_restorer(&video_, dim_.exterior);
}
if (dialog_style_.blur_radius) {
surface surf = ::get_surface_portion(video_.getSurface(), dim_.exterior);
surf = blur_surface(surf, dialog_style_.blur_radius);
sdl_blit(surf, nullptr, video_.getSurface(), &dim_.exterior);
}
if(bg_ == nullptr) {
ERR_DP << "could not find dialog background '" << dialog_style_.panel << "'" << std::endl;
return;
}
for(int i = 0; i < dim_.interior.w; i += bg_->w) {
for(int j = 0; j < dim_.interior.h; j += bg_->h) {
SDL_Rect src {0,0,0,0};
src.w = std::min(dim_.interior.w - i, bg_->w);
src.h = std::min(dim_.interior.h - j, bg_->h);
SDL_Rect dst = src;
dst.x = dim_.interior.x + i;
dst.y = dim_.interior.y + j;
sdl_blit(bg_, &src, video_.getSurface(), &dst);
}
}
}
SDL_Rect dialog_frame::draw_title(CVideo* video)
{
SDL_Rect rect = CVideo::get_singleton().screen_area();
return font::draw_text(video, rect, font::SIZE_TITLE, font::TITLE_COLOR,
title_, dim_.title.x, dim_.title.y, false, TTF_STYLE_NORMAL);
}
void dialog_frame::draw()
{
if (!dirty_)
return;
//draw background
draw_background();
//draw frame border
draw_border();
//draw title
if (!title_.empty()) {
draw_title(&video_);
}
//draw buttons
SDL_Rect buttons_area = dim_.button_row;
if(buttons_ != nullptr) {
#ifdef OK_BUTTON_ON_RIGHT
std::reverse(buttons_->begin(),buttons_->end());
#endif
for(std::vector<button*>::const_iterator b = buttons_->begin(); b != buttons_->end(); ++b) {
(**b).set_location(buttons_area.x, buttons_area.y);
buttons_area.x += (**b).width() + ButtonHPadding;
}
}
if(help_button_ != nullptr) {
help_button_->set_location(dim_.interior.x+ButtonHPadding, buttons_area.y);
}
dirty_ = false;
}
}

View file

@ -1,115 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
class surface;
#include "cursor.hpp"
#include "floating_label.hpp"
#include "tooltips.hpp"
#include "video.hpp"
#include "widgets/button.hpp"
namespace gui
{
extern const int ButtonHPadding;
extern const int ButtonVPadding;
enum DIALOG_RESULT {
DIALOG_BACK=-7,
DIALOG_FORWARD=-6,
CREATE_ITEM =-5,
DELETE_ITEM=-4,
ESCAPE_DIALOG=-3, //special return used by WML event dialogs
CONTINUE_DIALOG=-2,
CLOSE_DIALOG=-1
/* results (0..N) reserved for standard button indices */
};
bool in_dialog();
struct dialog_manager : private cursor::setter, private font::floating_label_context {
dialog_manager();
~dialog_manager();
private:
bool reset_to;
};
class dialog_frame :public video2::draw_layering {
public:
struct dimension_measurements {
dimension_measurements();
SDL_Rect interior, exterior, title, button_row;
};
class style {
public:
style(const std::string& p, int br) : panel(p), blur_radius(br) {}
std::string panel;
int blur_radius;
};
//Static members
static const int title_border_w, title_border_h;
static const style default_style;
static const style message_style;
static const style preview_style;
static const style titlescreen_style;
dialog_frame(CVideo &video, const std::string& title="",
const style& dialog_style=default_style,
bool auto_restore=true, std::vector<button*>* buttons=nullptr,
button* help_button=nullptr);
~dialog_frame();
dimension_measurements layout(int x, int y, int w, int h);
dimension_measurements layout(const SDL_Rect& frame_area);
void set_layout(dimension_measurements &new_dim) { dim_ = new_dim; }
dimension_measurements get_layout() const { return dim_; }
int top_padding() const;
int bottom_padding() const;
void draw();
//called by draw
void draw_border();
void draw_background();
//also called by layout with null param
SDL_Rect draw_title(CVideo *video);
void set_dirty(bool dirty = true);
virtual void handle_event(const SDL_Event&);
void handle_window_event(const SDL_Event& event);
private:
void clear_background();
std::string title_;
CVideo &video_;
const style& dialog_style_;
std::vector<button*>* buttons_;
button* help_button_;
surface_restorer* restorer_;
bool auto_restore_;
dimension_measurements dim_;
surface top_, bot_, left_, right_, top_left_, bot_left_, top_right_, bot_right_, bg_;
bool have_border_;
bool dirty_;
};
}

View file

@ -618,7 +618,7 @@ config write_stats()
void write_stats(config_writer &out)
{
out.write_key_val("mid_scenario", mid_scenario ? "yes" : "no");
out.write_key_val("mid_scenario", mid_scenario);
for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
out.open_child("scenario");

View file

@ -1,592 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/button.hpp"
#include "filesystem.hpp"
#include "font/sdl_ttf.hpp"
#include "game_config.hpp"
#include "game_errors.hpp"
#include "picture.hpp"
#include "log.hpp"
#include "font/marked-up_text.hpp"
#include "font/standard_colors.hpp"
#include "sdl/rect.hpp"
#include "serialization/string_utils.hpp"
#include "sound.hpp"
#include "video.hpp"
#include "wml_separators.hpp"
#include <boost/algorithm/string/predicate.hpp>
static lg::log_domain log_display("display");
#define ERR_DP LOG_STREAM(err, log_display)
namespace gui {
const int font_size = font::SIZE_NORMAL;
const int horizontal_padding = font::SIZE_SMALL;
const int checkbox_horizontal_padding = font::SIZE_SMALL / 2;
const int vertical_padding = font::SIZE_SMALL / 2;
button::button(CVideo& video, const std::string& label, button::TYPE type,
std::string button_image_name, SPACE_CONSUMPTION spacing,
const bool auto_join, std::string overlay_image)
: widget(video, auto_join), type_(type),
label_text_(label),
image_(nullptr), pressedImage_(nullptr), activeImage_(nullptr), pressedActiveImage_(nullptr),
disabledImage_(nullptr), pressedDisabledImage_(nullptr),
overlayImage_(nullptr), overlayPressedImage_(nullptr), overlayActiveImage_(nullptr),
state_(NORMAL), pressed_(false),
spacing_(spacing), base_height_(0), base_width_(0),
button_image_name_(), button_overlay_image_name_(overlay_image),
button_image_path_suffix_()
{
if (button_image_name.empty()) {
switch (type_) {
case TYPE_PRESS:
button_image_name_ = "buttons/button_normal/button_H22";
break;
case TYPE_TURBO:
button_image_name_ = "buttons/button_menu/menu_button_copper_H20";
break;
case TYPE_CHECK:
button_image_name_ = "buttons/checkbox";
break;
case TYPE_RADIO:
button_image_name_ = "buttons/radiobox";
break;
default:
break;
}
} else {
button_image_name_ = "buttons/" + button_image_name;
}
load_images();
}
void button::load_images() {
std::string size_postfix;
switch (location().h) {
case 25:
size_postfix = "_25";
break;
case 30:
size_postfix = "_30";
break;
case 60:
size_postfix = "_60";
break;
default:
break;
}
surface button_image(image::get_image(button_image_name_ + ".png" + button_image_path_suffix_));
surface pressed_image(image::get_image(button_image_name_ + "-pressed.png"+ button_image_path_suffix_));
surface active_image(image::get_image(button_image_name_ + "-active.png"+ button_image_path_suffix_));
surface disabled_image;
if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + "-disabled.png"))
disabled_image.assign((image::get_image(button_image_name_ + "-disabled.png"+ button_image_path_suffix_)));
surface pressed_disabled_image, pressed_active_image, touched_image;
if (!button_overlay_image_name_.empty()) {
if (button_overlay_image_name_.length() > size_postfix.length() &&
boost::algorithm::ends_with(button_overlay_image_name_, size_postfix)) {
button_overlay_image_name_.resize(button_overlay_image_name_.length() - size_postfix.length());
}
overlayImage_.assign(image::get_image(button_overlay_image_name_ + size_postfix + ".png"+ button_image_path_suffix_));
overlayPressedImage_.assign(image::get_image(button_overlay_image_name_ + size_postfix + "-pressed.png"+ button_image_path_suffix_));
if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-active.png"))
overlayActiveImage_.assign(image::get_image(button_overlay_image_name_ + size_postfix + "-active.png"+ button_image_path_suffix_));
if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled.png"))
overlayDisabledImage_.assign(image::get_image(button_overlay_image_name_ + size_postfix + "-disabled.png"+ button_image_path_suffix_));
if (overlayDisabledImage_.null())
overlayDisabledImage_ = image::get_image(button_overlay_image_name_ + size_postfix + ".png~GS()" + button_image_path_suffix_);
if (filesystem::file_exists(game_config::path + "/images/" + button_overlay_image_name_ + size_postfix + "-disabled-pressed.png"))
overlayPressedDisabledImage_.assign(image::get_image(button_overlay_image_name_ + size_postfix + "-disabled-pressed.png"+ button_image_path_suffix_));
if (overlayPressedDisabledImage_.null())
overlayPressedDisabledImage_ = image::get_image(button_overlay_image_name_ + size_postfix + "-pressed.png~GS()"+ button_image_path_suffix_);
} else {
overlayImage_.assign(nullptr);
}
if (disabled_image == nullptr) {
disabled_image = image::get_image(button_image_name_ + ".png~GS()" + button_image_path_suffix_);
}
if (pressed_image.null())
pressed_image.assign(button_image);
if (active_image.null())
active_image.assign(button_image);
if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
touched_image.assign(image::get_image(button_image_name_ + "-touched.png"+ button_image_path_suffix_));
if (touched_image.null())
touched_image.assign(pressed_image);
pressed_active_image.assign(image::get_image(button_image_name_ + "-active-pressed.png"+ button_image_path_suffix_));
if (pressed_active_image.null())
pressed_active_image.assign(pressed_image);
if (filesystem::file_exists(game_config::path + "/images/" + button_image_name_ + size_postfix + "-disabled-pressed.png"))
pressed_disabled_image.assign(image::get_image(button_image_name_ + "-disabled-pressed.png"+ button_image_path_suffix_));
if (pressed_disabled_image.null())
pressed_disabled_image = image::get_image(button_image_name_ + "-pressed.png~GS()"+ button_image_path_suffix_);
}
if (button_image.null()) {
std::string err_msg = "error initializing button images! file name: ";
err_msg += button_image_name_;
err_msg += ".png";
ERR_DP << err_msg << std::endl;
throw game::error(err_msg);
}
base_height_ = button_image->h;
base_width_ = button_image->w;
if (type_ != TYPE_IMAGE) {
set_label(label_text_);
}
if(type_ == TYPE_PRESS || type_ == TYPE_TURBO) {
image_.assign(scale_surface(button_image,location().w,location().h));
pressedImage_.assign(scale_surface(pressed_image,location().w,location().h));
activeImage_.assign(scale_surface(active_image,location().w,location().h));
disabledImage_.assign(scale_surface(disabled_image,location().w,location().h));
} else {
image_.assign(scale_surface(button_image,button_image->w,button_image->h));
activeImage_.assign(scale_surface(active_image,button_image->w,button_image->h));
disabledImage_.assign(scale_surface(disabled_image,button_image->w,button_image->h));
pressedImage_.assign(scale_surface(pressed_image,button_image->w,button_image->h));
if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
pressedDisabledImage_.assign(scale_surface(pressed_disabled_image,button_image->w,button_image->h));
pressedActiveImage_.assign(scale_surface(pressed_active_image, button_image->w, button_image->h));
touchedImage_.assign(scale_surface(touched_image, button_image->w, button_image->h));
}
}
if (type_ == TYPE_IMAGE){
calculate_size();
}
}
button::~button()
{
}
void button::calculate_size()
{
if (type_ == TYPE_IMAGE){
SDL_Rect loc_image = location();
loc_image.h = image_->h;
loc_image.w = image_->w;
set_location(loc_image);
return;
}
const SDL_Rect& loc = location();
bool change_size = loc.h == 0 || loc.w == 0;
if (!change_size) {
unsigned w = loc.w - (type_ == TYPE_PRESS || type_ == TYPE_TURBO ? horizontal_padding : checkbox_horizontal_padding + base_width_);
if (type_ != TYPE_IMAGE)
{
int fs = font_size;
int style = TTF_STYLE_NORMAL;
std::string::const_iterator i_beg = label_text_.begin(), i_end = label_text_.end(),
i = font::parse_markup(i_beg, i_end, &fs, nullptr, &style);
if (i != i_end) {
std::string tmp(i, i_end);
label_text_.erase(i - i_beg, i_end - i_beg);
label_text_ += font::make_text_ellipsis(tmp, fs, w, style);
}
}
}
if (type_ != TYPE_IMAGE){
textRect_ = font::draw_text(nullptr, video().screen_area(), font_size,
font::BUTTON_COLOR, label_text_, 0, 0);
}
// TODO: There's a weird text clipping bug, allowing the code below to run fixes it.
// The proper fix should possibly be in the draw_contents() function.
#if 0
if (!change_size)
return;
#endif
set_height(std::max(textRect_.h+vertical_padding,base_height_));
if(type_ == TYPE_PRESS || type_ == TYPE_TURBO) {
if(spacing_ == MINIMUM_SPACE) {
set_width(textRect_.w + horizontal_padding);
} else {
set_width(std::max(textRect_.w+horizontal_padding,base_width_));
}
} else {
if(label_text_.empty()) {
set_width(base_width_);
} else {
set_width(checkbox_horizontal_padding + textRect_.w + base_width_);
}
}
}
void button::set_check(bool check)
{
if (type_ != TYPE_CHECK && type_ != TYPE_RADIO && type_ != TYPE_IMAGE)
return;
STATE new_state;
if (check) {
new_state = (state_ == ACTIVE || state_ == PRESSED_ACTIVE)? PRESSED_ACTIVE : PRESSED;
} else {
new_state = (state_ == ACTIVE || state_ == PRESSED_ACTIVE)? ACTIVE : NORMAL;
}
if (state_ != new_state) {
state_ = new_state;
set_dirty();
}
}
void button::set_active(bool active)
{
if ((state_ == NORMAL) && active) {
state_ = ACTIVE;
set_dirty();
} else if ((state_ == ACTIVE) && !active) {
state_ = NORMAL;
set_dirty();
}
}
bool button::checked() const
{
return state_ == PRESSED || state_ == PRESSED_ACTIVE || state_ == TOUCHED_PRESSED;
}
void button::enable(bool new_val)
{
if(new_val != enabled())
{
pressed_ = false;
// check buttons should keep their state
if(type_ != TYPE_CHECK) {
state_ = NORMAL;
}
widget::enable(new_val);
}
}
void button::draw_contents()
{
surface image = image_;
const int image_w = image_->w;
int offset = 0;
switch(state_) {
case ACTIVE:
image = activeImage_;
break;
case PRESSED:
image = pressedImage_;
if (type_ == TYPE_PRESS)
offset = 1;
break;
case PRESSED_ACTIVE:
image = pressedActiveImage_;
break;
case TOUCHED_NORMAL:
case TOUCHED_PRESSED:
image = touchedImage_;
break;
default:
break;
}
const SDL_Rect& loc = location();
SDL_Rect clipArea = loc;
const int texty = loc.y + loc.h / 2 - textRect_.h / 2 + offset;
int textx;
if (type_ != TYPE_CHECK && type_ != TYPE_RADIO && type_ != TYPE_IMAGE)
textx = loc.x + image->w / 2 - textRect_.w / 2 + offset;
else {
clipArea.w += image_w + checkbox_horizontal_padding;
textx = loc.x + image_w + checkbox_horizontal_padding / 2;
}
color_t button_color = font::BUTTON_COLOR;
if (!enabled()) {
if (state_ == PRESSED || state_ == PRESSED_ACTIVE)
image = pressedDisabledImage_;
else image = disabledImage_;
button_color = font::GRAY_COLOR;
}
if (!overlayImage_.null()) {
surface noverlay = make_neutral_surface(
enabled() ? overlayImage_ : overlayDisabledImage_);
if (!overlayPressedImage_.null()) {
switch (state_) {
case ACTIVE:
if (!overlayActiveImage_.null())
noverlay = make_neutral_surface(overlayActiveImage_);
break;
case PRESSED:
case PRESSED_ACTIVE:
case TOUCHED_NORMAL:
case TOUCHED_PRESSED:
noverlay = make_neutral_surface( enabled() ?
overlayPressedImage_ : overlayPressedDisabledImage_);
break;
default:
break;
}
}
surface nimage = make_neutral_surface(image);
sdl_blit(noverlay, nullptr, nimage, nullptr);
image = nimage;
}
video().blit_surface(loc.x, loc.y, image);
if (type_ != TYPE_IMAGE){
clipArea.x += offset;
clipArea.y += offset;
clipArea.w -= 2*offset;
clipArea.h -= 2*offset;
font::draw_text(&video(), clipArea, font_size, button_color, label_text_, textx, texty);
}
}
bool button::hit(int x, int y) const
{
return sdl::point_in_rect(x,y,location());
}
static bool is_valid_image(const std::string& str) { return !str.empty() && str[0] != IMAGE_PREFIX; }
void button::set_image(const std::string& image_file)
{
if(!is_valid_image(image_file)) {
return;
}
button_image_name_ = "buttons/" + image_file;
load_images();
set_dirty();
}
void button::set_overlay(const std::string& image_file)
{
// We allow empty paths for overlays
if(image_file[0] == IMAGE_PREFIX) {
return;
}
button_overlay_image_name_ = image_file;
load_images();
set_dirty();
}
void button::set_label(const std::string& val)
{
label_text_ = val;
//if we have a list of items, use the first one that isn't an image
if (std::find(label_text_.begin(), label_text_.end(), COLUMN_SEPARATOR) != label_text_.end()) {
const std::vector<std::string>& items = utils::split(label_text_, COLUMN_SEPARATOR);
const std::vector<std::string>::const_iterator i = std::find_if(items.begin(),items.end(),is_valid_image);
if(i != items.end()) {
label_text_ = *i;
}
}
calculate_size();
set_dirty(true);
}
void button::mouse_motion(const SDL_MouseMotionEvent& event)
{
if (hit(event.x, event.y)) {
// the cursor is over the widget
if (state_ == NORMAL)
state_ = ACTIVE;
else if (state_ == PRESSED && (type_ == TYPE_CHECK || type_ == TYPE_RADIO))
state_ = PRESSED_ACTIVE;
} else {
// the cursor is not over the widget
if (type_ == TYPE_CHECK || type_ == TYPE_RADIO) {
switch (state_) {
case TOUCHED_NORMAL:
state_ = NORMAL;
break;
case TOUCHED_PRESSED:
state_ = PRESSED;
break;
case PRESSED_ACTIVE:
state_ = PRESSED;
break;
case ACTIVE:
state_ = NORMAL;
break;
default:
break;
}
} else if ((type_ != TYPE_IMAGE) || state_ != PRESSED)
state_ = NORMAL;
}
}
void button::mouse_down(const SDL_MouseButtonEvent& event)
{
if (hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT) {
switch (type_) {
case TYPE_RADIO:
case TYPE_CHECK:
if (state_ == PRESSED_ACTIVE)
state_ = TOUCHED_PRESSED;
else if (state_ == ACTIVE)
state_ = TOUCHED_NORMAL;
break;
case TYPE_TURBO:
sound::play_UI_sound(game_config::sounds::button_press);
state_ = PRESSED;
break;
default:
state_ = PRESSED;
break;
}
}
}
void button::release(){
state_ = NORMAL;
draw_contents();
}
void button::mouse_up(const SDL_MouseButtonEvent& event)
{
if (!(hit(event.x, event.y) && event.button == SDL_BUTTON_LEFT))
return;
// the user has stopped pressing the mouse left button while on the widget
switch (type_) {
case TYPE_CHECK:
switch (state_) {
case TOUCHED_NORMAL:
state_ = PRESSED_ACTIVE;
pressed_ = true;
break;
case TOUCHED_PRESSED:
state_ = ACTIVE;
pressed_ = true;
break;
default:
break;
}
if (pressed_) sound::play_UI_sound(game_config::sounds::checkbox_release);
break;
case TYPE_RADIO:
if (state_ == TOUCHED_NORMAL || state_ == TOUCHED_PRESSED) {
state_ = PRESSED_ACTIVE;
pressed_ = true;
// exit(0);
sound::play_UI_sound(game_config::sounds::checkbox_release);
}
//} else if (state_ == TOUCHED_PRESSED) {
// state_ = PRESSED_ACTIVE;
//}
break;
case TYPE_PRESS:
if (state_ == PRESSED) {
state_ = ACTIVE;
pressed_ = true;
sound::play_UI_sound(game_config::sounds::button_press);
}
break;
case TYPE_TURBO:
state_ = ACTIVE;
break;
case TYPE_IMAGE:
pressed_ = true;
sound::play_UI_sound(game_config::sounds::button_press);
break;
}
}
void button::handle_event(const SDL_Event& event)
{
gui::widget::handle_event(event);
if (hidden() || !enabled())
return;
STATE start_state = state_;
if (!mouse_locked())
{
switch(event.type) {
case SDL_MOUSEBUTTONDOWN:
mouse_down(event.button);
break;
case SDL_MOUSEBUTTONUP:
mouse_up(event.button);
break;
case SDL_MOUSEMOTION:
mouse_motion(event.motion);
break;
default:
return;
}
}
if (start_state != state_)
set_dirty(true);
}
bool button::pressed()
{
if (type_ != TYPE_TURBO) {
const bool res = pressed_;
pressed_ = false;
return res;
} else
return state_ == PRESSED || state_ == PRESSED_ACTIVE || state_ == TOUCHED_PRESSED;
}
}

View file

@ -1,97 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "widget.hpp"
#include "exceptions.hpp"
namespace gui {
class button : public widget
{
public:
struct error : public game::error {
error()
: game::error("GUI1 button error")
{}
};
enum TYPE { TYPE_PRESS, TYPE_CHECK, TYPE_TURBO, TYPE_IMAGE, TYPE_RADIO };
TYPE get_type() const { return type_; }
enum SPACE_CONSUMPTION { DEFAULT_SPACE, MINIMUM_SPACE };
button(CVideo& video, const std::string& label, TYPE type=TYPE_PRESS,
std::string button_image="", SPACE_CONSUMPTION spacing=DEFAULT_SPACE,
const bool auto_join=true, std::string overlay_image="");
/** Default implementation, but defined out-of-line for efficiency reasons. */
virtual ~button();
void set_check(bool check);
void set_active(bool active);
bool checked() const;
void set_label(const std::string& val);
void set_image(const std::string& image_file_base);
void set_overlay(const std::string& image_file_base);
void set_image_path_suffix(const std::string& suffix) { button_image_path_suffix_ = suffix; load_images(); }
bool pressed();
bool hit(int x, int y) const;
virtual void enable(bool new_val=true);
void release();
protected:
virtual void handle_event(const SDL_Event& event);
virtual void mouse_motion(const SDL_MouseMotionEvent& event);
virtual void mouse_down(const SDL_MouseButtonEvent& event);
virtual void mouse_up(const SDL_MouseButtonEvent& event);
virtual void draw_contents();
TYPE type_;
private:
void load_images();
void calculate_size();
std::string label_text_;
surface image_, pressedImage_, activeImage_, pressedActiveImage_,
touchedImage_, disabledImage_, pressedDisabledImage_,
overlayImage_, overlayPressedImage_, overlayPressedDisabledImage_, overlayDisabledImage_,
overlayActiveImage_;
SDL_Rect textRect_;
enum STATE { UNINIT, NORMAL, ACTIVE, PRESSED, PRESSED_ACTIVE, TOUCHED_NORMAL, TOUCHED_PRESSED };
STATE state_;
bool pressed_;
SPACE_CONSUMPTION spacing_;
int base_height_, base_width_;
std::string button_image_name_;
std::string button_overlay_image_name_;
std::string button_image_path_suffix_;
}; //end class button
}

File diff suppressed because it is too large Load diff

View file

@ -1,317 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include <map>
#include <set>
#include "scrollarea.hpp"
namespace image{
class locator;
}
namespace gui {
class menu : public scrollarea
{
public:
enum ROW_TYPE { NORMAL_ROW, SELECTED_ROW, HEADING_ROW };
//basic menu style
class style
{
public:
style();
virtual ~style();
virtual void init() {}
virtual SDL_Rect item_size(const std::string& item) const;
virtual void draw_row_bg(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
virtual void draw_row(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
void scale_images(int max_width, int max_height);
surface get_item_image(const image::locator &i_locator) const;
std::size_t get_font_size() const;
std::size_t get_cell_padding() const;
std::size_t get_thickness() const;
protected:
std::size_t font_size_;
std::size_t cell_padding_;
std::size_t thickness_; //additional cell padding for style use only
int normal_rgb_, selected_rgb_, heading_rgb_;
double normal_alpha_, selected_alpha_, heading_alpha_;
int max_img_w_, max_img_h_;
};
//image-border selection style
class imgsel_style : public style
{
public:
imgsel_style(const std::string &img_base, bool has_bg,
int normal_rgb, int selected_rgb, int heading_rgb,
double normal_alpha, double selected_alpha, double heading_alpha);
virtual ~imgsel_style();
virtual SDL_Rect item_size(const std::string& item) const;
virtual void draw_row_bg(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
virtual void draw_row(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
virtual void init() { load_images(); }
bool load_images();
protected:
const std::string img_base_;
std::map<std::string,surface> img_map_;
private:
bool load_image(const std::string &img_sub);
bool has_background_;
bool initialized_;
bool load_failed_;
int normal_rgb2_, selected_rgb2_, heading_rgb2_;
double normal_alpha2_, selected_alpha2_, heading_alpha2_;
//FIXME: why is this better than a plain surface?
struct bg_cache
{
bg_cache() : surf(), width(-1), height(-1)
{}
surface surf;
int width, height;
};
bg_cache bg_cache_;
};
friend class style;
friend class imgsel_style;
static style &default_style;
static style simple_style;
static imgsel_style bluebg_style;
struct item
{
item() : fields(), help(), id(0)
{}
item(const std::vector<std::string>& fields, std::size_t id)
: fields(fields), help(), id(id)
{}
std::vector<std::string> fields;
std::vector<std::string> help;
std::size_t id;
};
class sorter
{
public:
virtual ~sorter() {}
virtual bool column_sortable(int column) const = 0;
virtual bool less(int column, const item& row1, const item& row2) const = 0;
};
class basic_sorter : public sorter
{
public:
basic_sorter();
virtual ~basic_sorter() {}
basic_sorter& set_alpha_sort(int column);
basic_sorter& set_numeric_sort(int column);
basic_sorter& set_xp_sort(int column);
basic_sorter& set_level_sort(int level_column, int xp_column);
basic_sorter& set_id_sort(int column);
basic_sorter& set_redirect_sort(int column, int to);
basic_sorter& set_position_sort(int column, const std::vector<int>& pos);
protected:
virtual bool column_sortable(int column) const;
virtual bool less(int column, const item& row1, const item& row2) const;
private:
std::set<int> alpha_sort_, numeric_sort_, id_sort_, xp_sort_, level_sort_;
std::map<int,int> redirect_sort_;
std::map<int,std::vector<int>> pos_sort_;
int xp_col_; //used by level sort
};
menu(CVideo& video, const std::vector<std::string>& items,
bool click_selects=false, int max_height=-1, int max_width=-1,
const sorter* sorter_obj=nullptr, style *menu_style=nullptr, const bool auto_join=true);
/** Default implementation, but defined out-of-line for efficiency reasons. */
~menu();
int selection() const;
void move_selection(std::size_t id);
void move_selection_keeping_viewport(std::size_t id);
void reset_selection();
// allows user to change_item while running (dangerous)
void change_item(int pos1,int pos2,const std::string& str);
virtual void erase_item(std::size_t index);
void set_heading(const std::vector<std::string>& heading);
/// Set new items to show and redraw/recalculate everything. If
/// strip_spaces is false, spaces will remain at the item edges. If
/// keep_viewport is true, the menu tries to keep the selection at
/// the same position as it were before the items were set.
virtual void set_items(const std::vector<std::string>& items, bool strip_spaces=true,
bool keep_viewport=false);
/// Set a new max height for this menu. Note that this does not take
/// effect immediately, only after certain operations that clear
/// everything, such as set_items().
void set_max_height(const int new_max_height);
void set_max_width(const int new_max_width);
int get_max_height() const { return max_height_; }
int get_max_width() const { return max_width_; }
std::size_t number_of_items() const { return items_.size(); }
int process();
bool double_clicked();
void set_click_selects(bool value);
void set_numeric_keypress_selection(bool value);
void scroll(unsigned int pos);
//currently, menus do not manage the memory of their sorter
//this should be changed to a more object-oriented approach
void set_sorter(sorter *s);
void sort_by(int column);
int get_sort_by() const {return sortby_;}
bool get_sort_reversed() const {return sortreversed_;}
protected:
bool item_ends_with_image(const std::string& item) const;
virtual void handle_event(const SDL_Event& event);
void set_inner_location(const SDL_Rect& rect);
bool requires_event_focus(const SDL_Event *event=nullptr) const;
const std::vector<int>& column_widths() const;
virtual void draw_row(const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type);
style *style_;
bool silent_;
int hit(int x, int y) const;
std::pair<int,int> hit_cell(int x, int y) const;
int hit_column(int x) const;
int hit_heading(int x, int y) const;
void invalidate_row(std::size_t id);
void invalidate_row_pos(std::size_t pos);
void invalidate_heading();
private:
std::size_t max_items_onscreen() const;
std::size_t heading_height() const;
int max_height_, max_width_;
mutable int max_items_, item_height_;
void adjust_viewport_to_selection();
void key_press(SDL_Keycode key);
std::vector<item> items_;
std::vector<std::size_t> item_pos_;
std::vector<std::string> heading_;
mutable int heading_height_;
void create_help_strings();
void process_help_string(int mousex, int mousey);
std::pair<int,int> cur_help_;
int help_string_;
mutable std::vector<int> column_widths_;
std::size_t selected_;
bool click_selects_;
bool out_;
bool previous_button_;
//std::set<std::size_t> undrawn_items_;
bool show_result_;
bool double_clicked_;
void column_widths_item(const std::vector<std::string>& row, std::vector<int>& widths) const;
void clear_item(int item);
void draw_contents();
void draw();
mutable std::map<int,SDL_Rect> itemRects_;
SDL_Rect get_item_rect(int item) const;
SDL_Rect get_item_rect_internal(std::size_t pos) const;
std::size_t get_item_height_internal(const std::vector<std::string>& item) const;
std::size_t get_item_height(int item) const;
int items_start() const;
int items_end() const;
int items_height() const;
void update_scrollbar_grip_height();
///variable which determines whether a numeric keypress should
///select an item on the dialog
bool num_selects_;
// These two variables are used to get the correct double click
// behavior so that a click that causes one double click won't be
// counted as a first click in the "next" double click.
bool ignore_next_doubleclick_;
bool last_was_doubleclick_;
//ellipsis calculation is slightly off, so default to false
bool use_ellipsis_;
const sorter* sorter_;
int sortby_;
bool sortreversed_;
int highlight_heading_;
/// Set new items to show. If strip_spaces is false, spaces will
/// remain at the item edges.
void fill_items(const std::vector<std::string>& items, bool strip_spaces);
void do_sort();
void recalculate_pos();
void assert_pos();
void update_size();
enum SELECTION_MOVE_VIEWPORT { MOVE_VIEWPORT, NO_MOVE_VIEWPORT };
void set_selection_pos(std::size_t pos, bool silent=false, SELECTION_MOVE_VIEWPORT move_viewport=MOVE_VIEWPORT);
void move_selection_to(std::size_t id, bool silent=false, SELECTION_MOVE_VIEWPORT move_viewport=MOVE_VIEWPORT);
void move_selection_up(std::size_t dep);
void move_selection_down(std::size_t dep);
std::set<int> invalid_;
};
}

View file

@ -1,243 +0,0 @@
/*
wesnoth menu styles Copyright (C) 2006 - 2018 by Patrick Parker <patrick_x99@hotmail.com>
wesnoth menu Copyright (C) 2003-5 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/menu.hpp"
#include "font/constants.hpp"
#include "picture.hpp"
#include "lexical_cast.hpp"
#include "sdl/utils.hpp"
#include "video.hpp"
namespace gui {
//static initializations
menu::imgsel_style menu::bluebg_style("dialogs/selection", true,
0x000000, 0x000000, 0x333333,
0.35, 0.0, 0.3);
menu::style menu::simple_style;
menu::style &menu::default_style = menu::bluebg_style;
//constructors
menu::style::style() : font_size_(font::SIZE_NORMAL),
cell_padding_(font::SIZE_NORMAL * 3/5), thickness_(0),
normal_rgb_(0x000000), selected_rgb_(0x000099), heading_rgb_(0x333333),
normal_alpha_(0.2), selected_alpha_(0.6), heading_alpha_(0.3),
max_img_w_(-1), max_img_h_(-1)
{}
menu::style::~style()
{}
menu::imgsel_style::imgsel_style(const std::string &img_base, bool has_bg,
int normal_rgb, int selected_rgb, int heading_rgb,
double normal_alpha, double selected_alpha, double heading_alpha)
: img_base_(img_base), has_background_(has_bg), initialized_(false), load_failed_(false),
normal_rgb2_(normal_rgb), selected_rgb2_(selected_rgb), heading_rgb2_(heading_rgb),
normal_alpha2_(normal_alpha), selected_alpha2_(selected_alpha), heading_alpha2_(heading_alpha)
{}
menu::imgsel_style::~imgsel_style()
{}
std::size_t menu::style::get_font_size() const { return font_size_; }
std::size_t menu::style::get_cell_padding() const { return cell_padding_; }
std::size_t menu::style::get_thickness() const { return thickness_; }
void menu::style::scale_images(int max_width, int max_height)
{
max_img_w_ = max_width;
max_img_h_ = max_height;
}
surface menu::style::get_item_image(const image::locator& img_loc) const
{
surface surf = image::get_image(img_loc);
if(!surf.null())
{
int scale = 100;
if(max_img_w_ > 0 && surf->w > max_img_w_) {
scale = (max_img_w_ * 100) / surf->w;
}
if(max_img_h_ > 0 && surf->h > max_img_h_) {
scale = std::min<int>(scale, ((max_img_h_ * 100) / surf->h));
}
if(scale != 100)
{
return scale_surface(surf, (scale * surf->w)/100, (scale * surf->h)/100);
}
}
return surf;
}
bool menu::imgsel_style::load_image(const std::string &img_sub)
{
std::string path = img_base_ + "-" + img_sub + ".png";
const surface image = image::get_image(path);
img_map_[img_sub] = image;
return(!image.null());
}
bool menu::imgsel_style::load_images()
{
if(!initialized_)
{
if( load_image("border-botleft")
&& load_image("border-botright")
&& load_image("border-topleft")
&& load_image("border-topright")
&& load_image("border-left")
&& load_image("border-right")
&& load_image("border-top")
&& load_image("border-bottom") )
{
thickness_ = std::min(
img_map_["border-top"]->h,
img_map_["border-left"]->w);
if(has_background_ && !load_image("background"))
{
load_failed_ = true;
}
else
{
normal_rgb_ = normal_rgb2_;
normal_alpha_ = normal_alpha2_;
selected_rgb_ = selected_rgb2_;
selected_alpha_ = selected_alpha2_;
heading_rgb_ = heading_rgb2_;
heading_alpha_ = heading_alpha2_;
load_failed_ = false;
}
initialized_ = true;
}
else
{
thickness_ = 0;
initialized_ = true;
load_failed_ = true;
}
}
return (!load_failed_);
}
void menu::imgsel_style::draw_row_bg(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type)
{
if(type == SELECTED_ROW && has_background_ && !load_failed_) {
if(bg_cache_.width != rect.w || bg_cache_.height != rect.h)
{
//draw scaled background image
//scale image each time (to prevent loss of quality)
bg_cache_.surf = scale_surface(img_map_["background"], rect.w, rect.h);
bg_cache_.width = rect.w;
bg_cache_.height = rect.h;
}
SDL_Rect clip = rect;
menu_ref.video().blit_surface(rect.x,rect.y,bg_cache_.surf,nullptr,&clip);
}
else {
style::draw_row_bg(menu_ref, row_index, rect, type);
}
}
void menu::imgsel_style::draw_row(menu& menu_ref, const std::size_t row_index, const SDL_Rect& rect, ROW_TYPE type)
{
if(!load_failed_) {
//draw item inside
style::draw_row(menu_ref, row_index, rect, type);
if(type == SELECTED_ROW) {
// draw border
surface image;
SDL_Rect area;
SDL_Rect clip = rect;
area.x = rect.x;
area.y = rect.y;
image = img_map_["border-top"];
area.x = rect.x;
area.y = rect.y;
do {
menu_ref.video().blit_surface(area.x,area.y,image,nullptr,&clip);
area.x += image->w;
} while( area.x < rect.x + rect.w );
image = img_map_["border-left"];
area.x = rect.x;
area.y = rect.y;
do {
menu_ref.video().blit_surface(area.x,area.y,image,nullptr,&clip);
area.y += image->h;
} while( area.y < rect.y + rect.h );
image = img_map_["border-right"];
area.x = rect.x + rect.w - thickness_;
area.y = rect.y;
do {
menu_ref.video().blit_surface(area.x,area.y,image,nullptr,&clip);
area.y += image->h;
} while( area.y < rect.y + rect.h );
image = img_map_["border-bottom"];
area.x = rect.x;
area.y = rect.y + rect.h - thickness_;
do {
menu_ref.video().blit_surface(area.x,area.y,image,nullptr,&clip);
area.x += image->w;
} while( area.x < rect.x + rect.w );
image = img_map_["border-topleft"];
area.x = rect.x;
area.y = rect.y;
menu_ref.video().blit_surface(area.x,area.y,image);
image = img_map_["border-topright"];
area.x = rect.x + rect.w - image->w;
area.y = rect.y;
menu_ref.video().blit_surface(area.x,area.y,image);
image = img_map_["border-botleft"];
area.x = rect.x;
area.y = rect.y + rect.h - image->h;
menu_ref.video().blit_surface(area.x,area.y,image);
image = img_map_["border-botright"];
area.x = rect.x + rect.w - image->w;
area.y = rect.y + rect.h - image->h;
menu_ref.video().blit_surface(area.x,area.y,image);
}
}
else {
//default drawing
style::draw_row(menu_ref, row_index, rect, type);
}
}
SDL_Rect menu::imgsel_style::item_size(const std::string& item) const
{
SDL_Rect bounds = style::item_size(item);
bounds.w += 2 * thickness_;
bounds.h += 2 * thickness_;
return bounds;
}
} //namesapce gui

View file

@ -1,171 +0,0 @@
/*
Copyright (C) 2004 - 2018 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/** @file */
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/scrollarea.hpp"
#include "sdl/rect.hpp"
namespace gui {
scrollarea::scrollarea(CVideo &video, const bool auto_join)
: widget(video, auto_join), scrollbar_(video),
old_position_(0), recursive_(false), shown_scrollbar_(false),
shown_size_(0), full_size_(0)
{
scrollbar_.hide(true);
}
bool scrollarea::has_scrollbar() const
{
return shown_size_ < full_size_ && scrollbar_.is_valid_height(location().h);
}
sdl_handler_vector scrollarea::handler_members()
{
sdl_handler_vector h;
h.push_back(&scrollbar_);
return h;
}
void scrollarea::update_location(const SDL_Rect& rect)
{
SDL_Rect r = rect;
shown_scrollbar_ = has_scrollbar();
if (shown_scrollbar_) {
int w = r.w - scrollbar_.width();
r.x += w;
r.w -= w;
scrollbar_.set_location(r);
r.x -= w;
r.w = w;
}
if (!hidden())
scrollbar_.hide(!shown_scrollbar_);
set_inner_location(r);
}
void scrollarea::test_scrollbar()
{
if (recursive_)
return;
recursive_ = true;
if (shown_scrollbar_ != has_scrollbar()) {
bg_restore();
bg_cancel();
update_location(location());
}
recursive_ = false;
}
void scrollarea::hide(bool value)
{
widget::hide(value);
if (shown_scrollbar_)
scrollbar_.hide(value);
}
unsigned scrollarea::get_position() const
{
return scrollbar_.get_position();
}
unsigned scrollarea::get_max_position() const
{
return scrollbar_.get_max_position();
}
void scrollarea::set_position(unsigned pos)
{
scrollbar_.set_position(pos);
}
void scrollarea::adjust_position(unsigned pos)
{
scrollbar_.adjust_position(pos);
}
void scrollarea::move_position(int dep)
{
scrollbar_.move_position(dep);
}
void scrollarea::set_shown_size(unsigned h)
{
scrollbar_.set_shown_size(h);
shown_size_ = h;
test_scrollbar();
}
void scrollarea::set_full_size(unsigned h)
{
scrollbar_.set_full_size(h);
full_size_ = h;
test_scrollbar();
}
void scrollarea::set_scroll_rate(unsigned r)
{
scrollbar_.set_scroll_rate(r);
}
void scrollarea::process_event()
{
int grip_position = scrollbar_.get_position();
if (grip_position == old_position_)
return;
old_position_ = grip_position;
scroll(grip_position);
}
SDL_Rect scrollarea::inner_location() const
{
SDL_Rect r = location();
if (shown_scrollbar_)
r.w -= scrollbar_.width();
return r;
}
unsigned scrollarea::scrollbar_width() const
{
return scrollbar_.width();
}
void scrollarea::handle_event(const SDL_Event& event)
{
gui::widget::handle_event(event);
if (mouse_locked() || hidden())
return;
if (event.type != SDL_MOUSEWHEEL)
return;
const SDL_MouseWheelEvent &ev = event.wheel;
int x, y;
SDL_GetMouseState(&x, &y);
if (sdl::point_in_rect(x, y, inner_location())) {
if (ev.y > 0) {
scrollbar_.scroll_up();
} else if (ev.y < 0) {
scrollbar_.scroll_down();
}
}
}
} // end namespace gui

View file

@ -1,67 +0,0 @@
/*
Copyright (C) 2004 - 2018 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/** @file */
#pragma once
#include "scrollbar.hpp"
namespace gui {
class scrollarea : public widget
{
public:
/**
* Create a zone with automatic handling of scrollbar.
* @todo FIXME: parameterlist ??
*/
//- \param d the display object
//- \param pane the widget where wheel events take place
scrollarea(CVideo &video, bool auto_join=true);
virtual void hide(bool value = true);
protected:
virtual sdl_handler_vector handler_members();
virtual void update_location(const SDL_Rect& rect);
virtual void handle_event(const SDL_Event& event);
virtual void process_event();
virtual void scroll(unsigned int pos) = 0;
virtual void set_inner_location(const SDL_Rect& rect) = 0;
SDL_Rect inner_location() const;
unsigned scrollbar_width() const;
unsigned get_position() const;
unsigned get_max_position() const;
void set_position(unsigned pos);
void adjust_position(unsigned pos);
void move_position(int dep);
void set_shown_size(unsigned h);
void set_full_size(unsigned h);
void set_scroll_rate(unsigned r);
bool has_scrollbar() const;
private:
scrollbar scrollbar_;
int old_position_;
bool recursive_, shown_scrollbar_;
unsigned shown_size_;
unsigned full_size_;
void test_scrollbar();
};
} // end namespace gui

View file

@ -1,396 +0,0 @@
/*
Copyright (C) 2003 by David White <dave@whitevine.net>
2004 - 2015 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/** @file */
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/scrollbar.hpp"
#include "picture.hpp"
#include "sdl/rect.hpp"
#include "video.hpp"
#include <iostream>
namespace {
const std::string scrollbar_top = "buttons/scrollbars_large/scrolltop.png";
const std::string scrollbar_bottom = "buttons/scrollbars_large/scrollbottom.png";
const std::string scrollbar_mid = "buttons/scrollbars_large/scrollmid.png";
const std::string scrollbar_top_hl = "buttons/scrollbars_large/scrolltop-active.png";
const std::string scrollbar_bottom_hl = "buttons/scrollbars_large/scrollbottom-active.png";
const std::string scrollbar_mid_hl = "buttons/scrollbars_large/scrollmid-active.png";
const std::string scrollbar_top_pressed = "buttons/scrollbars_large/scrolltop-pressed.png";
const std::string scrollbar_bottom_pressed = "buttons/scrollbars_large/scrollbottom-pressed.png";
const std::string scrollbar_mid_pressed = "buttons/scrollbars_large/scrollmid-pressed.png";
const std::string groove_top = "buttons/scrollbars_large/scrollgroove-top.png";
const std::string groove_mid = "buttons/scrollbars_large/scrollgroove-mid.png";
const std::string groove_bottom = "buttons/scrollbars_large/scrollgroove-bottom.png";
}
namespace gui {
scrollbar::scrollbar(CVideo &video)
: widget(video)
, mid_scaled_(nullptr)
, groove_scaled_(nullptr)
, uparrow_(video, "", button::TYPE_TURBO, "button_square/button_square_25"
, gui::button::DEFAULT_SPACE, true,"icons/arrows/arrows_ornate_up_25")
, downarrow_(video, "", button::TYPE_TURBO, "button_square/button_square_25"
, gui::button::DEFAULT_SPACE, true,"icons/arrows/arrows_ornate_down_25")
, state_(NORMAL)
, minimum_grip_height_(0)
, mousey_on_grip_(0)
, grip_position_(0)
, grip_height_(0)
, full_height_(0)
, scroll_rate_(1)
{
uparrow_.enable(false);
downarrow_.enable(false);
static const surface img(image::get_image(scrollbar_mid));
if (img != nullptr) {
set_width(img->w);
// this is a bit rough maybe
minimum_grip_height_ = 2 * img->h;
}
}
sdl_handler_vector scrollbar::handler_members()
{
sdl_handler_vector h;
h.push_back(&uparrow_);
h.push_back(&downarrow_);
return h;
}
void scrollbar::update_location(const SDL_Rect& rect)
{
int uh = uparrow_.height(), dh = downarrow_.height();
uparrow_.set_location(rect.x, rect.y);
downarrow_.set_location(rect.x, rect.y + rect.h - dh);
SDL_Rect r = rect;
r.y += uh;
r.h -= uh + dh;
widget::update_location(r);
//TODO comment or remove
//bg_register(r);
}
void scrollbar::hide(bool value)
{
widget::hide(value);
uparrow_.hide(value);
downarrow_.hide(value);
}
unsigned scrollbar::get_position() const
{
return grip_position_;
}
unsigned scrollbar::get_max_position() const
{
return full_height_ - grip_height_;
}
void scrollbar::set_position(unsigned pos)
{
if (pos > full_height_ - grip_height_)
pos = full_height_ - grip_height_;
if (pos == grip_position_)
return;
grip_position_ = pos;
uparrow_.enable(grip_position_ != 0);
downarrow_.enable(grip_position_ < full_height_ - grip_height_);
set_dirty();
}
void scrollbar::adjust_position(unsigned pos)
{
if (pos < grip_position_)
set_position(pos);
else if (pos >= grip_position_ + grip_height_)
set_position(pos - (grip_height_ - 1));
}
void scrollbar::move_position(int dep)
{
int pos = grip_position_ + dep;
if (pos > 0)
set_position(pos);
else
set_position(0);
}
void scrollbar::set_shown_size(unsigned h)
{
if (h > full_height_)
h = full_height_;
if (h == grip_height_)
return;
bool at_bottom = get_position() == get_max_position() && get_max_position() > 0;
grip_height_ = h;
if (at_bottom)
grip_position_ = get_max_position();
set_position(grip_position_);
set_dirty(true);
}
void scrollbar::set_full_size(unsigned h)
{
if (h == full_height_)
return;
bool at_bottom = get_position() == get_max_position() && get_max_position() > 0;
full_height_ = h;
if (at_bottom)
grip_position_ = get_max_position();
downarrow_.enable(grip_position_ < full_height_ - grip_height_);
set_shown_size(grip_height_);
set_position(grip_position_);
set_dirty(true);
}
void scrollbar::set_scroll_rate(unsigned r)
{
scroll_rate_ = r;
}
bool scrollbar::is_valid_height(int height) const
{
int uh = uparrow_.height();
int dh = downarrow_.height();
if(uh + dh >= height) {
return false;
} else {
return true;
}
}
void scrollbar::scroll_down()
{
move_position(scroll_rate_);
}
void scrollbar::scroll_up()
{
move_position(-scroll_rate_);
}
void scrollbar::process_event()
{
if (uparrow_.pressed())
scroll_up();
if (downarrow_.pressed())
scroll_down();
}
SDL_Rect scrollbar::groove_area() const
{
SDL_Rect loc = location();
int uh = uparrow_.height();
int dh = downarrow_.height();
if(uh + dh >= loc.h) {
loc.h = 0;
} else {
loc.y += uh;
loc.h -= uh + dh;
}
return loc;
}
SDL_Rect scrollbar::grip_area() const
{
const SDL_Rect& loc = groove_area();
if (full_height_ == grip_height_)
return loc;
int h = static_cast<int>(loc.h) * grip_height_ / full_height_;
if (h < minimum_grip_height_)
h = minimum_grip_height_;
int y = loc.y + (static_cast<int>(loc.h) - h) * grip_position_ / (full_height_ - grip_height_);
return {loc.x, y, loc.w, h};
}
void scrollbar::draw_contents()
{
surface mid_img;
surface bottom_img;
surface top_img;
switch (state_) {
case NORMAL:
top_img.assign(image::get_image(scrollbar_top));
mid_img.assign(image::get_image(scrollbar_mid));
bottom_img.assign(image::get_image(scrollbar_bottom));
break;
case ACTIVE:
top_img.assign(image::get_image(scrollbar_top_hl));
mid_img.assign(image::get_image(scrollbar_mid_hl));
bottom_img.assign(image::get_image(scrollbar_bottom_hl));
break;
case DRAGGED:
top_img.assign(image::get_image(scrollbar_top_pressed));
mid_img.assign(image::get_image(scrollbar_mid_pressed));
bottom_img.assign(image::get_image(scrollbar_bottom_pressed));
break;
case UNINIT:
default:
break;
}
const surface top_grv(image::get_image(groove_top));
const surface mid_grv(image::get_image(groove_mid));
const surface bottom_grv(image::get_image(groove_bottom));
if (mid_img == nullptr || bottom_img == nullptr || top_img == nullptr
|| top_grv == nullptr || bottom_grv == nullptr || mid_grv == nullptr) {
std::cerr << "Failure to load scrollbar image.\n";
return;
}
SDL_Rect grip = grip_area();
int mid_height = grip.h - top_img->h - bottom_img->h;
if (mid_height <= 0) {
// For now, minimum size of the middle piece is 1.
// This should never really be encountered, and if it is,
// it's a symptom of a larger problem, I think.
mid_height = 1;
}
if(mid_scaled_.null() || mid_scaled_->h != mid_height) {
mid_scaled_.assign(scale_surface(mid_img, mid_img->w, mid_height));
}
SDL_Rect groove = groove_area();
int groove_height = groove.h - top_grv->h - bottom_grv->h;
if (groove_height <= 0) {
groove_height = 1;
}
if (groove_scaled_.null() || groove_scaled_->h != groove_height) {
groove_scaled_.assign(scale_surface(mid_grv, mid_grv->w, groove_height));
}
if (mid_scaled_.null() || groove_scaled_.null()) {
std::cerr << "Failure during scrollbar image scale.\n";
return;
}
if (grip.h > groove.h) {
std::cerr << "abort draw scrollbar: grip too large\n";
return;
}
// Draw scrollbar "groove"
video().blit_surface(groove.x, groove.y, top_grv);
video().blit_surface(groove.x, groove.y + top_grv->h, groove_scaled_);
video().blit_surface(groove.x, groove.y + top_grv->h + groove_height, bottom_grv);
// Draw scrollbar "grip"
video().blit_surface(grip.x, grip.y, top_img);
video().blit_surface(grip.x, grip.y + top_img->h, mid_scaled_);
video().blit_surface(grip.x, grip.y + top_img->h + mid_height, bottom_img);
}
void scrollbar::handle_event(const SDL_Event& event)
{
gui::widget::handle_event(event);
if (mouse_locked() || hidden())
return;
STATE new_state = state_;
const SDL_Rect& grip = grip_area();
const SDL_Rect& groove = groove_area();
switch (event.type) {
case SDL_MOUSEBUTTONUP:
{
const SDL_MouseButtonEvent& e = event.button;
bool on_grip = sdl::point_in_rect(e.x, e.y, grip);
new_state = on_grip ? ACTIVE : NORMAL;
break;
}
case SDL_MOUSEBUTTONDOWN:
{
const SDL_MouseButtonEvent& e = event.button;
bool on_grip = sdl::point_in_rect(e.x, e.y, grip);
bool on_groove = sdl::point_in_rect(e.x, e.y, groove);
if (on_grip && e.button == SDL_BUTTON_LEFT) {
mousey_on_grip_ = e.y - grip.y;
new_state = DRAGGED;
} else if (on_groove && e.button == SDL_BUTTON_LEFT && groove.h != grip.h) {
if (e.y < grip.y)
move_position(-static_cast<int>(grip_height_));
else
move_position(grip_height_);
} else if (on_groove && e.button == SDL_BUTTON_MIDDLE && groove.h != grip.h) {
int y_dep = e.y - grip.y - grip.h/2;
int dep = y_dep * int(full_height_ - grip_height_)/ (groove.h - grip.h);
move_position(dep);
}
break;
}
case SDL_MOUSEMOTION:
{
const SDL_MouseMotionEvent& e = event.motion;
if (state_ == NORMAL || state_ == ACTIVE) {
bool on_grip = sdl::point_in_rect(e.x, e.y, grip);
new_state = on_grip ? ACTIVE : NORMAL;
} else if (state_ == DRAGGED && groove.h != grip.h) {
int y_dep = e.y - grip.y - mousey_on_grip_;
int dep = y_dep * static_cast<int>(full_height_ - grip_height_) / (groove.h - grip.h);
move_position(dep);
}
break;
}
case SDL_MOUSEWHEEL:
{
const SDL_MouseWheelEvent& e = event.wheel;
int x, y;
SDL_GetMouseState(&x, &y);
bool on_groove = sdl::point_in_rect(x, y, groove);
if (on_groove && e.y < 0) {
move_position(scroll_rate_);
} else if (on_groove && e.y > 0) {
move_position(-scroll_rate_);
}
break;
}
default:
break;
}
if (new_state != state_) {
set_dirty();
mid_scaled_.assign(nullptr);
state_ = new_state;
}
}
} // end namespace gui

View file

@ -1,103 +0,0 @@
/*
Copyright (C) 2003 by David White <dave@whitevine.net>
2004 - 2015 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
/** @file */
#pragma once
#include "button.hpp"
namespace gui {
class scrollarea;
/** Scrollbar */
class scrollbar : public widget
{
public:
/**
* Create a scrollbar.
* @todo FIXME: parameterlist ??
*/
//- @param d the display object
//- @param pane the widget where wheel events take place
//- @param callback a callback interface for warning that the grip has been moved
scrollbar(CVideo &video);
virtual void hide(bool value = true);
/**
* Determine where the scrollbar is.
*
* @return the position.
* @retval returns 0 if the scrollbar is at the top,
* @retval returns (full_size - shown_size) if it is at the bottom.
*/
unsigned get_position() const;
unsigned get_max_position() const;
/** Manually update the scrollbar. */
void set_position(unsigned pos);
/** Ensure the viewport contains the position. */
void adjust_position(unsigned pos);
/** Move the scrollbar. */
void move_position(int dep);
/** Set the relative size of the grip. */
void set_shown_size(unsigned h);
/** Set the relative size of the scrollbar. */
void set_full_size(unsigned h);
/** Set scroll rate. */
void set_scroll_rate(unsigned r);
/** Return true if the scrollbar has a valid size. */
bool is_valid_height(int height) const;
/** Scrolls down one step */
void scroll_down();
/** Scrolls up one step */
void scroll_up();
protected:
virtual sdl_handler_vector handler_members();
virtual void update_location(const SDL_Rect& rect);
virtual void handle_event(const SDL_Event& event);
virtual void process_event();
virtual void draw_contents();
private:
SDL_Rect grip_area() const;
SDL_Rect groove_area() const;
surface mid_scaled_, groove_scaled_;
button uparrow_, downarrow_;
enum STATE { UNINIT, NORMAL, ACTIVE, DRAGGED };
STATE state_;
int minimum_grip_height_, mousey_on_grip_;
// Relative data
unsigned int grip_position_, grip_height_, full_height_;
int scroll_rate_;
};
} // end namespace gui

View file

@ -1,730 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/textbox.hpp"
#include "desktop/clipboard.hpp"
#include "font/sdl_ttf.hpp"
#include "log.hpp"
#include "sdl/rect.hpp"
#include "serialization/string_utils.hpp"
#include "video.hpp"
static lg::log_domain log_display("display");
#define WRN_DP LOG_STREAM(warn, log_display)
#define DBG_G LOG_STREAM(debug, lg::general())
namespace gui {
textbox::textbox(CVideo &video, int width, const std::string& text, bool editable, std::size_t max_size, int font_size, double alpha, double alpha_focus, const bool auto_join)
: scrollarea(video, auto_join), max_size_(max_size), font_size_(font_size), text_(unicode_cast<std::u32string>(text)),
cursor_(text_.size()), selstart_(-1), selend_(-1),
grabmouse_(false), text_pos_(0), editable_(editable),
show_cursor_(true), show_cursor_at_(0), text_image_(nullptr),
wrap_(false), line_height_(0), yscroll_(0), alpha_(alpha),
alpha_focus_(alpha_focus),
edit_target_(nullptr)
,listening_(false)
{
// static const SDL_Rect area = video.screen_area();
// const int height = font::draw_text(nullptr,area,font_size,font::NORMAL_COLOR,"ABCD",0,0).h;
set_measurements(width, font::get_max_height(font_size_));
set_scroll_rate(font::get_max_height(font_size_) / 2);
update_text_cache(true);
}
textbox::~textbox()
{
}
void textbox::update_location(const SDL_Rect& rect)
{
scrollarea::update_location(rect);
update_text_cache(true);
set_dirty(true);
}
void textbox::set_inner_location(const SDL_Rect& rect)
{
bg_register(rect);
if (text_image_.null()) return;
text_pos_ = 0;
update_text_cache(false);
}
const std::string textbox::text() const
{
const std::string &ret = unicode_cast<std::string>(text_);
return ret;
}
// set_text does not respect max_size_
void textbox::set_text(const std::string& text, const color_t& color)
{
text_ = unicode_cast<std::u32string>(text);
cursor_ = text_.size();
text_pos_ = 0;
selstart_ = -1;
selend_ = -1;
set_dirty(true);
update_text_cache(true, color);
handle_text_changed(text_);
}
void textbox::append_text(const std::string& text, bool auto_scroll, const color_t& color)
{
if(text_image_.get() == nullptr) {
set_text(text, color);
return;
}
//disallow adding multi-line text to a single-line text box
if(wrap_ == false && std::find_if(text.begin(),text.end(),utils::isnewline) != text.end()) {
return;
}
const bool is_at_bottom = get_position() == get_max_position();
const std::u32string& wtext = unicode_cast<std::u32string>(text);
surface new_text = add_text_line(wtext, color);
surface new_surface = create_compatible_surface(text_image_,std::max<std::size_t>(text_image_->w,new_text->w),text_image_->h+new_text->h);
adjust_surface_alpha(new_text, SDL_ALPHA_TRANSPARENT);
adjust_surface_alpha(text_image_, SDL_ALPHA_TRANSPARENT);
SDL_SetSurfaceBlendMode(text_image_, SDL_BLENDMODE_NONE);
sdl_blit(text_image_,nullptr,new_surface,nullptr);
SDL_SetSurfaceBlendMode(text_image_, SDL_BLENDMODE_BLEND);
SDL_Rect target {
0
, text_image_->h
, new_text->w
, new_text->h
};
SDL_SetSurfaceBlendMode(new_text, SDL_BLENDMODE_NONE);
sdl_blit(new_text,nullptr,new_surface,&target);
text_image_.assign(new_surface);
text_.insert(text_.end(), wtext.begin(), wtext.end());
set_dirty(true);
update_text_cache(false);
if(auto_scroll && is_at_bottom) scroll_to_bottom();
handle_text_changed(text_);
}
void textbox::clear()
{
text_.clear();
cursor_ = 0;
cursor_pos_ = 0;
text_pos_ = 0;
selstart_ = -1;
selend_ = -1;
set_dirty(true);
update_text_cache(true);
handle_text_changed(text_);
}
void textbox::set_selection(const int selstart, const int selend)
{
if (!editable_) {
return;
}
if (selstart < 0 || selend < 0 || std::size_t(selstart) > text_.size() ||
std::size_t(selend) > text_.size()) {
WRN_DP << "out-of-boundary selection" << std::endl;
return;
}
selstart_= selstart;
selend_ = selend;
set_dirty(true);
}
void textbox::set_cursor_pos(const int cursor_pos)
{
if (!editable_) {
return;
}
if (cursor_pos < 0 || std::size_t(cursor_pos) > text_.size()) {
WRN_DP << "out-of-boundary selection" << std::endl;
return;
}
cursor_ = cursor_pos;
update_text_cache(false);
set_dirty(true);
}
void textbox::draw_cursor(int pos) const
{
if(show_cursor_ && editable_ && enabled()) {
SDL_Rect rect {
location().x + pos
, location().y
, 1
, location().h
};
sdl::fill_rectangle(rect, {255, 255, 255, 255});
}
}
void textbox::draw_contents()
{
const SDL_Rect& loc = inner_location();
surface& surf = video().getSurface();
color_t c(0, 0, 0);
double& alpha = focus(nullptr) ? alpha_focus_ : alpha_;
c.a = 255 * alpha;
sdl::fill_rectangle(loc, c);
SDL_Rect src;
if(text_image_ == nullptr) {
update_text_cache(true);
}
if(text_image_ != nullptr) {
src.y = yscroll_;
src.w = std::min<std::size_t>(loc.w,text_image_->w);
src.h = std::min<std::size_t>(loc.h,text_image_->h);
src.x = text_pos_;
SDL_Rect dest = video().screen_area();
dest.x = loc.x;
dest.y = loc.y;
// Fills the selected area
if(enabled() && is_selection()) {
const int start = std::min<int>(selstart_,selend_);
const int end = std::max<int>(selstart_,selend_);
int startx = char_x_[start];
int starty = char_y_[start];
const int endx = char_x_[end];
const int endy = char_y_[end];
while(starty <= endy) {
const std::size_t right = starty == endy ? endx : text_image_->w;
if(right <= std::size_t(startx)) {
break;
}
SDL_Rect rect = sdl::create_rect(loc.x + startx
, loc.y + starty - src.y
, right - startx
, line_height_);
const clip_rect_setter clipper(surf, &loc);
color_t c2(0, 0, 160, 140);
sdl::fill_rectangle(rect, c2);
starty += int(line_height_);
startx = 0;
}
}
if(enabled()) {
sdl_blit(text_image_, &src, surf, &dest);
} else {
// HACK: using 30% opacity allows white text to look as though it is grayed out,
// while not changing any applicable non-grayscale AA. Actual colored text will
// not look as good, but this is not currently a concern since GUI1 textboxes
// are not used much nowadays, and they will eventually all go away.
adjust_surface_alpha(text_image_, ftofxp(0.3));
sdl_blit(text_image_, &src, surf, &dest);
}
}
draw_cursor(cursor_pos_ == 0 ? 0 : cursor_pos_ - 1);
}
void textbox::set_editable(bool value)
{
editable_ = value;
}
bool textbox::editable() const
{
return editable_;
}
int textbox::font_size() const
{
return font_size_;
}
void textbox::set_font_size(int fs)
{
font_size_ = fs;
}
void textbox::scroll_to_bottom()
{
set_position(get_max_position());
}
void textbox::set_wrap(bool val)
{
if(wrap_ != val) {
wrap_ = val;
update_text_cache(true);
set_dirty(true);
}
}
void textbox::scroll(unsigned int pos)
{
yscroll_ = pos;
set_dirty(true);
}
surface textbox::add_text_line(const std::u32string& text, const color_t& color)
{
line_height_ = font::get_max_height(font_size_);
if(char_y_.empty()) {
char_y_.push_back(0);
} else {
char_y_.push_back(char_y_.back() + line_height_);
}
char_x_.push_back(0);
// Re-calculate the position of each glyph. We approximate this by asking the
// width of each substring, but this is a flawed assumption which won't work with
// some more complex scripts (that is, RTL languages). This part of the work should
// actually be done by the font-rendering system.
std::string visible_string;
std::u32string wrapped_text;
std::u32string::const_iterator backup_itor = text.end();
std::u32string::const_iterator itor = text.begin();
while(itor != text.end()) {
//If this is a space, save copies of the current state so we can roll back
if(char(*itor) == ' ') {
backup_itor = itor;
}
visible_string.append(unicode_cast<std::string>(*itor));
if(char(*itor) == '\n') {
backup_itor = text.end();
visible_string = "";
}
int w = font::line_width(visible_string, font_size_);
if(wrap_ && w >= inner_location().w) {
if(backup_itor != text.end()) {
int backup = itor - backup_itor;
itor = backup_itor + 1;
if(backup > 0) {
char_x_.erase(char_x_.end()-backup, char_x_.end());
char_y_.erase(char_y_.end()-backup, char_y_.end());
wrapped_text.erase(wrapped_text.end()-backup, wrapped_text.end());
}
} else {
if (visible_string == std::string("").append(unicode_cast<std::string>(*itor))) {
break; //breaks infinite loop where when running with a fake display, we word wrap a single character infinitely.
}
}
backup_itor = text.end();
wrapped_text.push_back(char32_t('\n'));
char_x_.push_back(0);
char_y_.push_back(char_y_.back() + line_height_);
visible_string = "";
} else {
wrapped_text.push_back(*itor);
char_x_.push_back(w);
char_y_.push_back(char_y_.back() + (char(*itor) == '\n' ? line_height_ : 0));
++itor;
}
}
const std::string s = unicode_cast<std::string>(wrapped_text);
const surface res(font::get_rendered_text(s, font_size_, color));
return res;
}
void textbox::update_text_cache(bool changed, const color_t& color)
{
if(changed) {
char_x_.clear();
char_y_.clear();
text_image_.assign(add_text_line(text_, color));
}
int cursor_x = char_x_[cursor_];
if(cursor_x - text_pos_ > location().w) {
text_pos_ = cursor_x - location().w;
} else if(cursor_x - text_pos_ < 0) {
text_pos_ = cursor_x;
}
cursor_pos_ = cursor_x - text_pos_;
if (!text_image_.null()) {
set_full_size(text_image_->h);
set_shown_size(location().h);
}
}
bool textbox::is_selection()
{
return (selstart_ != -1) && (selend_ != -1) && (selstart_ != selend_);
}
void textbox::erase_selection()
{
if(!is_selection())
return;
std::u32string::iterator itor = text_.begin() + std::min(selstart_, selend_);
text_.erase(itor, itor + std::abs(selend_ - selstart_));
cursor_ = std::min(selstart_, selend_);
selstart_ = selend_ = -1;
}
namespace {
const unsigned int copypaste_modifier =
#ifdef __APPLE__
KMOD_LGUI | KMOD_RGUI
#else
KMOD_CTRL
#endif
;
}
bool textbox::requires_event_focus(const SDL_Event* event) const
{
if(!focus_ || hidden() || !enabled()) {
return false;
}
if(event == nullptr) {
//when event is not specified, signal that focus may be desired later
return true;
}
if(event->type == SDL_KEYDOWN) {
SDL_Keycode key = event->key.keysym.sym;
switch(key) {
case SDLK_UP:
case SDLK_DOWN:
case SDLK_PAGEUP:
case SDLK_PAGEDOWN:
//in the future we may need to check for input history or multi-line support
//for now, just return false since these events are not handled.
return false;
default:
return true;
}
}
//mouse events are processed regardless of focus
return false;
}
void textbox::handle_event(const SDL_Event& event)
{
gui::widget::handle_event(event);
handle_event(event, false);
}
bool textbox::handle_text_input(const SDL_Event& event)
{
bool changed = false;
std::string str = event.text.text;
std::u32string s = unicode_cast<std::u32string>(str);
DBG_G << "Char: " << str << "\n";
if (editable_) {
changed = true;
if (is_selection())
erase_selection();
if (text_.size() + 1 <= max_size_) {
text_.insert(text_.begin() + cursor_, s.begin(), s.end());
cursor_ += s.size();
}
} else {
pass_event_to_target(event);
}
return changed;
}
bool textbox::handle_key_down(const SDL_Event &event)
{
bool changed = false;
const SDL_Keysym& key = reinterpret_cast<const SDL_KeyboardEvent&>(event).keysym;
const SDL_Keymod modifiers = SDL_GetModState();
const int c = key.sym;
const int old_cursor = cursor_;
listening_ = true;
if(editable_) {
if(c == SDLK_LEFT && cursor_ > 0)
--cursor_;
if(c == SDLK_RIGHT && cursor_ < static_cast<int>(text_.size()))
++cursor_;
// ctrl-a, ctrl-e and ctrl-u are readline style shortcuts, even on Macs
if(c == SDLK_END || (c == SDLK_e && (modifiers & KMOD_CTRL)))
cursor_ = text_.size();
if(c == SDLK_HOME || (c == SDLK_a && (modifiers & KMOD_CTRL)))
cursor_ = 0;
if((old_cursor != cursor_) && (modifiers & KMOD_SHIFT)) {
if(selstart_ == -1)
selstart_ = old_cursor;
selend_ = cursor_;
}
} else if(c == SDLK_LEFT || c == SDLK_RIGHT || c == SDLK_END || c == SDLK_HOME) {
pass_event_to_target(event);
}
if(editable_) {
if(c == SDLK_BACKSPACE) {
changed = true;
if(is_selection()) {
erase_selection();
} else if(cursor_ > 0) {
--cursor_;
text_.erase(text_.begin()+cursor_);
}
}
if(c == SDLK_u && (modifiers & KMOD_CTRL)) { // clear line
changed = true;
cursor_ = 0;
text_.resize(0);
}
if(c == SDLK_DELETE && !text_.empty()) {
changed = true;
if(is_selection()) {
erase_selection();
} else {
if(cursor_ < static_cast<int>(text_.size())) {
text_.erase(text_.begin()+cursor_);
}
}
}
} else if(c == SDLK_BACKSPACE || c == SDLK_DELETE || (c == SDLK_u && (modifiers & KMOD_CTRL))) {
pass_event_to_target(event);
}
//movement characters may have a "Unicode" field on some platforms, so ignore it.
if(!(c == SDLK_UP || c == SDLK_DOWN || c == SDLK_LEFT || c == SDLK_RIGHT ||
c == SDLK_DELETE || c == SDLK_BACKSPACE || c == SDLK_END || c == SDLK_HOME ||
c == SDLK_PAGEUP || c == SDLK_PAGEDOWN)) {
if((event.key.keysym.mod & copypaste_modifier)
//on windows SDL fires for AltGr lctrl+ralt (needed to access @ etc on certain keyboards)
#ifdef _WIN32
&& !(event.key.keysym.mod & KMOD_ALT)
#endif
) {
switch(c) {
case SDLK_v: // paste
{
if(!editable()) {
pass_event_to_target(event);
break;
}
changed = true;
if(is_selection())
erase_selection();
std::string str = desktop::clipboard::copy_from_clipboard(false);
//cut off anything after the first newline
str.erase(std::find_if(str.begin(),str.end(),utils::isnewline),str.end());
std::u32string s = unicode_cast<std::u32string>(str);
if(text_.size() < max_size_) {
if(s.size() + text_.size() > max_size_) {
s.resize(max_size_ - text_.size());
}
text_.insert(text_.begin()+cursor_, s.begin(), s.end());
cursor_ += s.size();
}
}
break;
case SDLK_c: // copy
{
if(is_selection())
{
const std::size_t beg = std::min<std::size_t>(std::size_t(selstart_),std::size_t(selend_));
const std::size_t end = std::max<std::size_t>(std::size_t(selstart_),std::size_t(selend_));
std::u32string ws(text_.begin() + beg, text_.begin() + end);
std::string s = unicode_cast<std::string>(ws);
desktop::clipboard::copy_to_clipboard(s, false);
}
}
break;
}
}
else {
pass_event_to_target(event);
}
}
return changed;
}
void textbox::handle_event(const SDL_Event& event, bool was_forwarded)
{
if(!enabled())
return;
scrollarea::handle_event(event);
if(hidden())
return;
bool changed = false;
const int old_selstart = selstart_;
const int old_selend = selend_;
//Sanity check: verify that selection start and end are within text
//boundaries
if(is_selection() && !(std::size_t(selstart_) <= text_.size() && std::size_t(selend_) <= text_.size())) {
WRN_DP << "out-of-boundary selection" << std::endl;
selstart_ = selend_ = -1;
}
int mousex, mousey;
const uint8_t mousebuttons = SDL_GetMouseState(&mousex,&mousey);
if(!(mousebuttons & SDL_BUTTON(1))) {
grabmouse_ = false;
}
const SDL_Rect& loc = inner_location();
bool clicked_inside = !mouse_locked() && (event.type == SDL_MOUSEBUTTONDOWN
&& (mousebuttons & SDL_BUTTON(1))
&& sdl::point_in_rect(mousex, mousey, loc));
if(clicked_inside) {
set_focus(true);
}
if ((grabmouse_ && (!mouse_locked() && event.type == SDL_MOUSEMOTION)) || clicked_inside) {
const int x = mousex - loc.x + text_pos_;
const int y = mousey - loc.y;
int pos = 0;
int distance = x;
for(unsigned int i = 1; i < char_x_.size(); ++i) {
if(static_cast<int>(yscroll_) + y < char_y_[i]) {
break;
}
// Check individually each distance (if, one day, we support
// RTL languages, char_x_[c] may not be monotonous.)
if(std::abs(x - char_x_[i]) < distance && yscroll_ + y < char_y_[i] + line_height_) {
pos = i;
distance = std::abs(x - char_x_[i]);
}
}
cursor_ = pos;
if(grabmouse_)
selend_ = cursor_;
update_text_cache(false);
if(!grabmouse_ && (mousebuttons & SDL_BUTTON(1))) {
grabmouse_ = true;
selstart_ = selend_ = cursor_;
} else if (! (mousebuttons & SDL_BUTTON(1))) {
grabmouse_ = false;
}
set_dirty();
}
//if we don't have the focus, then see if we gain the focus,
//otherwise return
if(!was_forwarded && focus(&event) == false) {
if (!mouse_locked() && event.type == SDL_MOUSEMOTION && sdl::point_in_rect(mousex, mousey, loc))
events::focus_handler(this);
return;
}
const int old_cursor = cursor_;
if (event.type == SDL_TEXTINPUT && listening_) {
changed = handle_text_input(event);
} else
if (event.type == SDL_KEYDOWN) {
changed = handle_key_down(event);
}
else {
if(event.type != SDL_KEYDOWN || (!was_forwarded && focus(&event) != true)) {
draw();
return;
}
}
if(is_selection() && (selend_ != cursor_))
selstart_ = selend_ = -1;
//since there has been cursor activity, make the cursor appear for
//at least the next 500ms.
show_cursor_ = true;
show_cursor_at_ = SDL_GetTicks();
if(changed || old_cursor != cursor_ || old_selstart != selstart_ || old_selend != selend_) {
text_image_ = nullptr;
handle_text_changed(text_);
}
set_dirty(true);
}
void textbox::pass_event_to_target(const SDL_Event& event)
{
if(edit_target_ && edit_target_->editable()) {
edit_target_->handle_event(event, true);
}
}
void textbox::set_edit_target(textbox* target)
{
edit_target_ = target;
}
} //end namespace gui

View file

@ -1,122 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "serialization/unicode.hpp"
#include "font/constants.hpp"
#include "font/standard_colors.hpp"
#include "scrollarea.hpp"
namespace gui {
class textbox : public scrollarea
{
public:
textbox(CVideo &video, int width, const std::string& text="", bool editable=true, std::size_t max_size = 256, int font_size = font::SIZE_PLUS, double alpha = 0.4, double alpha_focus = 0.2, const bool auto_join = true);
virtual ~textbox();
const std::string text() const;
void set_text(const std::string& text, const color_t& color =font::NORMAL_COLOR);
void append_text(const std::string& text,bool auto_scroll = false, const color_t& color =font::NORMAL_COLOR);
void clear();
void set_selection(const int selstart, const int selend);
void set_cursor_pos(const int cursor_pos);
void set_editable(bool value);
bool editable() const;
int font_size() const;
void set_font_size(int fs);
void scroll_to_bottom();
void set_wrap(bool val);
void set_edit_target(textbox* target);
protected:
virtual void draw_contents();
virtual void update_location(const SDL_Rect& rect);
virtual void set_inner_location(const SDL_Rect& );
virtual void scroll(unsigned int pos);
private:
virtual void handle_text_changed(const std::u32string&) {}
std::size_t max_size_;
int font_size_;
std::u32string text_;
// mutable unsigned int firstOnScreen_;
int cursor_;
int selstart_;
int selend_;
bool grabmouse_;
int text_pos_;
int cursor_pos_;
std::vector<int> char_x_, char_y_;
bool editable_;
bool show_cursor_;
//records the time the cursor was shown at last
//the cursor should be inverted every 500 ms.
//this will be reset when keyboard input events occur
int show_cursor_at_;
surface text_image_;
bool wrap_;
std::size_t line_height_, yscroll_;
double alpha_;
double alpha_focus_;
textbox* edit_target_;
/* This boolean is used to filter out any TextInput events that are received without
* the corresponding KeyPress events. This is needed to avoid a bug when creating a
* textbox using a hotkey.
* */
bool listening_;
void handle_event(const SDL_Event& event, bool was_forwarded);
void handle_event(const SDL_Event& event);
void pass_event_to_target(const SDL_Event& event);
void draw_cursor(int pos) const;
void update_text_cache(bool reset = false, const color_t& color =font::NORMAL_COLOR);
surface add_text_line(const std::u32string& text, const color_t& color =font::NORMAL_COLOR);
bool is_selection();
void erase_selection();
//make it so that only one textbox object can be receiving
//events at a time.
bool requires_event_focus(const SDL_Event *event=nullptr) const;
bool show_scrollbar() const;
bool handle_text_input(const SDL_Event& event);
bool handle_key_down(const SDL_Event &event);
};
}

View file

@ -1,358 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "widgets/widget.hpp"
#include "video.hpp"
#include "sdl/rect.hpp"
#include "tooltips.hpp"
#include <cassert>
namespace {
const SDL_Rect EmptyRect {-1234,-1234,0,0};
}
namespace gui {
bool widget::mouse_lock_ = false;
widget::widget(const widget &o)
: events::sdl_handler(), focus_(o.focus_), video_(o.video_), restorer_(o.restorer_), rect_(o.rect_),
needs_restore_(o.needs_restore_), state_(o.state_), hidden_override_(o.hidden_override_),
enabled_(o.enabled_), clip_(o.clip_), clip_rect_(o.clip_rect_), volatile_(o.volatile_),
help_text_(o.help_text_), tooltip_text_(o.tooltip_text_), help_string_(o.help_string_), id_(o.id_), mouse_lock_local_(o.mouse_lock_local_)
{
}
widget::widget(CVideo& video, const bool auto_join)
: events::sdl_handler(auto_join), focus_(true), video_(&video), rect_(EmptyRect), needs_restore_(false),
state_(UNINIT), hidden_override_(false), enabled_(true), clip_(false),
clip_rect_(EmptyRect), volatile_(false), help_string_(0), mouse_lock_local_(false)
{
}
widget::~widget()
{
bg_cancel();
free_mouse_lock();
}
void widget::aquire_mouse_lock()
{
assert(!mouse_lock_);
mouse_lock_ = true;
mouse_lock_local_ = true;
}
void widget::free_mouse_lock()
{
if (mouse_lock_local_)
{
mouse_lock_local_ = false;
mouse_lock_ = false;
}
}
bool widget::mouse_locked() const
{
return mouse_lock_ && !mouse_lock_local_;
}
void widget::bg_cancel()
{
for(std::vector< surface_restorer >::iterator i = restorer_.begin(),
i_end = restorer_.end(); i != i_end; ++i)
i->cancel();
restorer_.clear();
}
void widget::set_location(const SDL_Rect& rect)
{
if(rect_.x == rect.x && rect_.y == rect.y && rect_.w == rect.w && rect_.h == rect.h)
return;
if(state_ == UNINIT && rect.x != -1234 && rect.y != -1234)
state_ = DRAWN;
bg_restore();
bg_cancel();
rect_ = rect;
set_dirty(true);
update_location(rect);
}
void widget::update_location(const SDL_Rect& rect)
{
bg_register(rect);
}
const SDL_Rect* widget::clip_rect() const
{
return clip_ ? &clip_rect_ : nullptr;
}
void widget::bg_register(const SDL_Rect& rect)
{
restorer_.emplace_back(&video(), rect);
}
void widget::set_location(int x, int y)
{
set_location({x, y, rect_.w, rect_.h});
}
void widget::set_width(int w)
{
set_location({rect_.x, rect_.y, w, rect_.h});
}
void widget::set_height(int h)
{
set_location({rect_.x, rect_.y, rect_.w, h});
}
void widget::set_measurements(int w, int h)
{
set_location({rect_.x, rect_.y, w, h});
}
int widget::width() const
{
return rect_.w;
}
int widget::height() const
{
return rect_.h;
}
const SDL_Rect& widget::location() const
{
return rect_;
}
void widget::set_focus(bool focus)
{
if (focus)
events::focus_handler(this);
focus_ = focus;
set_dirty(true);
}
bool widget::focus(const SDL_Event* event)
{
return events::has_focus(this, event) && focus_;
}
void widget::hide(bool value)
{
if (value) {
if ((state_ == DIRTY || state_ == DRAWN) && !hidden_override_)
bg_restore();
state_ = HIDDEN;
} else if (state_ == HIDDEN) {
state_ = DRAWN;
if (!hidden_override_) {
bg_update();
set_dirty(true);
}
}
}
void widget::hide_override(bool value) {
if (hidden_override_ != value) {
hidden_override_ = value;
if (state_ == DIRTY || state_ == DRAWN) {
if (value) {
bg_restore();
} else {
bg_update();
set_dirty(true);
}
}
}
}
void widget::set_clip_rect(const SDL_Rect& rect)
{
clip_rect_ = rect;
clip_ = true;
set_dirty(true);
}
bool widget::hidden() const
{
return (state_ == HIDDEN || hidden_override_ || state_ == UNINIT
|| (clip_ && !sdl::rects_overlap(clip_rect_, rect_)));
}
void widget::enable(bool new_val)
{
if (enabled_ != new_val) {
enabled_ = new_val;
set_dirty();
}
}
bool widget::enabled() const
{
return enabled_;
}
void widget::set_dirty(bool dirty)
{
if ((dirty && (volatile_ || hidden_override_ || state_ != DRAWN)) || (!dirty && state_ != DIRTY))
return;
state_ = dirty ? DIRTY : DRAWN;
if (!dirty)
needs_restore_ = true;
}
bool widget::dirty() const
{
return state_ == DIRTY;
}
const std::string& widget::id() const
{
return id_;
}
void widget::set_id(const std::string& id)
{
if (id_.empty()){
id_ = id;
}
}
void widget::bg_update()
{
for(std::vector< surface_restorer >::iterator i = restorer_.begin(),
i_end = restorer_.end(); i != i_end; ++i)
i->update();
}
void widget::bg_restore() const
{
clip_rect_setter clipper(video().getSurface(), &clip_rect_, clip_);
if (needs_restore_) {
for(std::vector< surface_restorer >::const_iterator i = restorer_.begin(),
i_end = restorer_.end(); i != i_end; ++i)
i->restore();
needs_restore_ = false;
}
}
void widget::bg_restore(const SDL_Rect& rect) const
{
clip_rect_setter clipper(video().getSurface(), &clip_rect_, clip_);
for(std::vector< surface_restorer >::const_iterator i = restorer_.begin(),
i_end = restorer_.end(); i != i_end; ++i)
i->restore(rect);
}
void widget::set_volatile(bool val)
{
volatile_ = val;
if (volatile_ && state_ == DIRTY)
state_ = DRAWN;
}
void widget::draw()
{
if (hidden() || !dirty())
return;
bg_restore();
clip_rect_setter clipper(video().getSurface(), &clip_rect_, clip_);
draw_contents();
set_dirty(false);
}
void widget::volatile_draw()
{
if (!volatile_ || state_ != DRAWN || hidden_override_)
return;
state_ = DIRTY;
bg_update();
draw();
}
void widget::volatile_undraw()
{
if (!volatile_)
return;
bg_restore();
}
void widget::set_help_string(const std::string& str)
{
help_text_ = str;
}
void widget::set_tooltip_string(const std::string& str)
{
tooltip_text_ = str;
}
void widget::process_help_string(int mousex, int mousey)
{
if (!hidden() && sdl::point_in_rect(mousex, mousey, rect_)) {
if(help_string_ == 0 && !help_text_.empty()) {
//std::cerr << "setting help string to '" << help_text_ << "'\n";
help_string_ = video().set_help_string(help_text_);
}
} else if(help_string_ > 0) {
video().clear_help_string(help_string_);
help_string_ = 0;
}
}
void widget::process_tooltip_string(int mousex, int mousey)
{
if (!hidden() && sdl::point_in_rect(mousex, mousey, rect_)) {
if (!tooltip_text_.empty())
tooltips::add_tooltip(rect_, tooltip_text_ );
}
}
void widget::handle_event(const SDL_Event& event) {
if (event.type == DRAW_ALL_EVENT) {
set_dirty();
draw();
}
}
void widget::handle_window_event(const SDL_Event& event) {
if (event.type == SDL_WINDOWEVENT) {
switch (event.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_RESTORED:
case SDL_WINDOWEVENT_SHOWN:
case SDL_WINDOWEVENT_EXPOSED:
set_dirty();
}
}
}
}

View file

@ -1,133 +0,0 @@
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "events.hpp"
#include "sdl/surface.hpp"
#include <string>
class CVideo;
namespace gui {
class widget : public events::sdl_handler
{
public:
const SDL_Rect& location() const;
virtual void set_location(const SDL_Rect& rect);
void set_location(int x, int y);
void set_width(int w);
void set_height(int h);
void set_measurements(int w, int h);
int width() const;
int height() const;
//focus() may gain the focus if the currently focused handler doesn't require this event
bool focus(const SDL_Event* event);
void set_focus(bool focus);
virtual void hide(bool value = true);
bool hidden() const;
virtual void enable(bool new_val = true);
bool enabled() const;
void set_clip_rect(const SDL_Rect& rect);
//Function to set the widget to draw in 'volatile' mode.
//When in 'volatile' mode, instead of using the normal
//save-background-redraw-when-dirty procedure, redrawing is done
//every frame, and then after every frame the area under the widget
//is restored to the state it was in before the frame. This is useful
//for drawing widgets with alpha components in volatile settings where
//the background may change at any time.
//(e.g. for putting widgets on top of the game map)
void set_volatile(bool val=true);
void set_dirty(bool dirty=true);
bool dirty() const;
const std::string& id() const;
void set_id(const std::string& id);
void set_help_string(const std::string& str);
void set_tooltip_string(const std::string& str);
virtual void process_help_string(int mousex, int mousey);
virtual void process_tooltip_string(int mousex, int mousey);
protected:
widget(const widget& o);
widget(CVideo& video, const bool auto_join=true);
virtual ~widget();
// During each relocation, this function should be called to register
// the rectangles the widget needs to refresh automatically
void bg_register(const SDL_Rect& rect);
void bg_restore() const;
void bg_restore(const SDL_Rect& rect) const;
void bg_update();
void bg_cancel();
CVideo& video() const { return *video_; }
public:
virtual void draw();
protected:
virtual void draw_contents() {}
virtual void update_location(const SDL_Rect& rect);
const SDL_Rect* clip_rect() const;
virtual sdl_handler_vector member_handlers() { return sdl_handler::handler_members(); }
virtual void handle_event(const SDL_Event&);
virtual void handle_window_event(const SDL_Event& event);
bool focus_; // Should user input be ignored?
bool mouse_locked() const;
void aquire_mouse_lock();
void free_mouse_lock();
private:
void volatile_draw();
void volatile_undraw();
void hide_override(bool value = true);
CVideo* video_;
std::vector< surface_restorer > restorer_;
SDL_Rect rect_;
mutable bool needs_restore_; // Have we drawn ourselves, so that if moved, we need to restore the background?
enum { UNINIT, HIDDEN, DIRTY, DRAWN } state_;
bool hidden_override_;
bool enabled_;
bool clip_;
SDL_Rect clip_rect_;
bool volatile_;
std::string help_text_;
std::string tooltip_text_;
int help_string_;
std::string id_;
bool mouse_lock_local_;
static bool mouse_lock_;
friend class dialog;
};
}