Ideally, I'd like this to be controllable via the Scroll-Wheel of the mouse as well as with keyboard shortcuts.
Sunday, 30 August 2009
Zoom!
Ideally, I'd like this to be controllable via the Scroll-Wheel of the mouse as well as with keyboard shortcuts.
Saturday, 29 August 2009
Making the most of the screen
Wednesday, 26 August 2009
Tuesday, 25 August 2009
Refactoring Progress #3
Here is the current progress:
Friday, 21 August 2009
Thursday, 20 August 2009
The Crawl Poll
1. What is you age?
Let's just say Thirty-something :-)
2. Which country do you live in?
The United Kingdom.
3. Do you play locally, on a server or both?
Usually locally. I've tried playing on CAO, but the intricacies of setting up terminal options so that they're just so discourages me a bit.
I do like to be the voyeur and watch others playing though.
4. Do you play Tiles or ASCII or both?
Both. I would love the full screen mouse-tile interface to work with an ASCII version though. ASCII tiles perhaps? (hinthint)
5. Which Operating Systems do you use at home?
Primarily Windows (Vista, urgh...it came with the laptop, its not a patch on XP). I do have it dual-installed with Ubuntu which I do occasionally use (I was a former UNIX monkey. And before you ask, XEmacs).
6. Which Roguelikes have you played before? (Nethack, ADOM, Angbandetc.)
Of the big ones (and in order of time spent playing in descending order): Crawl, TOME, Angband, Unangband, Sangband, ADOM, Rogue.
And lots and lots of the minor ones.
You'll notice no Nethack. That's deliberate. I'm also especially crap at ADOM.
7. Where did you learn about Crawl?
I think it was actually r.g.r.m.
8. And when?
A long time ago, but not that long ago - I think it was just after Stone Soup came out and I had gotten back into roguelikes after a long time away. I should really play some b26 as well to see what that feels like in comparison to SS.
9. How often did you win Crawl? (If none, you may specify your best game.)
Win? You're having a laugh! My best was a 2-rune MDFi 19 that died due to "let's-die-from carelessness-for-no-good-reason-at-all-because-I-didn't-quite-have-my-Crawl-head-after-shortly-commencing-another-session-of-play-the-next-day" syndrome.
10. If you take part in the Crawl tournament: where did you hear about it?
I don't take part mainly due to lack of time and my own general crapness at Crawl.
11. Did you ever recommend Crawl to someone?
Yes, several. Including my other half, who has recently given up playing due to Orc Priests getting nerfed.
12. Which computer game played most in the last month (July)?
In July? Hardly any. Though I did reinstall and subsequentlydelete Civilisation 4 for what seems like the umpteenth time.
Big or small levels?
So...should Kharne keep its current (IMHO) big levels or move to smaller levels?
(if it helps the discussion, I intend for the shortest path through the dungeon to be forty levels, split over four dungeon branches, and for the maximum character experience level to be XL20).
Wednesday, 19 August 2009
On rehabilitating old roguelikes...
Both of these have not been updated for a long time (Saladir since 1998, NW since 2005.) If I wasn't so involved with a part-time degree, coding Kharne, and occasionally dabbling in C#, I'd be very keen on taking over one of these projects.
The code for Nethermost Wanderings on the other hand, is freely available. Perhaps someone else out there is masochistic enough to try some new development? (although it doesn't say anything on the website, when you look at the current CVS source, it has actually been GPLed)
Mind you, I hope in years to come people don't talk about Kharne in the same way...but at least by releasing the source code, if something does happen, at least it has a chance of living on.
Saturday, 15 August 2009
Refactoring Progress #2
Friday, 14 August 2009
More on Item Effects
For example, consider an amulet that grants +alertness and +reslifedraing.
This would be the display in the message log when you wear-identify it:
You see an Ornate Copper Amulet here
You pick up an Ornate Copper Amulet
You now have an Ornate Copper Amulet
You put on an Ornate Copper Amulet
You feel more resistant to lifedraining
You feel more alert
It is an Copper Amulet of Being!
You take off an Copper Amulet of Being
You feel less resistant to lifedraining
You feel less alert
Thursday, 13 August 2009
Refactoring Progress #1
(yes, that is the View->Project Manager dialog for the main Kharne executable)
And here is the refactored version:
function TMonster.GetSinglePrefix: string;
begin
if (Length(Trim(UniqueName)) <> 0) then Result := ' ' else
begin
if (MonsterName[1] in ['A', 'E', 'I', 'O', 'U', 'a',
'i', 'e', 'o', 'u']) then
Result := 'an '
else
Result := 'a ';
end;
end;
{ Return the singular form of the prefix }
function TMonster.GetSinglePrefix: string;
begin
{ Logging }
hLog.Add('{now} {lNum} TMonster.GetSinglePrefix()');
{ Default result }
Result := '';
try
{ Again, Uniques do not have prefixes }
if (FUnique) then
Result := ' '
else
begin
{ Deal with vowels }
if (Vowel(FName)) then
Result := 'an '
else
Result := 'a ';
end;
except
{ in case of error, log the Exception }
on E: Exception do hLog.AddException(E);
end;
end;
Wednesday, 12 August 2009
Refactoring and the MPL
- I don't want Kharne to die through any hypothetical future lack of involvement on my own part
- I welcome the scrutiny that other people reading and checking my code brings
- I want there to be at least one Roguelike out there written in Delphi
However, releasing something as Open Source doesn't just mean tacking the appropriate headers onto the top of each unit of source code. For better or for ill, the current Kharne source code is very much like the proverbial Curate's Egg. Some of it is spellbindingly excellent and some of it is just plain awful. So prior to release of the code, I'll engage is a massive code refactor and tidyup.
This refactor is something I've already started. But what does this it entail?
- General Code Tidyup and Commenting
- Adherence to Coding Standards
- Removing Redundant Units
- Proper Encapsulation of all Classes
- Abstraction of Procedural and non-Class code into Classes (where possible)
- Logging and Error Handling
Now all this will take time, but I don't expect there to be much delay in the release of future releases, if any at all (didn't expect me to say that, did you?). The pain now will be worth the gain in the future.
Tuesday, 11 August 2009
Logging and Locking up Issues
Not only do I intend to log errors and exceptions, I also intend to log, as much as possible, the complete execution of the program as well.
So far, I've only converted a couple of source code units to use Logging, but there is some sample output below. This will be written to a log file in the same directory as KHARNE.EXE.
2009-08-11 00:01:44 1 TVault.Create(1, 'Round')
2009-08-11 00:01:44 2 TVault.Create(2, 'Octagon')
2009-08-11 00:01:44 3 TVault.Create(3, 'Square')
2009-08-11 00:01:44 4 TVault.Create(4, 'Open')
2009-08-11 00:01:44 5 TVault.Create(5, 'Lesser Cell')
2009-08-11 00:01:44 6 TVault.Create(6, 'Left Diagonal')
2009-08-11 00:01:44 7 TVault.Create(7, 'Right Diagonal')
2009-08-11 00:01:44 8 TVault.Create(8, 'Octagon')
2009-08-11 00:01:44 9 TVault.Create(9, 'Rooms')
2009-08-11 00:01:44 10 TVault.Create(10, 'Central')
2009-08-11 00:01:44 11 TVault.Create(11, 'Intermeshed')
2009-08-11 00:01:44 12 TVault.Create(12, 'Turns')
2009-08-11 00:01:44 13 TVault.Create(13, 'Tiny')
2009-08-11 00:01:45 14 TVault.Create(14, 'Small Entry')
2009-08-11 00:01:45 15 TVault.Create(15, 'Small')
2009-08-11 00:01:45 16 TVault.Create(16, 'Long Pit')
2009-08-11 00:01:45 17 TVault.Create(17, 'Entry')
2009-08-11 00:01:51 18 UnitVault.GetRandomVault(0, 0)
2009-08-11 00:01:51 19 TVault.GetVaultMap()
2009-08-11 00:01:51 20 UnitVault.GetEntryVault()
2009-08-11 00:01:51 21 TVault.GetVaultMap()
Adding logging such as this is another task I need to do before I MPL the source code.
Monday, 10 August 2009
Andrew Doull on Permadeath
Sunday, 9 August 2009
Some thoughts on Implementation of Potions
In this post I'll be talking about potions in particular. I've already defined them previously, but I want to talk more about their implementation. (Incidentally Kharne will follow the usual Roguelike concepts with potions for the time being. Eventually, I do want to allow them to be used also in some sort of Alchemy-based crafting system, much like that is found in some MMORPGs)
So, to begin with, these are the potions I intend to implement for the next release:
- Potion of Healing: Andrew Doull of Unangband fame (and much more besides) commented a while back on the role of healing in Angband. I don't intend to deviate from his analysis too much, but I propose that standard Potions of Healing (which should be the commonest potion type found) shouild heal a flat 50% of lost HP instantly.
- Potion of Extra Healing: Like its lesser brother, this will restore HP. instantly I'm minded to make this restore 100% of lost HP. Unlike the original Rogue, I'm not intending to allow maximum HP to be increased by chugging Potions at maximum health.
- Potion of Curing: In Crawl, healing potions cure effects like poison and sickness, but I'm not keen on that usage as I feel it leads to using up valuable potions. I think the alternative method of providing a specialised potion to cure negative status effects would be useful instead (if it turns out that this is a bad idea, I'd be willing to change healing potions to work like Crawl's)
- Potion of Regeneration: This was proposed by Andrew in the post mentioned above, and I'm also intending to implement it in Kharne. This should work like Crawl's Trog's Hand. For X turns, you regenerate 10% of maximum health back a turn.
- Potion of Speed: For X turns, the player can move and act at 3 times his/her normal speed.
- Potion of Restore Abilities: This potion will remove any negative status effects upon the player's being (e.g. if they are drained of strength by a creature, like Shadows used to do back in the days of AD&D). (This will require some additional coding for the class that holds the player to include a temporary buffer pool for each stat - it is this buffer pool that gets adjusted for temporary effects like strength drain and NOT the actual player's inherent strength).
- Potion of Confusion: This will mimic the standard Confusion effect found in many Roguelikes, and lasts for X turns. This is actually very easy to implement, as it just requires a check on keypress to see if a movement key has been pressed, and if so, randomly choose another direction instread)
- Potion of Blindness: Again, another potion with unbeneficial side effects that lasts for X turns. Technically speaking, this is just a matter of reducing the effective FOV radius to 0 squares (instead of the standard 7).
- Potion of Paralysis: Paralysis will prevent you from acting. (I also intend it to be a monster attack posessed by Ghouls). Again lasts for X turns.
- Potion of Free Action: Whilst under the effects of this potion, you cannot be paralysed or slowed. Another potion with a duration of X turns.
- Potion of Combat Mastery: Massively increase the player's chance to hit for a certain number of turns. To balance this out, I feel the duration should not be as long as other potions, perhaps only 10-20 turns.
- Potion of Reflexes: A defensive as opposed to an offensive potion. I intend for this to boost evasion for a limited period. Evasion is the stat used to determine if an attack hits or not, and is affected by armour worn, dexterity and other factors. Unlike Armour Class, its actually a logarithmic stat.
- Potion of Might: Massively increase the player's damage for a certain number of turns. Like Potions of Combat Master, I feel the duration should not be as long as other potions, perhaps only 10 turns maximum.
Thoughts?
Saturday, 8 August 2009
Implementing Transient Events - an update
{ Decrease the duration of the timer by a turn }Moving the decrement of the tick timer to after the tick events means that it now correctly works with a duration of one turn - in these circumstances previously only the Start and End Events would be fired.
function TGameTimer.Tick: Boolean;
begin
{ Trigger events if they are defined }
if (FTimerDurationLeft > 0) then
begin
{ Interval Event }
if (Assigned(FTimerTickEvent)) then
FTimerTickEvent(Self);
end
else if (FTimerDurationLeft = 0) then
begin
{ End Event }
if (Assigned(FTimerEndEvent)) then
FTimerEndEvent(Self);
end;
{ Decrement the Counter }
Dec(FTimerDurationLeft);
{ Return true if the Timer is still active }
Result := GetStatus;
end;
The interface to the Event Hooks themselves have changed
{ Procedure Pointer for Event Hook }
type TGameTimerEvent = procedure(const GameEvent: TObject);
This allows passing in of the triggering TGameTimer object (referenced by Self in the tick method, and subsequently cast to TGameTimer in the actual hook).
For those of you familiar with Delphi this is pretty much (with a few minor differences) what TNotifyEvent does.
I've also removed the Progress property entirely. Since the Tick event is designed to be called every game turn then its simply a matter of comparing FTimerDuration and FTimerDurationLeft as needed there instead.
Friday, 7 August 2009
Kharne 0.03c Available
(edit: see the addendum at the bottom of this post if you downloaded this version before midnight GMT on the 7th)
Although this version doesn't have the promised potions and scrolls implemented, the timing functionality is now included and I hope to release the next version soon.
Here are the changes in this version:
- Added milestones to the Character Log.
- Multiple Items can now exist on the same square.
- Firing a ranged combat weapon now correctly updates the turn count.
- The maximum damage a monster can do to a player in a single hit is now (Maximum Hit Points - 1).
- Piles of Silver and Bronze can now be found throughout the dungeons.
- Items have been reduced in cost by a factor of five.
- There is now a wider variety of entry vaults.
- Amulets can now be found in fountains.
- Combat Text is now much more descriptive.
- Monster attacks that hit you and cause damage are now displayed in a different colour.
- Co-ordinates are now only displayed if the Wizard Window is displayed.
- The number of hit points and magic points gained per level has been slightly increased.
- The effect on speed of carrying weights greater than maximum load has been reduced slightly.
- Monsters will use ranged attacks less often.
Thursday, 6 August 2009
Implementing Transient Events: An example
The default radius of the FOV for this demo is 3 squares (normally it is 7), and at the start of the event, this is increased to 9. Half-way through (15 turns later), this is reduced to 6, and then 15 turns after that, the radius is reduced to 3. Appropriate messages are displayed in the messagelog.
I'll go through the code required to produce this in my next post.
Implementing Transient Events: The TGameTimer class
Hence the TGameTimer class.
I've made the full source code available here and here, and also listed it below (you may need to copy and past to get around wordwrapping issues). Although written in Delphi, it should compile in FreePascal without too many changes (if any). I'm releasing it as public domain, so feel free to use and abuse it as you see fit.
Basically, the class uses good ole' Procedure Pointers to allow (optional) event hooks to occur in three scenarios:
- Upon Event Creation
- Upon an Event Tick
- Upon Event Expiry
The constructor of the object takes an event type and an event duration in turns. as well as the event hooks themselves.
If anyone wants a demo app showing the class in action, let me know and I'll post one. Otherwise, you'll have to wait until the next major release of Kharne, when I intend to use TGameTimer to implement potion functionaility (finally!).
I'll do a followup post over the weekend with some additional comments, and thoughts on how it can be improved. In the meantime, comments are most welcome.
{ Timer Handling
A TGameTimer represents a transient event that has a duration.
An example would be the side-effects of drinking a Potion of Might - it increases
strength for a limited number of turns. We optionally define a number of procedure
pointers to point to events that occur at the beginning and end of the duration,
and also on every turn.
For example, upon drinking a Potion of Might, the event might display a message
stating you feel mighty (as well as increasing your strength). As the duration
of the effects decrease, further messages will be displayed stating that the
effects of the potion are wearing off, and then when the duration has expired,
your strength reverts back to normal
To implement this, use the follwing steps as a guide:
1. Set up a variable:
DrinkMightPotionEvent: TGameTimer;
2. Define three events as follows:
procedure MightPotionDrink(const Turns: Integer = 0);
procedure MightPotionTick(const Turns: Integer = 0);
procedure MightPotionEnd(const Turns: Integer = 0);
procedure MightPotionDrink(const Turns: Integer);
begin
DisplayMessage('You feel mighty!');
Player.Strength := Player.Strength + 10;
end;
procedure MightPotionTick(const Turns: Integer);
begin
if (DrinkMightPotionEvent.Progress = 50) then
DisplayMessage('The effects are wearing off!');
Player.Strength := Player.Strength - 5;
end;
procedure MightPotionEnd(const Turns: Integer);
begin
DisplayMessage('You no longer feel so mighty!');
Player.Strength := Player.Strength - 5;
end;
3. Instantiate the event:
DrinkMightPotionEvent := TGameTimer.Create(timMight,
DURATION_MIGHT_POTION,
MightPotionTick,
MightPotionDrink,
MightPotionEnd);
4. Then, on every turn that passes, simply call
if (Assigned(DrinkMightPotionEvent)) then DrinkMightPotionEvent.Tick;
}
{ Define the types of timers as an enum for simplicity }
type TGameTimerType = (timSpeed,
timConfusion,
timBlindness,
timSeeInvisible,
timParalysis,
timFreeAction,
timCombatMastery,
timReflexes,
timMight);
{ Procedure Pointer for Event Hook }
type TGameTimerEvent = procedure(const Turns: Integer = 0);
{ Class Definition - it inherits the ICommonObject interface to gain access
to the StringValue method to allow easy persistance }
type TGameTimer = class(ICommonObject)
private
FTimerType: TGameTimerType; // Timer Type, from the enum previously defined
FTimerDuration: Integer; // Starting Duration, in turns
FTimerDurationLeft: Integer; // Duration Left, in turns
FTimerTickEvent: TGameTimerEvent; // Optional Event to call on each decrement
FTimerStartEvent: TGameTimerEvent; // Optional Event to call on starting the timer
FTimerEndEvent: TGameTimerEvent; // Optional Event to call on ending the timer (i.e. turns left = 0)
{ Private functions to support class properties defined below }
function GetStatus: Boolean;
function GetProgress: Integer;
public
{ Standard Constructor }
constructor Create(TimerType: TGameTimerType;
TimerDuration: Integer;
TimerTickEvent: TGameTimerEvent = nil;
TimerStartEvent: TGameTimerEvent = nil;
TimerEndEvent: TGameTimerEvent = nil);
{ Decrement the Timer by one turn. Will return true if the timer has not expired }
function Tick: Boolean;
{ Interface Method for Persistance }
function GetStringValue: String;
{ Properties }
property TimerType: TGameTimerType read FTimerType; // Return the enum
property TimerDuration: Integer read FTimerDurationLeft; // Number of Turns left
property TimerProgress: Integer read GetProgress; // Number of Turns left as a percantage (0-100) of the original duration
property Active: Boolean read GetStatus; // True if Number of Turns left is > 0
end;
implementation
{ Standard Constructor - this is deliberately the only way to set up the duration etc }
constructor TGameTimer.Create(TimerType: TGameTimerType;
TimerDuration: Integer;
TimerTickEvent: TGameTimerEvent;
TimerStartEvent: TGameTimerEvent;
TimerEndEvent: TGameTimerEvent);
begin
{ Load the private member data }
FTimerType := TimerType;
FTimerDuration := TimerDuration;
FTimerDurationLeft := TimerDuration;
FTimerTickEvent := TimerTickEvent;
FTimerStartEvent := TimerStartEvent;
FTimerEndEvent := TimerEndEvent;
{ If we have a start event defined then execute it now }
if (Assigned(FTimerStartEvent)) then
FTimerStartEvent(FTimerDurationLeft);
end;
{ Return true if the timer hasn't expired }
function TGameTimer.GetStatus: Boolean;
begin
Result := FTimerDurationLeft > 0;
end;
{ Decrease the duration of the timer by a turn }
function TGameTimer.Tick: Boolean;
begin
Dec(FTimerDurationLeft);
{ Trigger events if they are defined }
if (FTimerDurationLeft > 0) then
begin
{ Interval Event }
if (Assigned(FTimerTickEvent)) then
FTimerTickEvent(FTimerDurationLeft);
end
else if (FTimerDurationLeft = 0) then
begin
{ End Event }
if (Assigned(FTimerEndEvent)) then
FTimerEndEvent(FTimerDurationLeft);
end;
{ Return true if the Timer is still active }
Result := GetStatus;
end;
{ Returns the number of Turns left as a percantage (0-100) of the original duration }
function TGameTimer.GetProgress: Integer;
var
Percentage: Double;
begin
Percentage := FTimerDurationLeft / FTimerDuration;
Result := Trunc(Percentage * 100);
end;
{ Get the Timer as a String }
function TGameTimer.GetStringValue: String;
begin
{ TODO: We will need to extend this to allow hashing of the attached procedures }
Result := Format('%d%d', [Ord(FTimerType), FTimerDuration]);
end;
Flavoured Yummy Combat
Wednesday, 5 August 2009
Sprucing up Monster AI #1
Put simply:
Allow certain monsters to flee away from the player if they are sufficiently injured. If there are other monsters visible that belong to the same ecology, then flee towards them. Else flee directly away from the player. If a fleeing monster cannot move away from the player, then it turns and fights.
Print fleeing/unfleeing messages as appropriate ("the wibbly flees!", "the wibbly turns and fights!")
Now, there is some additional subtlety I intend to code. If there is a Unique Monster within line of sight, injured monsters don't flee but instead shout something along the lines of "We will die for you, Wibbly the Doomy!"
Perhaps the opposite effect could also occur - when a Unique dies, all sentient monsters within view flee with a "Seeing its Champion dead, the wibbly runs away". Obviously this would not be a permanent fleeing, but of time-limited duration.
(cross-posted at r.g.r.d.)
Tuesday, 4 August 2009
Spicing up ASCII: Combat
I've had a similar idea in mind for Kharne for a while now. To summarise:
- if you take damage, the character's symbol (@ of course) flashes red briefly. How deep a red depends upon how much damage has been taken.
- if you inflict damage, the symbol of the monster that has taken damage flashes red briefly. This is not graduated.
- if a monster attacks you, its symbol changes briefly to a green colour.
- if a monster has a status effect, for example, held, paralysed, sleeping, poisoned, then its ascii character is shaded a different colour.
Flickr & New Links
Monday, 3 August 2009
Sunday, 2 August 2009
Preview: Character Milestones
15328 | Lair:9 | Identified the +2 helmet of Intuition (You bought it in a shop on level 9 of the Lair of Beasts)
15332 | Lair:9 | Identified the cursed -2 pair of boots of Xacriyts (You bought it in a shop on level 9 of the Lair of Beasts)
15336 | Lair:9 | Identified the +2 cloak of Unreason (You bought it in a shop on level 9 of the Lair of Beasts)
15882 | Lair:10 | Entered Level 10 of the Lair of Beasts
16246 | Lair:10 | Reached skill 16 in Axes
16600 | Slime:1 | Entered Level 1 of the Pits of Slime
16883 | Snake:1 | Entered Level 1 of the Snake Pit
16958 | Snake:1 | Reached XP level 13. HP: 86/118 MP: 8/8
17470 | Snake:1 | Noticed Rupert
17495 | Snake:1 | Defeated Rupert
18143 | Snake:2 | Reached skill 10 in Armour
18393 | Snake:2 | Reached skill 17 in Axes
18409 | Snake:2 | Received a gift from Trog
20544 | Snake:5 | Entered Level 5 of the Snake Pit
20837 | Snake:5 | Reached XP level 14. HP: 128/128 MP: 9/9
21258 | Snake:5 | Reached skill 1 in Spellcasting
21683 | Snake:5 | Reached skill 18 in Axes
21694 | Snake:5 | HP: 10/256 [naga warrior/uncursed club (6)]
21698 | Snake:5 | HP: 4/128 [poison]
22626 | Snake:4 | HP: 6/128 [poison]
22631 | Snake:4 | HP: 4/128 [poison]
22636 | Snake:4 | HP: 5/128 [poison]
24007 | Snake:5 | Got a serpentine rune of Zot
25141 | Swamp:5 | Entered Level 5 of the Swamp
25278 | Swamp:5 | Reached skill 19 in Axes
25522 | Swamp:5 | Reached XP level 15. HP: 64/138 MP: 12/12
25817 | Swamp:5 | Got a decaying rune of Zot
So far, I have:
- Starting out
- Dying
- Gaining an XP level
- Gaining rank 5/10/15 etc in a skill
- Noticing a unique monster
- Killing a unique monster
- Finding an artifact
Turn Place Note
--------------------------------------------------------------
0000191 The Wilderlands 1 Identified The ancient artifact "Ethaedric"
0000396 The Wilderlands 1 Noticed Adrialith the Wrathful
0000409 The Wilderlands 1 Killed Adrialith the Wrathful
0000409 The Wilderlands 1 Reached Level 2, HP: 11/11, MP: 21/21
0000571 The Wilderlands 2 Explored Level 2 of The Wilderlands
0000709 The Wilderlands 2 Noticed Etib the Tormentor
0000709 The Wilderlands 2 Killed Etib the Tormentor
0000814 The Wilderlands 3 Explored Level 3 of The Wilderlands
0000841 The Wilderlands 4 Explored Level 4 of The Wilderlands
0000933 The Wilderlands 4 Reached Level 3, HP: 15/15, MP: 32/32
0000938 The Wilderlands 5 Explored Level 5 of The Wilderlands
0001119 The Wilderlands 6 Explored Level 6 of The Wilderlands
0001384 The Wilderlands 6 Gained Skill Level 5 in Heavy Armour
0001386 The Wilderlands 6 Reached Level 4, HP: 19/19, MP: 43/43
0001672 The Wilderlands 7 Explored Level 7 of The Wilderlands
0002151 The Wilderlands 7 Killed by a Desperate Adventurer
I think it looks pretty sparse, so far. So...what character actions are worthy of milestones?